@passioncode-ai/fabric-agent-adapter 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +224 -0
  2. package/README.md +6 -5
  3. package/SECURITY.md +6 -5
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +9 -7
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +43 -8
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +28 -7
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -1
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +222 -24
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
  47. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +4 -1
  48. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +139 -0
  49. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
  50. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
  51. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
  52. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
  53. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
  54. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
  55. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
  56. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
  57. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,229 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.56.0 (2026-10-28)
4
+
5
+ Stability and Windows: the debts of 0.53.0 to 0.55.0 that touch how the hooks, the board responder and the interop
6
+ gate behave.
7
+
8
+ ### Fixed
9
+
10
+ - The board responder's notices reach a service's event feed: they were sent at level `warn`, which
11
+ `JsonlEventLog.append` refuses (its levels are `info`, `notice`, `warning`, `error`), so `board.down`,
12
+ `board.access.needed`, `board.slot.moved`, `board.slot.contested`, `board.request.held`, `board.request.skipped`,
13
+ `board.reconcile.needed`, `board.attempt.lost` and `board.outcome.invalid` were dropped wherever `emit=log.append`
14
+ (the reference service included, since 0.28.0; `board.slot.moved` since 0.53.0). They are sent at `warning` now, in
15
+ both twins, as is the new `board.slot.refused` (FAA-163). Each notice names its `subject` (`{type, id}`: its request,
16
+ else its capability, else its episode, such as a block's cause, else the Project), so a host tells one request's,
17
+ capability's or cause's episode from another's. In Python the subject goes to an `emit` whose signature names a
18
+ `subject` parameter with no value bound (`JsonlEventLog.append` now names one); any other `emit` (only `**kwargs`, a
19
+ subject bound with `functools.partial`, a signature that cannot be read) is called as 0.55.0 called it, once, without
20
+ one. The Node responder always passes it in its options object, which an `emit` that does not read it ignores.
21
+ - **working-with-fabric on Windows** (FAA-175, FAA-177). A turn names the repositories it touched on Windows too:
22
+ Git Bash `/c/...` and `/cygdrive/c/...`, `C:/...` and quoted `C:\...` paths, and `git.exe -C` (any case); Bash
23
+ text is split with Git Bash's own quoting, so an unquoted backslash is an escape there too. A `cd` inside `( )`, a
24
+ pipeline's stage or a list sent to the background no longer moves the folder a later relative `-C` is read
25
+ against; a newline ends a command, a here-document's body is text (a `#` comment no longer hides its lines, and
26
+ `<<<` opens none), `&>` only redirects, `git -C /a -C b` runs in `/a/b`, and `pushd` and a `cd` after `{` or `then`
27
+ are read. Cygwin and MSYS Python read paths as POSIX. Before, the Stop hook did not read the
28
+ transcript on Windows at all, and a turn from Windows came without `repositories`. The hooks run `python3`, which
29
+ must name Python 3.9 or later in Git Bash there (session-hooks.md; FAA-183 makes the plugin find it itself).
30
+ - **A relative `git -C` after `cd`** (FAA-166) is read against the folder the command last changed into with an
31
+ absolute `cd`; after a relative or unknown `cd` it is dropped, never read against the session's folder.
32
+ - **The board responder's recovery** (FAA-163, FAA-158). A refused enroll of one capability no longer stops the
33
+ others: it is retried on its own, the pause doubling, and said once as `board.slot.refused`. A slot's move is written
34
+ under the responder's lock and only while its record is unchanged, so an operator's take-back at the same moment
35
+ wins. The slot code moved to its own twin modules, `fabric_board_slots.py` and `fabric-board-slots.mjs`.
36
+ - **An idle stop leaves the board within 3 s** (FAA-173): a responder with no work in hand closes its session with a
37
+ confirmed `DELETE` inside the hard exit (`close(timeout)`: a timeout not above 0 is the session's own, and a session built
38
+ with one takes 15 s, in both twins),
39
+ instead of letting the slot lapse (a board call already in flight when the
40
+ stop comes runs first, and when the join ends before the DELETE the slot lapses as before). The slot records are forgotten only after
41
+ a 2xx: a 404 (the session already gone) keeps them, since a move made before it ended is read at the next
42
+ `com.status`. A refusal of the whole board session (`not_available` included) is the board down, never a refused
43
+ capability; a capability refused, or contested, again after it was held is said again, once per capability. A clean
44
+ leave forgets only the records this session confirmed (a slot it held, paused under the operator's hold included, or
45
+ one `com.status` showed unmoved) and no fenced call has put in doubt since, so a slot moved while the service was
46
+ down stays `moved` when the session is dropped at connect or recovery stops early, and is never enrolled again on
47
+ its own. A paused or retrying slot reads `com.status` before each new ask, so a move during the operator's hold is
48
+ said. `Checkpoint.leave(project, held)` takes `held` as a required argument in both twins.
49
+ - **`action_gate` fails closed** (FAA-139): a spend that is not a finite number is read as no estimate, which a
50
+ mandate's `perCallMaxUsd` ceiling refuses (a mandate with no ceiling covers it; the question is with the contract,
51
+ FAA-185), and a project that cannot be read is never covered by a mandate limited to `projects`. A `null` mandate in
52
+ `_meta` reads as an unreadable one, not as none (FAA-178; Python's `approval_step`, while a Node service passes `{}`
53
+ to `jobStep` itself, interop.md). A job's start (`job_step`) reads spend and project as its answer does, and keeps
54
+ the spend as it read it (a NaN is no estimate, never a bare `NaN` in the job file).
55
+ - **`fabric_secrets` matches the contract's ECMAScript reading** (FAA-136): `\s`, `.` and case folding as JavaScript
56
+ reads them, so both ports flag the same values. `secret_url` also reads a special scheme as `new URL` does: every
57
+ slash or backslash after `https:` (or `http`, `ftp`, `ws`, `wss`) is skipped and a backslash before the query reads
58
+ as a slash, so `https:u:p@host` and `https:\\u:p@host` carry a password in Python as in Node. Python splits the URL
59
+ by hand rather than with `urlsplit`, which refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), and drops
60
+ one leading `?` from a parameter's name as `URLSearchParams` does. Where `new URL` refuses a host (none, a port over
61
+ 65535), Python still reads the password and the parameters, so it can flag more than Node, never less. The
62
+ `source.repository` check of a descriptor and of a provider entry reads a special scheme's password the same way
63
+ (`https:bot:pw@host`), up to the user information's last `@` (`https://me@corp.example:pw@host`), and says so
64
+ before it judges the host. The query and the fragment are read percent-encoded, as `new URL` hands them over.
65
+ `fabric_http.redact_url` reads a special scheme the same way (below).
66
+ - **What the kit sends, refuses and redacts as a URL** (FAA-136; the remaining differences are FAA-187):
67
+ - `redact_url`: the URL an `HttpError` carries for `https:u:pw@host` has no password, as in Node; a URL of any
68
+ other scheme (a misspelled `htps:` included) keeps only its scheme in both twins (`htps:<redacted>`), since its
69
+ user information may sit anywhere in the rest. It first trims C0 controls and space from the ends and removes
70
+ tabs and line breaks, as `new URL` does, so ` https://u:pw@host` keeps no password on Python 3.9 either. A
71
+ string with no scheme (`ht tps:`, `1ttps:`, a leading byte order mark or no-break space) and a URL whose host or
72
+ port `new URL` refuses (a password holding `/`, a port that is not ASCII digits such as `1_2` or `+1`, which
73
+ Python 3.9's `int()` takes, a bracketed host that is no IPv6 address, a `ws`, `wss` or `ftp` URL with no host or
74
+ a port above 65535) read as `<unparseable url>`; the scheme is read by the WHATWG rule, not by `urlsplit`, whose
75
+ rule differs between 3.9 patch releases. On Python a host holding `%` reads as `<unparseable url>` too, where
76
+ Node writes it decoded. The path is not redacted in either twin (Node writes it as `new URL` normalises it,
77
+ Python as given), so a key never belongs in it.
78
+ - `check_url` and `request`: the same ports and bracketed hosts (an IPv6 zone included) are refused. On Python a
79
+ backslash, `%`, space or control character in the host (a Unicode space and a C1 control included) is refused
80
+ too, since urllib would decode it or dial another host (`h.example%3A8443` reached port 8443), and so is a space
81
+ or ASCII control character before the URL's first `#`, or a character outside ASCII in its path or query, which
82
+ urllib would send and http.client refuse: such a URL is refused before it is sent (kind `refused`), where 0.55.0
83
+ retried it and answered kind `connection`. Python's `check_url` checks the host's shape, not its name (FAA-187
84
+ (14), (16)). The Node twin reads several of these as `new URL` does, among them a tab or line break removed, a
85
+ space or a character outside ASCII percent-encoded, a backslash as `/`, an extra slash before the host skipped
86
+ and a `%` host decoded (refused when the decoded host is not allowed, as `h.example%3A8443`).
87
+ - What is sent: the default transport sends a URL up to its first `#` in both twins (urllib alone kept everything
88
+ before the last one, so `/x#a#b` asked for `/x#a`); a transport you pass receives the URL as given. ASCII
89
+ whitespace at the ends, which urllib and `new URL` both trim, is trimmed, so a URL ending in a line break is still
90
+ sent, as in 0.55.0 (on Python 3.9 a URL with a leading space too, which 0.55.0 refused there). On Python a URL
91
+ whose part before its first `#` ends in a Unicode space (a no-break or ideographic space) is now refused; 0.55.0
92
+ sent it trimmed when the space ended the URL and failed every call when a fragment followed it. Node keeps it
93
+ percent-encoded.
94
+ - The OpenRouter clients trim a base URL's ends first: ASCII whitespace and trailing slashes in both, and on Node
95
+ every C0 control there and a tab or line break anywhere, as `new URL` reads them. So `.../v1\n`, `.../v1/ ` and,
96
+ on Node, `.../v1\n/` are sent to `.../v1/chat/completions`. Both then refuse, when they are built, a base URL
97
+ holding a query or a fragment (kind `config`, read before `check_url`; `/chat/completions` would land in it, so a path-routed endpoint
98
+ such as OpenRouter failed every call in 0.55.0). Python also refuses, through `check_url` (kind `refused`), a
99
+ base with a space, a control or a non-ASCII character in its path, which 0.55.0 accepted and then failed every
100
+ call with; Node sends such a path percent-encoded, as 0.55.0 did (FAA-187 (18)). Attribution answers `not-openrouter` for a port of other
101
+ digits (`:٤٤٣`), as `new URL` reads it.
102
+ - A JWKS source: the Python kit reads its host and port as urllib dials them, https included. It refuses a space
103
+ or ASCII control character before the URL's first `#` (it reads the URL up to there, as Node does), a character
104
+ outside ASCII in its path or query, a host holding `%`, a backslash, a Unicode space or a control character, a
105
+ bracketed host that is no IPv6 address, a port that is not ASCII digits or is above 65535, no host, and any `@`
106
+ (an empty userinfo dialled `@127.0.0.1`). A trailing ASCII space or line break is trimmed, as urllib reads it; a trailing Unicode space
107
+ before the first `#` is refused for the same reason as in `check_url`, though urllib would trim it and read the
108
+ keys when it ends the URL; a leading space is refused as not a URL, as in 0.55.0 and in Node. Neither `[::1%...]` nor
109
+ `http://[::1]%2Eevil.example/` (which Python 3.9's `urlsplit` reads as `::1`) nor a mapped `[::ffff:127.0.0.1]`
110
+ (loopback to Python 3.13+) counts as loopback. The Node kit accepts several of these as `new URL` reads them,
111
+ among them a `%` host decoded, a backslash as `/`, a tab or line break removed, an empty userinfo dropped and an
112
+ extra slash before the host skipped (`https:///j` reads host `j`).
113
+ - **A too-deep protocol value** is refused by name (FAA-122), in both twins.
114
+ - **A job's approval step and its answer read the mandate under the same name** (FAA-179): the job's stored
115
+ capability, also when a handler created the job under another name; `on_fault` hears both under it, not the answer
116
+ under `fabric.job.get`.
117
+ - **Nudges** (FAA-143): `lost=` counts every reminder that did not make it back to the file (this turn's capped lines,
118
+ and the pending ones when the write-back fails too); an answer's nudges are read up to 1,000 items, in linear time.
119
+
120
+ ### Added
121
+
122
+ - `FABRIC_SESSION_NO_REPOSITORIES=1`, or a `no-repositories` file in the hooks' folder (which a cache cleaner may
123
+ remove), turns the transcript read off (FAA-160); `fabric_session.py status` says
124
+ whether it is on (only while a running Fabric advertises `session-hooks/1`, `fabric` is registered for the folder and
125
+ Fabric takes the repositories), off, waiting for Fabric, or not done because `fabric` is not registered. The repositories of a turn can miss one (FAA-174): a turn longer
126
+ than the transcript's last 512 KB names only what that tail shows; until FAA-184 they can also name a wrong one,
127
+ since a relative `git -C` in a later Bash call is read against the session's folder.
128
+ - The reference service's dashboard shows a moved slot with a Take back button (`POST /api/board/take-back`, operator
129
+ only; FAA-157), and the button has its tool: `board.take-back`, a `relayed` action an agent whose client sets `_meta`
130
+ calls with the operator's decision (Claude Code and Codex reach the page until Fabric forwards decisions, CT-125).
131
+ The tool is a manifest capability too (effect `change-policy`, idempotency `required`), as every tool the well-known
132
+ lists must be: `register --board-project <uuid>` writes it into the manifest, so `check_service.py` passes the sample
133
+ with its board on. The manifest stays register's to write: a serve whose board mode differs from it at its start
134
+ logs `service.manifest.mismatch` (warning), and the probe fails until it is registered again. Its `/mcp` reads a loopback call the service token authenticated as T0. The
135
+ page, the route and the tool read a move from the checkpoint's record too, so a take-back made after a restart and
136
+ before the session reconnects (or while Fabric is down) is kept; a checkpoint it cannot write is a tool error. A
137
+ repeat after a lost answer is answered `{capability, moved: false}` by the route as by the tool, and each take-back is
138
+ a `board.slot.returned` event (the capability as its subject) whose sentence says the route in words and whose `data`
139
+ holds `by`, `via` and the `caller` (none when the feed refuses the caller), never the decision's words. The service installs its stop handler before its board thread starts, so a SIGTERM at any moment drains and leaves the board session. The
140
+ page's CSP allows the button's own `fetch` (`connect-src 'self'`), and a failed request says so on the button.
141
+ - working-with-fabric Step 4: a `fabric_task_create` that ended in an error may have filed the piece, so look for it
142
+ by title in `fabric_work_list` before filing it again (FAA-168), with the eval `a-failed-create-may-have-filed`.
143
+
144
+ ### Changed
145
+
146
+ - working-with-fabric names Fabric v0.3.5 as the release that brought `fabric-mcp` and the `fabric_work_*` tools, not
147
+ 0.4.0 (the plan milestone), and goes by whether the tools are listed (FAA-170).
148
+ - The `board.slot.moved` notice says the accepted work stays `accepted` (not "held", the operator's word) and that
149
+ after its take-back action a restart or reconnect while accepted work waits takes the slot back at once; a restart
150
+ alone does not (FAA-164, FAA-171);
151
+ project-board.md says the same to a requester. board.md says a take-back without the operator's relayed decision is
152
+ refused. session-hooks.md gives the Bash cap as 64 K characters and the downgrade window as until a worker next
153
+ reaches the running Fabric.
154
+
155
+ ### Upgrading
156
+
157
+ - On Windows the Stop hook now reads the transcript's tail and the touched repositories' `origin` (FAA-175), once
158
+ Fabric takes `repositories`, as it does on macOS and Linux; `FABRIC_SESSION_NO_REPOSITORIES=1` (in Claude Code's
159
+ settings `env`, which reached hooks declared in settings and in a plugin's `hooks.json` in runs on macOS; Windows is
160
+ FAA-183's run) turns it off.
161
+ - A Python `JwksCache` source is now refused when the cache is built, so a service that builds it at start stops
162
+ there, if it is any source the JWKS source item under Fixed lists. Among them, 0.55.0 read: a host holding `%` (an
163
+ IPv6 zone included, as in `http://[::1%25lo0]/`), which urllib decoded and dialled; a port that is not ASCII digits
164
+ (`:1_8080`); a bracketed IPv4 host (`[127.0.0.1]`, which Python 3.9 dialled as `127.0.0.1`); a mapped
165
+ `[::ffff:127.0.0.1]` loopback (on Python 3.13+); a Unicode space ending the URL. Write the host and port plainly
166
+ (`127.0.0.1:18080`) and trim the space. The other sources it lists 0.55.0 accepted and then answered every
167
+ delegated call `issuer-unavailable`.
168
+ - On Python, `check_url` and `request` now refuse before sending (kind `refused`) a URL that 0.55.0 sent: a host
169
+ holding `%` (`h.example%3A8443` reached port 8443), a port that is not ASCII digits (`:1_2` reached port 12 on
170
+ 3.9), a bracketed IPv4 host (`[127.0.0.1]`, dialled on 3.9), and a URL whose part before its first `#` ends in a Unicode space (sent trimmed). A URL with a space, a
171
+ control or a non-ASCII character in its path or query, which 0.55.0 retried and answered kind `connection`, is
172
+ refused the same way. Both OpenRouter clients refuse, when they are built, a base URL holding a query or a
173
+ fragment, and the Python client one with a space, a control or a non-ASCII character in its path, so a service
174
+ that builds its client at start stops there. Write the host and port plainly, trim the space, drop the query or fragment where the URL is
175
+ configured, and percent-encode a space, a control or a non-ASCII character in each path segment or query
176
+ value (`urllib.parse.quote(value, safe="")`, `urllib.parse.urlencode` for a query), never the whole URL.
177
+ - A service on the kit joins its responder thread at stop, bounded inside its hard exit, as `sample_service.py` does
178
+ (board.md, "Wiring it into the service"); on Node it aborts the run's signal and awaits the run. The idle leave above
179
+ happens only then. A service that stops without the join still works; its
180
+ slot lapses on the board's clock as before.
181
+ - A service that calls `Checkpoint.leave` itself now passes the generations it confirmed (`held`, required in both
182
+ twins); the responder's own leave does this.
183
+ - A service's own `emit` that matched the board responder's level `warn` now sees `warning`, with a `subject` when its
184
+ signature names one. A service with `emit=log.append` now logs the board's notices, and those with `notify` (access
185
+ needed, a contested or moved slot, a held request, a reconcile needed) reach the host's banners, without a link to
186
+ the page that resolves them (FAA-187 (21)). A Fabric outage's `board.down` (no notify) is now a row in the feed too,
187
+ said again after each recovery (FAA-187 (22)).
188
+ - A reference service served with `--board-project` registers again with the same `--board-project`
189
+ (`sample_service.py register ... --board-project <uuid>`): a registration from 0.55.0 lacks `board.take-back`, so
190
+ `check_service.py` fails `interop.well-known-capabilities` and the log says `service.manifest.mismatch`. Register
191
+ before you serve: the service checks its manifest only when it starts, so a warning logged before the registration
192
+ stays in the log until the next start.
193
+ - A service that calls `action_gate` (`actionGate`) itself passes a finite `spend_usd` and a string `project`, or
194
+ expects the operator's decision under a mandate with a `perCallMaxUsd` ceiling or a `projects` limit; an `on_fault`
195
+ that matched a job's answer under `fabric.job.get` now matches the job's capability; a descriptor's or provider
196
+ entry's `source.repository` names the repository without a password in its user information (`https:bot:pw@host`
197
+ is refused).
198
+
199
+ ## 0.55.0 (2026-10-27)
200
+
201
+ ### Added
202
+
203
+ - **creating-fabric-agents: agents that answer people** (FAA-162, board request `1ad39d4e`). A new reference,
204
+ `references/agents-that-answer-people.md`, lists what an agent that answers people (a support chat, mail, store
205
+ reviews, a group) carries from its first release, each part with the check that proves it: a turn record with a trace
206
+ id and a fixed outcome list, one `support.quality_daily` per closed UTC day and channel with `notMeasured` named,
207
+ model lanes whose fallback, checker and judge are other vendors with fallback recoveries counted, a writer change only
208
+ after an offline replay, false-state gates before the model checker, a checker that sees the material before the
209
+ instructions, numbers and account state from tools, the live tail re-read before sending, repair then research then a
210
+ hold that comes back by itself, hand-offs by cause and never as answers, a commitments ledger, a knowledge client that
211
+ reads the service's `answer` flag, a gap queue, and a feed re-read when a reader starts on a new event kind. Its
212
+ sources are agent-stack's support quality standard and the practices from an audit of five such agents, linked at
213
+ their commits (the practices at agent-stack PR #70 until it merges). Step 0 of the skill gains the row "Does it answer
214
+ people?" and Step 1 links the reference. New scenario eval `an-agent-that-answers-people` and its prose test
215
+ (`test/test_answering_agents_prose.py`), which fails when a part or its check is removed. The support-agent recipe now
216
+ says it is the draft-and-check step of such an agent, not the agent: used alone it hands the message to a person at
217
+ the first refusal (the refused draft is not kept), and its Lookup is a relevance rule an agent that answers people
218
+ replaces with the knowledge service's `answer` flag. It says the calling service routes `policy` before it, maps its
219
+ hand-offs to causes (a failed check goes to repair and counts as `inability` only at a hold on the material, a hold
220
+ after an outage or at the research time cap being `infrastructure`; `no-knowledge` and `model-unsure` are `inability`
221
+ and open a gap; a money reason is never researched; `model:*` is `infrastructure`, `model:ceiling` and `model:payment`
222
+ also the `limit` outcome), and tells such an agent to pass an `on_handoff` that redacts what it stores, in place of
223
+ the outbox, and to keep 30 days or fewer; `from_config` takes `on_handoff=`, and a call to `answer` from inside
224
+ `on_handoff` is refused at once with `ReentryError` rather than hanging (copy `support_agent.py` again to get both);
225
+ building-fabric-services and the README point at it. Backlog FAA-116 records what is left for the recipe.
226
+
3
227
  ## 0.54.0 (2026-10-27)
4
228
 
5
229
  ### Changed
package/README.md CHANGED
@@ -25,7 +25,7 @@ a terminal that works in a project through Fabric. It finds Fabric (`hub.json`,
25
25
  lifecycle broker start it when it is down, registers `fabric-mcp` once with no secret, calls `fabric_whoami`
26
26
  first, reads the project board (`com.status`, `com.list`, `com.read_ack` only what was read) and asks other
27
27
  Projects only through `com.submit`, asks who is working on what and claims its task before it starts
28
- (`fabric_work_list`, `fabric_work_claim`, Fabric 0.4.0), links tasks to backlog rows instead of copying them, and
28
+ (`fabric_work_list`, `fabric_work_claim`, Fabric v0.3.5 and later), links tasks to backlog rows instead of copying them, and
29
29
  releases at the end (`fabric_work_release`). `scripts/fabric_session.py status` prints where Fabric stands and one
30
30
  next action:
31
31
 
@@ -38,13 +38,14 @@ resume) puts that preamble into the session when the MCP server `fabric` is regi
38
38
  uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
39
39
  is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
40
40
  project that uses Fabric; Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
41
- and the board's unread count, and, once the running Fabric takes them (not on Windows), the other repositories the turn touched (it then
41
+ and the board's unread count, and, once the running Fabric takes them (unless `FABRIC_SESSION_NO_REPOSITORIES=1`), the other repositories the turn touched (it then
42
42
  reads the last 512 KB of the session transcript (message text and tool output included, which can hold a secret the session displayed; read in memory only), uses only tool names and path arguments, and sends host/owner/name, never a
43
43
  local path or text); UserPromptSubmit adds one line only when that count rose, and each reminder Fabric gave on a turn once (a turn `watch-mail` delivered prints there instead); SessionEnd releases what the
