@passioncode-ai/fabric-agent-adapter 0.55.0 → 0.57.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 (59) hide show
  1. package/CHANGELOG.md +242 -0
  2. package/README.md +5 -4
  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 +3 -3
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +18 -7
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +30 -6
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -6
  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 +12 -1
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +19 -6
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
  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 +9 -4
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +2 -1
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +17 -4
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +2 -1
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +19 -3
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +226 -26
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
  49. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  50. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +11 -5
  51. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +18 -18
  52. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
  53. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
  54. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
  55. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
  56. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
  57. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
  58. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
  59. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,247 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.57.0 (2026-10-28)
4
+
5
+ The contract pin moves to `3830a33` (`v0.1.0+2026-10-27.6`) from `00e050ba` (FAA-176).
6
+
7
+ ### Added
8
+
9
+ - **`comparison` as a human step** (DEC-0142, contract CT-186). `humanStep` in an action declaration may be
10
+ `comparison`: the person compares two codes shown on two screens, or chooses to share without a comparison. Both
11
+ kits' action-declaration checks take it (`fabric_actions.py`, `fabric-actions.mjs`, `actions-vectors.json`
12
+ regenerated: 68 cases, 12 declarations), the probe passes a declaration that uses it, and interop.md, delegation.md
13
+ and working-with-fabric's human gates (OA-5), which its refusal table cites, name it. A reader pinned before (a 0.56.0
14
+ kit, Fabric Dashboards before 0.8.9) refuses such a declaration, which Project Observatory declares on its share
15
+ buttons from OBS-264.
16
+ - **The protocol register** gains the six peer-share revisions (DEC-0142, `fabric-peer-share/1` and its parts) and
17
+ `org-adaptation/1` and `org-adaptation-receipt/1` (DEC-0138) in both twins (`feed-protocols-vectors.json`), so a
18
+ service's well-known that names one is read as a known revision.
19
+ - **The provider entry's manifest path** (DEC-0145, CT-192): `fabric_provider.py` names the schema's
20
+ `MANIFEST_PATTERN`, read as a search that ends at the string's end; the reader accepts the manifest's name after a
21
+ backslash, the writer still writes it with `/` and refuses the other with a sentence naming CT-192, and the CLI's
22
+ stem is read after either separator (CT-193). The contract's ten `provider.schema.json` fixtures are vendored
23
+ (`provider-fixtures.json`) and tested in every run.
24
+
25
+ ### Changed
26
+
27
+ - On Windows, `fabric_provider.py`'s `services_dir()` (its FAC-SEM-013 check that an id is not a service's) and
28
+ `providers_dir()` read `%LOCALAPPDATA%\passioncode-fabric` (provider.md), as the service kit's readers already did.
29
+ - agents-that-answer-people.md (A5) names the contract's `support-quality-daily.schema.json`, says when a producer may
30
+ carry `calibration` (CT-188) and that a reader validates against its own pinned copy (DEC-0144).
31
+ - The hosted contract job runs `test_provider`, and `test/validate.py` fails a workflow whose contract job leaves out a
32
+ module that holds a test gated on the contract.
33
+
34
+ ### Upgrading
35
+
36
+ - A bundle pinned to `00e050ba` or an earlier supported commit keeps working; `adapt_project.py`
37
+ accepts it (`SUPPORTED_CONTRACT_COMMITS`). A new scaffold writes `3830a33`.
38
+ - DEC-0139 (the machines' report) and DEC-0138's organization server route reach no kit code: the adapter registers
39
+ their protocol revisions and implements neither side. DEC-0143 (`secret.prompt`, the local channel) and DEC-0142's
40
+ peer-share profile beyond its register entries reach no kit code either; Project Observatory implements them
41
+ (CT-186).
42
+ - On Windows, a provider entry written by `fabric_provider.py` now goes to the folder Fabric reads
43
+ (`%LOCALAPPDATA%\passioncode-fabric\providers`). Fabric v0.3.15 and earlier read a manifest path only under `~/` or
44
+ `/`; a drive letter (`C:/.../fabric-agent.json`) is read by the Fabric release after v0.3.15 (CO-479). Until then
45
+ write the manifest path under `~/`. An entry an older kit wrote on Windows sits in
46
+ `%USERPROFILE%\.local\share\passioncode-fabric\providers`, which Fabric never read and `remove` no longer finds:
47
+ write it again and delete the old file.
48
+
49
+ ## 0.56.0 (2026-10-28)
50
+
51
+ Stability and Windows: the debts of 0.53.0 to 0.55.0 that touch how the hooks, the board responder and the interop
52
+ gate behave.
53
+
54
+ ### Fixed
55
+
56
+ - The board responder's notices reach a service's event feed: they were sent at level `warn`, which
57
+ `JsonlEventLog.append` refuses (its levels are `info`, `notice`, `warning`, `error`), so `board.down`,
58
+ `board.access.needed`, `board.slot.moved`, `board.slot.contested`, `board.request.held`, `board.request.skipped`,
59
+ `board.reconcile.needed`, `board.attempt.lost` and `board.outcome.invalid` were dropped wherever `emit=log.append`
60
+ (the reference service included, since 0.28.0; `board.slot.moved` since 0.53.0). They are sent at `warning` now, in
61
+ both twins, as is the new `board.slot.refused` (FAA-163). Each notice names its `subject` (`{type, id}`: its request,
62
+ else its capability, else its episode, such as a block's cause, else the Project), so a host tells one request's,
63
+ capability's or cause's episode from another's. In Python the subject goes to an `emit` whose signature names a
64
+ `subject` parameter with no value bound (`JsonlEventLog.append` now names one); any other `emit` (only `**kwargs`, a
65
+ subject bound with `functools.partial`, a signature that cannot be read) is called as 0.55.0 called it, once, without
66
+ one. The Node responder always passes it in its options object, which an `emit` that does not read it ignores.
67
+ - **working-with-fabric on Windows** (FAA-175, FAA-177). A turn names the repositories it touched on Windows too:
68
+ Git Bash `/c/...` and `/cygdrive/c/...`, `C:/...` and quoted `C:\...` paths, and `git.exe -C` (any case); Bash
69
+ text is split with Git Bash's own quoting, so an unquoted backslash is an escape there too. A `cd` inside `( )`, a
70
+ pipeline's stage or a list sent to the background no longer moves the folder a later relative `-C` is read
71
+ against; a newline ends a command, a here-document's body is text (a `#` comment no longer hides its lines, and
72
+ `<<<` opens none), `&>` only redirects, `git -C /a -C b` runs in `/a/b`, and `pushd` and a `cd` after `{` or `then`
73
+ are read. Cygwin and MSYS Python read paths as POSIX. Before, the Stop hook did not read the
74
+ transcript on Windows at all, and a turn from Windows came without `repositories`. The hooks run `python3`, which
75
+ must name Python 3.9 or later in Git Bash there (session-hooks.md; FAA-183 makes the plugin find it itself).
76
+ - **A relative `git -C` after `cd`** (FAA-166) is read against the folder the command last changed into with an
77
+ absolute `cd`; after a relative or unknown `cd` it is dropped, never read against the session's folder.
78
+ - **The board responder's recovery** (FAA-163, FAA-158). A refused enroll of one capability no longer stops the
79
+ others: it is retried on its own, the pause doubling, and said once as `board.slot.refused`. A slot's move is written
80
+ under the responder's lock and only while its record is unchanged, so an operator's take-back at the same moment
81
+ wins. The slot code moved to its own twin modules, `fabric_board_slots.py` and `fabric-board-slots.mjs`.
82
+ - **An idle stop leaves the board within 3 s** (FAA-173): a responder with no work in hand closes its session with a
83
+ confirmed `DELETE` inside the hard exit (`close(timeout)`: a timeout not above 0 is the session's own, and a session built
84
+ with one takes 15 s, in both twins),
85
+ instead of letting the slot lapse (a board call already in flight when the
86
+ stop comes runs first, and when the join ends before the DELETE the slot lapses as before). The slot records are forgotten only after
87
+ a 2xx: a 404 (the session already gone) keeps them, since a move made before it ended is read at the next
88
+ `com.status`. A refusal of the whole board session (`not_available` included) is the board down, never a refused
89
+ capability; a capability refused, or contested, again after it was held is said again, once per capability. A clean
90
+ leave forgets only the records this session confirmed (a slot it held, paused under the operator's hold included, or
91
+ one `com.status` showed unmoved) and no fenced call has put in doubt since, so a slot moved while the service was
92
+ down stays `moved` when the session is dropped at connect or recovery stops early, and is never enrolled again on
93
+ its own. A paused or retrying slot reads `com.status` before each new ask, so a move during the operator's hold is
94
+ said. `Checkpoint.leave(project, held)` takes `held` as a required argument in both twins.
95
+ - **`action_gate` fails closed** (FAA-139): a spend that is not a finite number is read as no estimate, which a
96
+ mandate's `perCallMaxUsd` ceiling refuses (a mandate with no ceiling covers it; the question is with the contract,
97
+ FAA-185), and a project that cannot be read is never covered by a mandate limited to `projects`. A `null` mandate in
98
+ `_meta` reads as an unreadable one, not as none (FAA-178; Python's `approval_step`, while a Node service passes `{}`
99
+ to `jobStep` itself, interop.md). A job's start (`job_step`) reads spend and project as its answer does, and keeps
100
+ the spend as it read it (a NaN is no estimate, never a bare `NaN` in the job file).
101
+ - **`fabric_secrets` matches the contract's ECMAScript reading** (FAA-136): `\s`, `.` and case folding as JavaScript
102
+ reads them, so both ports flag the same values. `secret_url` also reads a special scheme as `new URL` does: every
103
+ slash or backslash after `https:` (or `http`, `ftp`, `ws`, `wss`) is skipped and a backslash before the query reads
104
+ 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
105
+ by hand rather than with `urlsplit`, which refuses a `[` or `]` outside an IPv6 host (`https://u:p[w@h/`), and drops
106
+ one leading `?` from a parameter's name as `URLSearchParams` does. Where `new URL` refuses a host (none, a port over
107
+ 65535), Python still reads the password and the parameters, so it can flag more than Node, never less. The
108
+ `source.repository` check of a descriptor and of a provider entry reads a special scheme's password the same way
109
+ (`https:bot:pw@host`), up to the user information's last `@` (`https://me@corp.example:pw@host`), and says so
110
+ before it judges the host. The query and the fragment are read percent-encoded, as `new URL` hands them over.
111
+ `fabric_http.redact_url` reads a special scheme the same way (below).
112
+ - **What the kit sends, refuses and redacts as a URL** (FAA-136; the remaining differences are FAA-187):
113
+ - `redact_url`: the URL an `HttpError` carries for `https:u:pw@host` has no password, as in Node; a URL of any
114
+ other scheme (a misspelled `htps:` included) keeps only its scheme in both twins (`htps:<redacted>`), since its
115
+ user information may sit anywhere in the rest. It first trims C0 controls and space from the ends and removes
116
+ tabs and line breaks, as `new URL` does, so ` https://u:pw@host` keeps no password on Python 3.9 either. A
117
+ string with no scheme (`ht tps:`, `1ttps:`, a leading byte order mark or no-break space) and a URL whose host or
118
+ port `new URL` refuses (a password holding `/`, a port that is not ASCII digits such as `1_2` or `+1`, which
119
+ Python 3.9's `int()` takes, a bracketed host that is no IPv6 address, a `ws`, `wss` or `ftp` URL with no host or
120
+ a port above 65535) read as `<unparseable url>`; the scheme is read by the WHATWG rule, not by `urlsplit`, whose
121
+ rule differs between 3.9 patch releases. On Python a host holding `%` reads as `<unparseable url>` too, where
122
+ Node writes it decoded. The path is not redacted in either twin (Node writes it as `new URL` normalises it,
123
+ Python as given), so a key never belongs in it.
124
+ - `check_url` and `request`: the same ports and bracketed hosts (an IPv6 zone included) are refused. On Python a
125
+ backslash, `%`, space or control character in the host (a Unicode space and a C1 control included) is refused
126
+ too, since urllib would decode it or dial another host (`h.example%3A8443` reached port 8443), and so is a space
127
+ or ASCII control character before the URL's first `#`, or a character outside ASCII in its path or query, which
128
+ urllib would send and http.client refuse: such a URL is refused before it is sent (kind `refused`), where 0.55.0
129
+ retried it and answered kind `connection`. Python's `check_url` checks the host's shape, not its name (FAA-187
130
+ (14), (16)). The Node twin reads several of these as `new URL` does, among them a tab or line break removed, a
131
+ space or a character outside ASCII percent-encoded, a backslash as `/`, an extra slash before the host skipped
132
+ and a `%` host decoded (refused when the decoded host is not allowed, as `h.example%3A8443`).
133
+ - What is sent: the default transport sends a URL up to its first `#` in both twins (urllib alone kept everything
134
+ before the last one, so `/x#a#b` asked for `/x#a`); a transport you pass receives the URL as given. ASCII
135
+ whitespace at the ends, which urllib and `new URL` both trim, is trimmed, so a URL ending in a line break is still
136
+ 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
137
+ whose part before its first `#` ends in a Unicode space (a no-break or ideographic space) is now refused; 0.55.0
138
+ sent it trimmed when the space ended the URL and failed every call when a fragment followed it. Node keeps it
139
+ percent-encoded.
140
+ - The OpenRouter clients trim a base URL's ends first: ASCII whitespace and trailing slashes in both, and on Node
141
+ every C0 control there and a tab or line break anywhere, as `new URL` reads them. So `.../v1\n`, `.../v1/ ` and,
142
+ on Node, `.../v1\n/` are sent to `.../v1/chat/completions`. Both then refuse, when they are built, a base URL
143
+ holding a query or a fragment (kind `config`, read before `check_url`; `/chat/completions` would land in it, so a path-routed endpoint
144
+ such as OpenRouter failed every call in 0.55.0). Python also refuses, through `check_url` (kind `refused`), a
145
+ base with a space, a control or a non-ASCII character in its path, which 0.55.0 accepted and then failed every
146
+ 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
147
+ digits (`:٤٤٣`), as `new URL` reads it.
148
+ - A JWKS source: the Python kit reads its host and port as urllib dials them, https included. It refuses a space
149
+ or ASCII control character before the URL's first `#` (it reads the URL up to there, as Node does), a character
150
+ outside ASCII in its path or query, a host holding `%`, a backslash, a Unicode space or a control character, a
151
+ bracketed host that is no IPv6 address, a port that is not ASCII digits or is above 65535, no host, and any `@`
152
+ (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
153
+ before the first `#` is refused for the same reason as in `check_url`, though urllib would trim it and read the
154
+ 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
155
+ `http://[::1]%2Eevil.example/` (which Python 3.9's `urlsplit` reads as `::1`) nor a mapped `[::ffff:127.0.0.1]`
156
+ (loopback to Python 3.13+) counts as loopback. The Node kit accepts several of these as `new URL` reads them,
157
+ among them a `%` host decoded, a backslash as `/`, a tab or line break removed, an empty userinfo dropped and an
158
+ extra slash before the host skipped (`https:///j` reads host `j`).
159
+ - **A too-deep protocol value** is refused by name (FAA-122), in both twins.
160
+ - **A job's approval step and its answer read the mandate under the same name** (FAA-179): the job's stored
161
+ capability, also when a handler created the job under another name; `on_fault` hears both under it, not the answer
162
+ under `fabric.job.get`.
163
+ - **Nudges** (FAA-143): `lost=` counts every reminder that did not make it back to the file (this turn's capped lines,
164
+ and the pending ones when the write-back fails too); an answer's nudges are read up to 1,000 items, in linear time.
165
+
166
+ ### Added
167
+
168
+ - `FABRIC_SESSION_NO_REPOSITORIES=1`, or a `no-repositories` file in the hooks' folder (which a cache cleaner may
169
+ remove), turns the transcript read off (FAA-160); `fabric_session.py status` says
170
+ whether it is on (only while a running Fabric advertises `session-hooks/1`, `fabric` is registered for the folder and
171
+ 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
172
+ than the transcript's last 512 KB names only what that tail shows; until FAA-184 they can also name a wrong one,
173
+ since a relative `git -C` in a later Bash call is read against the session's folder.
174
+ - The reference service's dashboard shows a moved slot with a Take back button (`POST /api/board/take-back`, operator
175
+ only; FAA-157), and the button has its tool: `board.take-back`, a `relayed` action an agent whose client sets `_meta`
176
+ calls with the operator's decision (Claude Code and Codex reach the page until Fabric forwards decisions, CT-125).
177
+ The tool is a manifest capability too (effect `change-policy`, idempotency `required`), as every tool the well-known
178
+ lists must be: `register --board-project <uuid>` writes it into the manifest, so `check_service.py` passes the sample
179
+ with its board on. The manifest stays register's to write: a serve whose board mode differs from it at its start
180
+ 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
181
+ page, the route and the tool read a move from the checkpoint's record too, so a take-back made after a restart and
182
+ before the session reconnects (or while Fabric is down) is kept; a checkpoint it cannot write is a tool error. A
183
+ repeat after a lost answer is answered `{capability, moved: false}` by the route as by the tool, and each take-back is
184
+ a `board.slot.returned` event (the capability as its subject) whose sentence says the route in words and whose `data`
185
+ 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
186
+ page's CSP allows the button's own `fetch` (`connect-src 'self'`), and a failed request says so on the button.
187
+ - working-with-fabric Step 4: a `fabric_task_create` that ended in an error may have filed the piece, so look for it
188
+ by title in `fabric_work_list` before filing it again (FAA-168), with the eval `a-failed-create-may-have-filed`.
189
+
190
+ ### Changed
191
+
192
+ - working-with-fabric names Fabric v0.3.5 as the release that brought `fabric-mcp` and the `fabric_work_*` tools, not
193
+ 0.4.0 (the plan milestone), and goes by whether the tools are listed (FAA-170).
194
+ - The `board.slot.moved` notice says the accepted work stays `accepted` (not "held", the operator's word) and that
195
+ after its take-back action a restart or reconnect while accepted work waits takes the slot back at once; a restart
196
+ alone does not (FAA-164, FAA-171);
197
+ project-board.md says the same to a requester. board.md says a take-back without the operator's relayed decision is
198
+ refused. session-hooks.md gives the Bash cap as 64 K characters and the downgrade window as until a worker next
199
+ reaches the running Fabric.
200
+
201
+ ### Upgrading
202
+
203
+ - On Windows the Stop hook now reads the transcript's tail and the touched repositories' `origin` (FAA-175), once
204
+ Fabric takes `repositories`, as it does on macOS and Linux; `FABRIC_SESSION_NO_REPOSITORIES=1` (in Claude Code's
205
+ settings `env`, which reached hooks declared in settings and in a plugin's `hooks.json` in runs on macOS; Windows is
206
+ FAA-183's run) turns it off.
207
+ - A Python `JwksCache` source is now refused when the cache is built, so a service that builds it at start stops
208
+ there, if it is any source the JWKS source item under Fixed lists. Among them, 0.55.0 read: a host holding `%` (an
209
+ IPv6 zone included, as in `http://[::1%25lo0]/`), which urllib decoded and dialled; a port that is not ASCII digits
210
+ (`:1_8080`); a bracketed IPv4 host (`[127.0.0.1]`, which Python 3.9 dialled as `127.0.0.1`); a mapped
211
+ `[::ffff:127.0.0.1]` loopback (on Python 3.13+); a Unicode space ending the URL. Write the host and port plainly
212
+ (`127.0.0.1:18080`) and trim the space. The other sources it lists 0.55.0 accepted and then answered every
213
+ delegated call `issuer-unavailable`.
214
+ - On Python, `check_url` and `request` now refuse before sending (kind `refused`) a URL that 0.55.0 sent: a host
215
+ holding `%` (`h.example%3A8443` reached port 8443), a port that is not ASCII digits (`:1_2` reached port 12 on
216
+ 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
217
+ control or a non-ASCII character in its path or query, which 0.55.0 retried and answered kind `connection`, is
218
+ refused the same way. Both OpenRouter clients refuse, when they are built, a base URL holding a query or a
219
+ fragment, and the Python client one with a space, a control or a non-ASCII character in its path, so a service
220
+ 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
221
+ configured, and percent-encode a space, a control or a non-ASCII character in each path segment or query
222
+ value (`urllib.parse.quote(value, safe="")`, `urllib.parse.urlencode` for a query), never the whole URL.
223
+ - A service on the kit joins its responder thread at stop, bounded inside its hard exit, as `sample_service.py` does
224
+ (board.md, "Wiring it into the service"); on Node it aborts the run's signal and awaits the run. The idle leave above
225
+ happens only then. A service that stops without the join still works; its
226
+ slot lapses on the board's clock as before.
227
+ - A service that calls `Checkpoint.leave` itself now passes the generations it confirmed (`held`, required in both
228
+ twins); the responder's own leave does this.
229
+ - A service's own `emit` that matched the board responder's level `warn` now sees `warning`, with a `subject` when its
230
+ signature names one. A service with `emit=log.append` now logs the board's notices, and those with `notify` (access
231
+ needed, a contested or moved slot, a held request, a reconcile needed) reach the host's banners, without a link to
232
+ the page that resolves them (FAA-187 (21)). A Fabric outage's `board.down` (no notify) is now a row in the feed too,
233
+ said again after each recovery (FAA-187 (22)).
234
+ - A reference service served with `--board-project` registers again with the same `--board-project`
235
+ (`sample_service.py register ... --board-project <uuid>`): a registration from 0.55.0 lacks `board.take-back`, so
236
+ `check_service.py` fails `interop.well-known-capabilities` and the log says `service.manifest.mismatch`. Register
237
+ before you serve: the service checks its manifest only when it starts, so a warning logged before the registration
238
+ stays in the log until the next start.
239
+ - A service that calls `action_gate` (`actionGate`) itself passes a finite `spend_usd` and a string `project`, or
240
+ expects the operator's decision under a mandate with a `perCallMaxUsd` ceiling or a `projects` limit; an `on_fault`
241
+ that matched a job's answer under `fabric.job.get` now matches the job's capability; a descriptor's or provider
242
+ entry's `source.repository` names the repository without a password in its user information (`https:bot:pw@host`
243
+ is refused).
244
+
3
245
  ## 0.55.0 (2026-10-27)
4
246
 
5
247
  ### Added
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)
@@ -284,7 +285,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
284
285
  The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
285
286
 
286
287
  - version: `0.1.0`
287
- - commit: `00e050ba7429c0c60261cec871779e0fd6aeeb28`
288
+ - commit: `3830a3353923e20f826d466610e1c479f120bec0`
288
289
 
289
290
  Every other mention of the default contract revision (this section, the skills' metadata, the
290
291
  skill card, `adapt_project.py`) must equal it; `python3 test/validate.py` fails a tree
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.55.0",
3
+ "version": "0.57.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.55.0",
6
+ "version": "0.57.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,9 +5,9 @@ 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.55.0"
8
+ version: "0.57.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
10
+ contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -76,7 +76,7 @@ Use exactly:
76
76
 
77
77
  - contract version `0.1.0`;
78
78
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
79
- - commit `00e050ba7429c0c60261cec871779e0fd6aeeb28`.
79
+ - commit `3830a3353923e20f826d466610e1c479f120bec0`.
80
80
 
81
81
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
82
82
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -11,6 +11,7 @@ directory:
11
11
  |---|---|
12
12
  | macOS | `~/Library/Application Support/ai.passioncode.fabric/providers/` |
13
13
  | Linux | `${XDG_DATA_HOME:-~/.local/share}/passioncode-fabric/providers/` |
14
+ | Windows | `%LOCALAPPDATA%\passioncode-fabric\providers\` (the same root as the services folder, DEC-0032; `~\AppData\Local` when `LOCALAPPDATA` is unset) |
14
15
  | any | `FABRIC_PROVIDERS_DIR` when set |
15
16
 
16
17
  A service uses a descriptor instead (the `building-fabric-services` skill). One id is a
@@ -49,15 +50,25 @@ It refuses, with one sentence that never quotes a value:
49
50
  credential itself: a token prefix, a private key, a JWT (FAC-SEM-015: a value is
50
51
  checked by its form; name patterns apply to names);
51
52
  - what the pinned `provider.schema.json` refuses: a URL other than `http://127.0.0.1:<port>/mcp` with a port from 100
52
- to 65535, a manifest path that is not an absolute or `~/` path to `fabric-agent.json` (or is a network share, or over
53
- 1024 characters), a shell-string command, more than 32 arguments or one over 1024 characters, more than 64 env
54
- names, a `source.repository` that is not an absolute URI, an `installedAt` that is not an RFC 3339 date-time,
55
- unknown fields. The patterns are the schema's, copied (`test_provider.SchemaParityTests` compares them with a
56
- contract checkout at the pin when `FABRIC_CONTRACT_NEW` names one);
53
+ to 65535, a manifest path that is not an absolute, `~/` or `~\` path to a file named `fabric-agent.json` after
54
+ either separator (DEC-0145; or is a network share, or over 1024 characters), a shell-string command, more than 32
55
+ arguments or one over 1024 characters, more than 64 env names, a `source.repository` that is not an absolute URI, an
56
+ `installedAt` that is not an RFC 3339 date-time, unknown fields. The patterns are the schema's, copied
57
+ (`test_provider.SchemaParityTests` compares them with a contract checkout at the pin when `FABRIC_CONTRACT_NEW` names
58
+ one, as the contract job of the nightly CI does), and the reader is held to every `provider.schema.json` fixture of
59
+ the pin (`test/fixtures/contract/provider-fixtures.json`, `test_provider.ContractFixtureTests`);
57
60
  - a `source.repository` with a `user:password@` or a value shaped like a credential: it names the repository, never
58
- reaches it (since 0.49.3, as the service kit's descriptor check).
61
+ reaches it (since 0.49.3, as the service kit's descriptor check);
62
+ - a manifest whose name follows a backslash (`C:\Agents\example\fabric-agent.json`). The reader,
63
+ `validate_provider_entry`, accepts it since DEC-0145 (`MANIFEST_PATTERN`, the schema's), but a reader that has not
64
+ re-pinned to DEC-0145 refuses the entry, Fabric's registry among them (CT-192), so the writer does not emit it until
65
+ the oldest Fabric release in use reads it (the release after v0.3.15, CO-479). Write the path with `/` before the name. A manifest under the home folder
66
+ (`~/Agents/example/fabric-agent.json`) is read by every Fabric release; one with a drive letter
67
+ (`C:/Agents/example/fabric-agent.json`) only by Fabric after v0.3.15, whose registry took the repin (CO-479): v0.3.15
68
+ and earlier read only a manifest under `~/` or `/`, so not `~\` either.
59
69
 
60
- `validate` also checks that the file is named `<id>.json` (FAC-SEM-014, slug half).
70
+ `validate` also checks that the file is named `<id>.json` (FAC-SEM-014, slug half): the stem is what follows the last
71
+ `/` or `\`, on every platform (CT-193).
61
72
  From Python: `write_provider_entry(entry)`, `remove_provider_entry(id)`,
62
73
  `validate_provider_entry(entry)`, `manifest_problems(entry)`, `providers_dir()`.
63
74
 
@@ -22,10 +22,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
22
22
 
23
23
  CONTRACT_VERSION = "0.1.0"
24
24
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
25
- CONTRACT_COMMIT = "00e050ba7429c0c60261cec871779e0fd6aeeb28"
25
+ CONTRACT_COMMIT = "3830a3353923e20f826d466610e1c479f120bec0"
26
26
  # #region supported-contract-revisions — docs: README.md#contract-pin
27
27
  # Only this declaration may name a legacy revision in live files.
28
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "1197a90dc4b57152cabcc71ad17db7559f35a27f", "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "00e050ba7429c0c60261cec871779e0fd6aeeb28", "1197a90dc4b57152cabcc71ad17db7559f35a27f", "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
29
29
  # #endregion supported-contract-revisions
30
30
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
31
31
  MCP_REVISION = "2026-07-28"
@@ -41,16 +41,26 @@ PROTOCOL = "fabric-provider/0.1"
41
41
  URL_PATTERN = r"^http://127\.0\.0\.1:([1-9][0-9]{2,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])/mcp$"
42
42
  LOCAL_PATH_PATTERN = "^(~[\\\\/]|/(?![\\\\/])|[A-Za-z]:[\\\\/])[^\\u0000]*$"
43
43
  LOCAL_PATH_MAX = 1024
44
+ # DEC-0145 (CT-192): the manifest is named fabric-agent.json after either separator, so a Windows path names it too. The
45
+ # schema's pattern is a search, not a whole-value match; it is applied with its `$` read as \Z (R1, below).
46
+ MANIFEST_PATTERN = r"(^|[\\/])fabric-agent\.json$"
44
47
  ARGV_MAX_ITEMS = 32
45
48
  ARGV_MAX_ITEM = 1024
46
49
  ENV_MAX = 64
47
50
  # Whole-value patterns end in \Z, never `$` (R1: `$` also matches before a trailing newline).
48
51
  _ID = re.compile(r"^[a-z][a-z0-9-]{1,62}\Z")
49
52
  _ENV_NAME = re.compile(r"^[A-Z_][A-Z0-9_]*\Z")
50
- _URI_PASSWORD = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?#@]*:[^/?#@]*@")
53
+ # A special scheme (http, https, ws, wss, ftp) reads a user:password@ after any run of slashes, none included, as
54
+ # `new URL` does: https:bot:pw@host carries the password too (0.56.0 seam r7).
55
+ _URI_PASSWORD = re.compile(r"^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt][Pp]):[/\\]*[^/\\?#]*:[^/\\?#]*@"
56
+ # any other scheme: a backslash belongs to the user information (seam r8)
57
+ r"|(?![Hh][Tt][Tt][Pp][Ss]?:|[Ww][Ss][Ss]?:|[Ff][Tt][Pp]:)[A-Za-z][A-Za-z0-9+.-]*://[^/?#]*:[^/?#]*@)")
51
58
  _SECRET_REF = re.compile(r"^secret-ref:([A-Za-z0-9][A-Za-z0-9._/:@-]{0,255})\Z")
52
59
  _LOCAL_PATH = re.compile(LOCAL_PATH_PATTERN)
53
60
  _URL = re.compile(URL_PATTERN)
61
+ _MANIFEST = re.compile(MANIFEST_PATTERN[:-1] + r"\Z")
62
+ # The manifest's name after a backslash: valid since DEC-0145, but no writer emits it until every reader accepts it.
63
+ _MANIFEST_AFTER_BACKSLASH = re.compile(r"\\fabric-agent\.json\Z")
54
64
  # format: uri (RFC 3986 absolute URI): a scheme, a colon, then no whitespace or control character.
55
65
  _URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:[^\s\x00-\x1f\x7f]+\Z")
56
66
  # format: date-time (RFC 3339).
@@ -85,6 +95,10 @@ def services_dir() -> Path:
85
95
  home = Path.home()
86
96
  if sys.platform == "darwin":
87
97
  return home / "Library/Application Support/ai.passioncode.fabric/services"
98
+ if sys.platform == "win32":
99
+ # provider.md "Where the directory is" (DEC-0032): LOCALAPPDATA, never the roaming APPDATA.
100
+ local = os.environ.get("LOCALAPPDATA")
101
+ return (Path(local) if local else home / "AppData" / "Local") / "passioncode-fabric" / "services"
88
102
  return Path(os.environ.get("XDG_DATA_HOME") or str(home / ".local/share")) / "passioncode-fabric/services"
89
103
 
90
104
 
@@ -124,8 +138,9 @@ def validate_provider_entry(entry: Any) -> List[str]:
124
138
  problems.append("summary is at most 200 characters")
125
139
  manifest = entry["manifest"]
126
140
  if (not isinstance(manifest, str) or len(manifest) > LOCAL_PATH_MAX or not _LOCAL_PATH.fullmatch(manifest)
127
- or not re.search(r"(^|/)fabric-agent\.json\Z", manifest)):
128
- problems.append("manifest is an absolute or ~/ path to fabric-agent.json, at most %d characters, never a network share" % LOCAL_PATH_MAX)
141
+ or not _MANIFEST.search(manifest)):
142
+ problems.append("manifest is an absolute, ~/ or ~\\ path to fabric-agent.json (the name after either separator), at most %d characters, "
143
+ "never a network share" % LOCAL_PATH_MAX)
129
144
  if "source" in entry:
130
145
  source = entry["source"]
131
146
  if not isinstance(source, dict) or not set(source) <= {"repository"}:
@@ -233,8 +248,15 @@ def _service_ids(directory: Path) -> Dict[str, str]:
233
248
  def write_provider_entry(entry: Dict[str, Any], directory: Optional[Path] = None, services: Optional[Path] = None) -> Path:
234
249
  """Installer-only. Refuses an invalid entry and an id that is already a service (FAC-SEM-013)."""
235
250
  problems = validate_provider_entry(entry)
236
- if not problems:
237
- problems = manifest_problems(entry)
251
+ if problems:
252
+ raise ProviderError("Provider entry is invalid: %s." % "; ".join(problems))
253
+ if _MANIFEST_AFTER_BACKSLASH.search(entry["manifest"]):
254
+ # provider.md (DEC-0145): a reader before DEC-0145, Fabric's registry among them, refuses this entry until it
255
+ # re-pins (CT-192), so the writer does not emit it yet.
256
+ raise ProviderError("The manifest's name follows a backslash, which a reader that has not re-pinned to DEC-0145 refuses (CT-192). "
257
+ "Write the path with / before the name: ~/.../fabric-agent.json, which every Fabric release reads, or "
258
+ "C:/.../fabric-agent.json, which Fabric reads only after v0.3.15 (its repin, CO-479).")
259
+ problems = manifest_problems(entry)
238
260
  if problems:
239
261
  raise ProviderError("Provider entry is invalid: %s." % "; ".join(problems))
240
262
  clash = _service_ids(services or services_dir()).get(entry["id"])
@@ -297,7 +319,9 @@ def main(argv: Optional[List[str]] = None) -> int:
297
319
  if args.command == "validate":
298
320
  entry = json.loads(args.path.read_text(encoding="utf-8"))
299
321
  problems = validate_provider_entry(entry)
300
- if isinstance(entry, dict) and entry.get("id") != args.path.stem:
322
+ # CT-193 (FAC-SEM-014): the stem is what follows the last / or \ and loses .json, on every platform.
323
+ stem = re.sub(r"\.json\Z", "", re.sub(r"^.*[\\/]", "", str(args.path), flags=re.S))
324
+ if isinstance(entry, dict) and entry.get("id") != stem:
301
325
  problems.append("the file name %s is not <id>.json for %s (FAC-SEM-014)" % (args.path.name, entry.get("id")))
302
326
  if not problems:
303
327
  problems = manifest_problems(entry)