44
44
  session left claimed. Stop, UserPromptSubmit and SessionEnd spool and call Fabric only when the running Fabric
45
45
  advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched, never block the session, read no Fabric token (an `origin` URL's embedded credential is dropped in memory, never
46
46
  kept or sent) and always
47
- exit 0. `fabric_session.py watch-mail` waits for board mail under a background watcher, and `install-host-hooks
47
+ exit 0 once `python3` starts (on Windows, Claude Code runs hooks in Git Bash, or PowerShell without it, where `python3`
48
+ must name Python 3.9 or later; FAA-183). `fabric_session.py watch-mail` waits for board mail under a background watcher, and `install-host-hooks
48
49
  --host codex|kimi|hermes|opencode|openclaw|pi` installs each host's native context bridge (dry run unless `--apply`).
49
50
  Codex receives structured SessionStart JSON; Hermes, OpenCode, OpenClaw and Pi use native plugin APIs.
50
51
  See the [native host matrix](plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md)
@@ -64,8 +65,8 @@ installed into a service with `scripts/fabric_kit.py install` and checked with `
64
65
  ([kit modules](plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md), including
65
66
  the dates API); connectors for outbound HTTP and OpenRouter (`fabric_http`, `fabric_openrouter`: one deadline, safe
66
67
  retries, the agent's own key, a spend ceiling before every attempt, usage receipts, a fallback model;
67
- [connectors](plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md)); a ready
68
- [support-agent recipe](plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md);
68
+ [connectors](plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md)); the draft-and-check step of a support agent as a
69
+ [recipe](plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md);
69
70
  a complete `scripts/sample_service.py`; and `scripts/check_service.py`, a live probe. A service that answers
70
71
  requests other Projects post to its Project on Fabric's project board uses the board responder
71
72
  (`scripts/fabric_hub.py` and `scripts/fabric_board.py`, Node `fabric-hub.mjs` and `fabric-board.mjs`): the
package/SECURITY.md CHANGED
@@ -33,9 +33,10 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
33
33
  reads by name at run time; the kit itself calls no outside host on its own. A board
34
34
  responder talks only to Fabric's hub on loopback.
35
35
  - The working-with-fabric session hooks run as the person: Stop and SessionEnd run `git`
36
- in the session's folder (counts only, at most 1.5 s; with the mark below and not on Windows, also that folder's own `origin`, only to leave its repository out), read the process table to tell a
37
- session Fabric launched. Stop also, never on Windows and only once the running Fabric's `fabric_session_observe` takes
38
- `repositories` (FAA-140; a worker sees it in the tool's schema and leaves a mark), reads the
36
+ in the session's folder (counts only, at most 1.5 s; with the mark below, also that folder's own `origin`, only to leave its repository out), read the process table to tell a
37
+ session Fabric launched. Stop also, only once the running Fabric's `fabric_session_observe` takes
38
+ `repositories` (FAA-140; a worker sees it in the tool's schema and leaves a mark) and unless the person turned it off
39
+ (`FABRIC_SESSION_NO_REPOSITORIES=1` or a `no-repositories` file in the hooks' folder, FAA-160), reads the
39
40
  last 512 KB of the session's transcript (earlier turns included; only the entries after the
40
41
  last prompt the person typed are used; the tail holds message text and tool output too, file contents and
41
42
  command output that can carry a secret the session displayed, read in memory only and never spooled, logged or sent)
@@ -45,8 +46,8 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
45
46
  host, owner and name, never a local path, a command or message text (on a self-hosted ssh
46
47
  remote, `owner` is the folder path on that server before the repository's name). The mark is
47
48
  removed only by a worker that sees a schema without the field, so after Fabric is downgraded
48
- the turns that end before a worker next runs (about a minute) are still read; none of their
49
- names is sent. They write their spool and log under the user's cache folder (0600),
49
+ the turns that end before a worker next reaches the running Fabric (about a minute; longer while
50
+ rejoins fail) are still read; none of their names is sent. They write their spool and log under the user's cache folder (0600),
50
51
  and the delivery worker starts `fabric-mcp` in its own process group and ends that group
51
52
  (`killpg`, or `taskkill /T /F` on Windows) at its 10 s deadline. They read no Fabric token. An
52
53
  `origin` URL that embeds a credential (`https://user:token@host/...`) is read by `git config`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.54.0",
3
+ "version": "0.56.0",
4
4
  "description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
5
5
  "scripts": {
6
6
  "test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
6
- "version": "0.54.0",
6
+ "version": "0.56.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (never on Windows), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0, and hands any Fabric call to a detached worker with a 10 s deadline.",
2
+ "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (unless FABRIC_SESSION_NO_REPOSITORIES=1), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0 once python3 starts (on Windows it must name Python in Git Bash, FAA-183), and hands any Fabric call to a detached worker with a 10 s deadline.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.54.0"
8
+ version: "0.56.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
11
11
  ---
@@ -47,7 +47,11 @@ ENV_MAX = 64
47
47
  # Whole-value patterns end in \Z, never `$` (R1: `$` also matches before a trailing newline).
48
48
  _ID = re.compile(r"^[a-z][a-z0-9-]{1,62}\Z")
49
49
  _ENV_NAME = re.compile(r"^[A-Z_][A-Z0-9_]*\Z")
50
- _URI_PASSWORD = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?#@]*:[^/?#@]*@")
50
+ # A special scheme (http, https, ws, wss, ftp) reads a user:password@ after any run of slashes, none included, as
51
+ # `new URL` does: https:bot:pw@host carries the password too (0.56.0 seam r7).
52
+ _URI_PASSWORD = re.compile(r"^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt][Pp]):[/\\]*[^/\\?#]*:[^/\\?#]*@"
53
+ # any other scheme: a backslash belongs to the user information (seam r8)
54
+ r"|(?![Hh][Tt][Tt][Pp][Ss]?:|[Ww][Ss][Ss]?:|[Ff][Tt][Pp]:)[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*:[^/?#]*@)")
51
55
  _SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})\Z")
52
56
  _LOCAL_PATH = re.compile(LOCAL_PATH_PATTERN)
53
57
  _URL = re.compile(URL_PATTERN)
@@ -2,12 +2,15 @@
2
2
  """The contract's one credential detector (Fabric Agent Contract DEC-0044, `src/secret-shape.ts` at the pin): a name
3
3
  that can only carry a credential, a value shaped like one, a launch argument that carries one, and a URL that does
4
4
  (LC-21). A port held to the contract's own answers by test/fixtures/contract/profile-vectors.json. Standard library
5
- only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4)."""
5
+ only. The dashboard kit's fabric-button.js carries its own copy of the name and value checks (FL-4).
6
+
7
+ It reads as the contract's ECMAScript does (FAA-136): ECMAScript's whitespace (Python's `\\s` also takes U+0085 and
8
+ U+001C to U+001F, and not U+FEFF), `.` stopping at every line terminator, ASCII case folding, an ASCII word boundary."""
6
9
 
7
10
  from __future__ import annotations
8
11
 
9
12
  import re
10
- from typing import Dict, List
13
+ from typing import Dict, List, Tuple
11
14
  import urllib.parse
12
15
 
13
16
  CREDENTIAL_WORDS = (
@@ -22,20 +25,28 @@ CREDENTIAL_PAIRS: Dict[str, tuple] = {
22
25
  "session": ("token", "id"),
23
26
  "code": ("login", "auth", "access", "session"),
24
27
  }
28
+ #: ECMAScript's \s, as a character class body.
29
+ JS_SPACE = "\t\n\x0b\x0c\r \xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
30
+ _NOT_LINE_END = "[^\n\r\u2028\u2029]" # ECMAScript's `.` without the s flag
25
31
  JWT_SHAPE = re.compile(r"eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*")
26
- SECRET_VALUE = re.compile(r"(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})",
27
- re.IGNORECASE | re.DOTALL)
32
+ SECRET_VALUE = re.compile(r"(^bearer[%s]%s+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}"
33
+ r"|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})" % (JS_SPACE, _NOT_LINE_END), re.IGNORECASE | re.ASCII)
28
34
  _LONG_RUN = re.compile(r"[A-Za-z0-9+/=_-]{40,}")
29
- _BEARER_INSIDE = re.compile(r"\bbearer\s+(?=[A-Za-z0-9._~+/-]*\d)[A-Za-z0-9._~+/-]{12,}=*", re.IGNORECASE)
30
- _PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE)
35
+ _BEARER_INSIDE = re.compile(r"(?<![A-Za-z0-9_])bearer[%s]+(?=[A-Za-z0-9._~+/-]*[0-9])[A-Za-z0-9._~+/-]{12,}=*" % JS_SPACE,
36
+ re.IGNORECASE | re.ASCII)
37
+ _PATH_LIKE = re.compile(r"^(?:[/~.]|[A-Za-z]:[\\/]|[a-z][a-z0-9+.-]*://)", re.IGNORECASE | re.ASCII)
38
+ _WORD_SPLIT = re.compile("[%s._-]+" % JS_SPACE)
39
+ _ARGUMENT_SPLIT = re.compile("[%s=]" % JS_SPACE)
31
40
  _SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
41
+ #: WHATWG's special schemes that carry an authority with user information (file has none).
42
+ _SPECIAL = ("ftp", "http", "https", "ws", "wss")
32
43
 
33
44
 
34
45
  def name_words(name: str) -> List[str]:
35
46
  """A name's words: split at `.`, `_`, `-` and camelCase humps, lowercased (`APIKey` is `api key`)."""
36
47
  spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name)
37
48
  spaced = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", spaced)
38
- return [w.lower() for w in re.split(r"[\s._-]+", spaced) if w]
49
+ return [w.lower() for w in _WORD_SPLIT.split(spaced) if w]
39
50
 
40
51
 
41
52
  def _with_singulars(words: List[str]) -> List[str]:
@@ -73,7 +84,7 @@ def secret_argument(value: str) -> bool:
73
84
  so a folder path holding a session id is not taken for a key."""
74
85
  if JWT_SHAPE.search(value) or SECRET_VALUE.search(value) or _BEARER_INSIDE.search(value):
75
86
  return True
76
- for piece in re.split(r"[\s=]", value):
87
+ for piece in _ARGUMENT_SPLIT.split(value):
77
88
  if _path_like(piece):
78
89
  if any(_long_secret(part) for part in re.split(r"[\\/]", piece)):
79
90
  return True
@@ -93,7 +104,8 @@ def _decode_keeping_plus(raw: str) -> str:
93
104
 
94
105
 
95
106
  def _form_name(text: str) -> str:
96
- pairs = urllib.parse.parse_qsl(text, keep_blank_values=True)
107
+ # URLSearchParams drops one leading `?` from the text it is given (0.56.0 unit r7).
108
+ pairs = urllib.parse.parse_qsl(text[1:] if text.startswith("?") else text, keep_blank_values=True)
97
109
  return pairs[0][0] if pairs else ""
98
110
 
99
111
 
@@ -109,23 +121,66 @@ def raw_values(query: str) -> Dict[str, List[str]]:
109
121
  return values
110
122
 
111
123
 
124
+ #: The C0 controls and space, U+0000 to U+0020: what WHATWG URL parsing trims from a URL's ends.
125
+ _C0_OR_SPACE = "".join(map(chr, range(0x21)))
126
+
127
+
128
+ _LONE_SURROGATE = re.compile("[\ud800-\udfff]")
129
+
130
+
131
+ def _percent_encode(text: str, also: str) -> str:
132
+ """WHATWG's percent-encoding of a query or a fragment: every C0 control, space, `"`, `<`, `>`, every code point
133
+ above U+007E and the characters in `also`, as UTF-8 %XX (a lone surrogate as U+FFFD, as a USVString reads it).
134
+ `new URL` hands the contract's secretUrl the text in this form, so a malformed escape left in it is read the same
135
+ way by both ports (0.56.0 unit r8)."""
136
+ out = []
137
+ for char in _LONE_SURROGATE.sub("\ufffd", text):
138
+ if ord(char) <= 0x20 or ord(char) > 0x7E or char in '"<>' or char in also:
139
+ out.append("".join("%%%02X" % byte for byte in char.encode("utf-8")))
140
+ else:
141
+ out.append(char)
142
+ return "".join(out)
143
+
144
+
145
+ def _url_parts(value: str) -> Tuple[str, str, str]:
146
+ """The password, query and fragment of an absolute URL as WHATWG URL parsing splits them, read by hand: urlsplit
147
+ refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), where `new URL` reads the password (0.56.0 unit
148
+ r7). The fragment starts at the first `#`, the query at the first `?` before it. A special scheme's authority starts
149
+ after every slash or backslash that follows `scheme:` (the special authority ignore slashes state) and ends at the
150
+ next slash, backslash, `?` or `#`, so `https:u:p@h` and `https:\\u:p@h` carry the password p; any other scheme
151
+ has an authority only after `//`, and `file` has no user information. The user information ends at the authority's
152
+ last `@`, and the password follows its first `:`. A host `new URL` would refuse is not checked: such a URL is read
153
+ for its password and parameters all the same, the safe direction for a detector."""
154
+ scheme, _, rest = value.partition(":")
155
+ rest, _, fragment = rest.partition("#")
156
+ rest, _, query = rest.partition("?")
157
+ lowered = scheme.lower()
158
+ if lowered in _SPECIAL:
159
+ authority = rest.replace("\\", "/").lstrip("/").partition("/")[0]
160
+ elif lowered != "file" and rest.startswith("//"):
161
+ authority = rest[2:].partition("/")[0]
162
+ else:
163
+ authority = ""
164
+ userinfo, at, _ = authority.rpartition("@")
165
+ return ((userinfo.partition(":")[2] if at else ""), _percent_encode(query, "'" if lowered in _SPECIAL or lowered == "file" else ""), # file is special too
166
+ _percent_encode(fragment, "`"))
167
+
168
+
112
169
  def secret_url(value: str) -> bool:
113
170
  """Whether a URL an MCP entry or a launch names carries a credential (LC-21): anything secret_argument finds, a
114
171
  password in its user information, or a query or fragment parameter whose name can only carry a credential and has
115
172
  a value, or whose value is shaped like one. A string that is not an absolute URL is checked as an argument only."""
116
173
  if secret_argument(value):
117
174
  return True
118
- if not _SCHEME.match(value.strip()):
175
+ # What WHATWG URL parsing does first: trim C0 controls and space from the ends (never other Unicode spaces, which it
176
+ # refuses), then remove every tab, CR and LF.
177
+ value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
178
+ if not _SCHEME.match(value):
119
179
  return False # not an absolute URL (WHATWG URL parsing refuses it)
120
- try:
121
- parts = urllib.parse.urlsplit(value.strip())
122
- except ValueError:
123
- return False
124
- if not parts.scheme:
125
- return False
126
- if parts.password:
180
+ password, query, fragment = _url_parts(value)
181
+ if password:
127
182
  return True
128
- for part in (parts.query, parts.fragment):
183
+ for part in (query, fragment):
129
184
  for name, raws in raw_values(part).items():
130
185
  if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
131
186
  return True
@@ -14,7 +14,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
14
14
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
15
15
  metadata:
16
16
  author: PassionCode.ai
17
- version: "0.54.0"
17
+ version: "0.56.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
20
  extension-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
@@ -104,8 +104,8 @@ responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) f
104
104
  every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
105
105
  The request body is data behind the service's own input gate. From 0.53.0 a slot the operator moved to another
106
106
  session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate `relayed`, or it
107
- stays moved for good (until Fabric's hub forwards decisions, CT-125, the operator reaches it through the service's page
108
- or its elicitation). Read [the board reference](references/board.md).
107
+ stays moved for good (the operator reaches it through an agent that relays their decision in `_meta`, the service's
108
+ page or its elicitation; Claude Code and Codex wait for CT-125's forwarding). Read [the board reference](references/board.md).
109
109
 
110
110
  ## Step 2: the non-negotiables
111
111
 
@@ -165,8 +165,9 @@ handler.
165
165
 
166
166
  Calling out: [the connectors reference](references/connectors.md) (`fabric_http` with one
167
167
  deadline, safe retries and a size cap; `fabric_openrouter` with the agent's own key, a
168
- spend ceiling before every attempt, receipts and a fallback). A ready support agent on the
169
- kit: [`recipes/support-agent/`](recipes/support-agent/README.md).
168
+ spend ceiling before every attempt, receipts and a fallback). The draft-and-check step of a
169
+ support agent on the kit: [`recipes/support-agent/`](recipes/support-agent/README.md); what the whole agent carries is
170
+ creating-fabric-agents' `references/agents-that-answer-people.md`.
170
171
 
171
172
  Startup order is the part that goes wrong; keep it exactly:
172
173
 
@@ -259,8 +260,9 @@ or `human` with its `humanStep`) and declared with `build_well_known(actions=ser
259
260
  `extensions/actions/0.1`. A `relayed` action called by the operator's own agent with their decision in
260
261
  `_meta.decision` is the operator's act: never answer it "operator session required" or send them to the page.
261
262
  A service adopts the relayed gate once Fabric's hub forwards decisions (CT-125), as the contract says; until then its tools
262
- answer as today, except the board responder's take-back action, declared `relayed` now and reached through the
263
- page or elicitation until CT-125 ([board.md](references/board.md)). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
263
+ answer as today, except the board responder's take-back action, declared `relayed` now: an agent whose client sets
264
+ `_meta` calls it with the operator's decision; Claude Code and Codex reach it through the page until CT-125
265
+ ([board.md](references/board.md); the reference service's `board.take-back`). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
264
266
  refusals.
265
267
 
266
268
  **Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other