@passioncode-ai/fabric-agent-adapter 0.60.0 → 0.62.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 (56) hide show
  1. package/CHANGELOG.md +246 -0
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  5. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +4 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +1 -1
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +57 -0
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +11 -9
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +8 -1
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +19 -2
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +58 -7
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +4 -2
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +2 -2
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +2 -0
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +23 -6
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -1
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +4 -2
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +6 -4
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -2
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +16 -3
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +18 -2
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +33 -3
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +10 -1
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +117 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +24 -5
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +3 -1
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +33 -3
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +14 -2
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +10 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +14 -8
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +24 -4
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +3 -0
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +57 -0
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +16 -1
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +146 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +20 -5
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +55 -4
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +5 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +33 -25
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +19 -6
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +23 -3
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +17 -3
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +86 -7
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +57 -0
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +4 -0
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +217 -29
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -0
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +42 -4
  53. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  54. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +23 -23
  55. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +16 -1
  56. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,251 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.62.0 (2026-11-04)
4
+
5
+ A new install names the repository of its code, and a service says what it uses, so a map of the installed agents can
6
+ link each to its project and draw its chain (FAA-198, FAA-199).
7
+
8
+ ### Added
9
+
10
+ - The reference service declares its connections (`extensions/connections/0.1`): the template it drafts from and the
11
+ demo source `serve --degraded` marks failing; the online example (`sample-remote-service.mjs`) declares an empty list,
12
+ since it calls nothing outside itself. The support-agent recipe's `SupportAgent.connections()` names its two models
13
+ for the service that runs it, keeping only an id the connections rule accepts, and imports `fabric_connections` only
14
+ when called (the recipe's install line names the module). `check_service.py` reports `well-known.connections` as `NOT_RUN` when the
15
+ well-known document has no connections declaration (it reported nothing before); an empty list is one and reads
16
+ `PASS`, and adapting-projects-to-fabric names the declaration in its
17
+ adapter step (FAA-199).
18
+
19
+ ### Changed
20
+
21
+ - `write_descriptor` / `writeDescriptor` refuse a new descriptor that names no `source.repository` (no descriptor of its
22
+ `id.instance` is installed yet: an installed one is a regular file, whatever it holds, so a folder, a broken link or
23
+ a path that cannot be examined is none), with `ServiceError` naming the descriptor; `register_remote` / `registerRemote` refuse it before they
24
+ write the token file, so a refused registration leaves nothing, and a rotation that names no `source` keeps the
25
+ installed one when the kit accepts it (a hand-edited one it refuses is dropped, as before). Rewriting a descriptor already installed without it still writes it and warns
26
+ (`NoSourceRepositoryWarning`, a `UserWarning` attributed to the installer's call; Node `process.emitWarning`, code
27
+ `FABRIC_NO_SOURCE_REPOSITORY`), so an installed agent's update keeps working. `source_refusal` / `sourceRefusal` say
28
+ why an install would be refused, for an installer that checks before it validates or writes anything else (a `source`
29
+ that is no object names nothing), and `names_repository` / `namesRepository` whether a descriptor names one.
30
+ `check_service.py` names a descriptor without it, or with one the descriptor's rules refuse, and why
31
+ (`descriptor.source-repository`, `NOT_RUN`, never a pass), and prints a repository without its userinfo, query or
32
+ fragment (a URL through `redact_url`; a local path, one that starts with `/`, `~`, `.`, `\\` or a drive, by its name;
33
+ a value of a URL scheme such as `https:` also without `//`, through `redact_url`; an scp form or any other value from
34
+ after its user and password, whatever they hold; one with a piece shaped like a credential that is no name not at
35
+ all), as
36
+ `state.outside-code` and `dashboard.opens-in-host` now do too for the source and for the data checkout's origin (they
37
+ printed them as found).
38
+ `sample_service.py register` writes this repository, whose code its descriptor runs whatever `--id` names, takes
39
+ `--repository`, and writes its manifest only after the descriptor, so a refusal of any kind leaves none naming it, and
40
+ a manifest it cannot write takes a new descriptor back with it, or puts an installed one's bytes back; an installed
41
+ descriptor it cannot read is never replaced (it refuses before writing anything). No owner field is added: the
42
+ organization follows from the repository (FAA-198). `source.repository` is read for a credential by one rule in both kits and
43
+ in the provider entry's check (`repository_credential` / `repositoryCredential` in `fabric_secrets`): its user, its query
44
+ and fragment as `secret_url` reads them (a parameter named for a credential, such as `access_token`, now counts), and
45
+ a path segment shaped like a key; a path segment in lower case, or of letters with no digit, is the repository's
46
+ name unless a key's shape (such as `sk-`, `ghp_`, `xoxb-`, `glpat-`, `AKIA`, `eyJ`) starts it or follows a `.`,
47
+ `_` or `-` in it, so `task-pipeline-dashboard` and `Task-Pipeline-Dashboard`, which read as an `sk-` key, are accepted (a source with it was refused before). A user is accepted only in a URL's
48
+ authority (`https://user@host/path`): an scp form or an opaque URI holding an `@` (`bot:...@host:path`,
49
+ `https:user@host/path`) is refused, and so is a password in the user information read any way a reader takes it:
50
+ as written (`https://h\u:pw@x/r`, as git reads it) or as the URL standard does (`https:///u:pw@h/r`). Node reads
51
+ the user information and parameters by hand, as Python does, so a special scheme written without `//`
52
+ (`https:host/r?access_token=...`) or a host `new URL` refuses is read alike.
53
+
54
+ ### Fixed
55
+
56
+ - `redact_url` / `redactUrl` (the URL an `HttpError` carries and `check_url` / `checkUrl` print) dropped a user
57
+ information only as the URL standard reads it, so `https://h\u:pw@x/r`, which git and Python's `urllib` read as a
58
+ password, printed `pw` in its path in both kits (0.61.0 too); they now drop the user information as
59
+ written first, after any run of slashes or backslashes that follows the scheme (`https:/h\u:pw@x/r`,
60
+ `https:\\h\u:pw@x/r`; 0.62.0 seam readings 11 and 13).
61
+ - Node's `checkUrl` sent `ht<tab>tps://h\u:pw@x/r` and `https:\/h\u:pw@x/r`, which Python's `check_url` refuses
62
+ (since 0.60.0): it now reads the written user information on the text `new URL` reads, after any run of slashes.
63
+ - Node's `redactUrl` printed `<unparseable url>` for a URL of a scheme other than http, https, ws, wss or ftp (`file:`
64
+ included) that `new URL` cannot parse (`ssh://[zz]/r`, `ssh://h:99999/r`, `file://h:1/r`), where Python keeps the
65
+ scheme: both now print `<scheme>:<redacted>`.
66
+ - A subscription `runner.kind` that is no string (a list or an object) raised `TypeError` in Python's
67
+ `validate_descriptor`, `write_descriptor` and `check_service.py` (0.61.0 too), and Node, for a list whose text is a
68
+ subscription runner's name (`["claude-code"]`, which `Object.hasOwn` reads as that key), named only one of its two
69
+ problems: both kits now name both as descriptor problems.
70
+ - Node's key set cache (`JwksCache`) accepted `https://h\u:pw@x/j` and `https://@x/j`, which the Python twin refuses:
71
+ it now reads the authority as written too.
72
+
73
+ ### Upgrading
74
+
75
+ - An installer that writes a descriptor for a new install (`register_remote` / `registerRemote` included) and a desktop
76
+ app that writes its own (DEC-0069) pass `source: {"repository": "https://github.com/owner/name"}` (the repository
77
+ their code is built from) in the call that writes it: a source set by a later write is too late, since the first call
78
+ is refused before anything is written. An installer that loads the kit from the installed plugin meets this as
79
+ soon as the plugin updates; an installed agent's next update warns until it names one. Only a rewrite in
80
+ place keeps working: an update that removes the descriptor before it writes it again, a new instance (a preview) and a
81
+ moved services directory are new installs. In Python the warning is `NoSourceRepositoryWarning` (a `UserWarning`), so
82
+ a suite that runs with warnings as errors (`-W error`) sees the rewrite raise; filter that class there.
83
+ - Once a descriptor names its repository, two probe rules that read `NOT_RUN` without it start judging:
84
+ `state.outside-code` fails data kept inside that checkout, and `dashboard.opens-in-host` reads the checkout for pages
85
+ opened in a browser. A service that passed may fail them until its data moves out of the checkout or its pages open
86
+ through Fabric Dashboards.
87
+ - Declare what the service uses with `connections_extension` / `connectionsExtension` (an empty list when it uses
88
+ nothing): until it does, `well-known.connections` reads `NOT_RUN` and the connection map has no row for it.
89
+ - A `source.repository` (a service's or a provider entry's) that names a user outside a URL's authority (an scp form,
90
+ `https:user@host/path`) or carries a query parameter named for a credential is refused: write it as
91
+ `https://github.com/owner/name`, with no user, query or fragment. A path segment that mixes upper case with a digit
92
+ is read for a key's shape (`sk-` and 16 more characters, a `ghp_` run, 40 characters of mixed case and digits), so
93
+ `Task-Pipeline-Dashboard2` is refused while `Agent-V2` is not: write the repository's name in lower case (hosts such
94
+ as GitHub take either). A segment that a key's shape starts, or follows a `.`, `_` or `-` in, is refused whatever
95
+ its case and also percent-encoded (`%73k-` reads as `sk-`). The shapes are those of `fabric_secrets.SECRET_VALUE`,
96
+ such as `sk-` and 16 more characters, `gh` and one of p, o, u, s, r then `_` and 20, `xox` and one of a, b, o, p, r,
97
+ s then `-` and 10 (so the word `xoxo-` counts), `glpat-` and 20, `AKIA` and 16, and a JWT as written (`eyJ` in
98
+ that case, `JWT_SHAPE`); `bearer` and a space counts at a segment's start once decoded (`Bearer%20Tracking` is
99
+ refused, `team-Bearer%20Tracking` stays a name), and anywhere in a segment or a host when 12 or more token
100
+ characters with a digit follow it and no letter, digit or `_` comes before it (`x-Bearer%20<a token>` is refused).
101
+ A host is read for a key's shape anywhere in it, inside a word too, so `task-pipeline-dashboard.example.com` is
102
+ refused although the same path segment is a name (FAA-200 (32)). Such a host, like a repository such
103
+ as `sk-learn-` followed by a longer name, has no spelling the kit accepts, so an agent built from it is not installed
104
+ new, and
105
+ an installed one keeps working by a rewrite in place that leaves the source out (FAA-200 (24)).
106
+
107
+ ## 0.61.0 (2026-11-04)
108
+
109
+ The board responder reads the operator's mandate from a request of his own Project (FAA-135), and working-with-fabric
110
+ says that his approval holds for the whole task, at every link (FAA-195).
111
+
112
+ ### Added
113
+
114
+ - `Responder(..., agent_id=, mandate_from=)`: with both set, a request whose sender the board verified
115
+ (`provenance: trusted`) and whose Project `mandate_from` names (a uuid in any case; or every such sender,
116
+ `mandate_from="personal-board"`, only on the personal board's responder) is read for one `mandate/1` block fenced
117
+ ` ```json `, each fence at the start of its own line with no indent, in its body (contract project-comms.md, DEC-0125 MD-3). The handler gets it as `ctx.mandate`, and
118
+ `ctx.mandate_covers(call)` answers `mandate_covers` with the sender placed as T1, the responder's wall clock (`wall=`,
119
+ `time.time` by default; Node `Date.now`, milliseconds) and the sender Project as the `caller` of a covered answer's
120
+ `recordAs`; None when no mandate travelled, and `{"fault": "now-unreadable"}` when the clock returns a value that is not a finite instant (one that raises passes its
121
+ error to the handler). A `source` in the block is dropped (OA-7). Two `mandate/1` blocks, or one whose text holds
122
+ `mandate/1` and is not JSON (an integer longer than 4300 digits included, on every Python), read as none, with a `board.request.mandate_unread` warning carrying the request id and its `reason`, never the block.
123
+ Off by default: a responder that names neither reads nothing, as before. Node's `WorkContext` serialises without the
124
+ mandate. Both kits (FAA-135). `ctx.mandate_covers` reads the call's `spendUsd` and `project` failing closed, as the
125
+ kit's MCP and job mandate readers do (FAA-139): a spend that is not a finite number is none, which a `perCallMaxUsd`
126
+ refuses, and a `project` that is not a string (in Python an exact `str`, so a subclass too: the core compares a
127
+ project by exact type, so every Python reader refuses one there; the board path marks it unreadable first), None (Node null) included, is refused by a mandate limited to
128
+ projects, as the contract's `mandateCovers` refuses a null project (the MCP and job readers read None as naming
129
+ none; leave `project` out on the board) (a draft of this release passed them as given; 0.61.0 seam reading 14). Only
130
+ the five members `mandate_covers` reads are read, each once, as the contract reads them (`call[name]` in Python; a
131
+ plain property read in Node, inherited, getter and Proxy members included), but for a Node spend, read as the call's
132
+ own member; the rest of a call is never touched (0.61.0 seam readings 16 and 17).
133
+
134
+ ### Fixed
135
+
136
+ - The Python mandate readers (McpToolServer and `action_gate`, the job step, and the core `mandate_covers` and
137
+ `action_decision` beneath them; a draft of this release's board path too) read a spend estimate that is an `int` or
138
+ `float` subclass through its own `float()` and comparisons, so a subclass whose `float()` answered 0.0 for 100, or
139
+ whose `>` lied about an integer past the double range, passed a `perCallMaxUsd` of 5. A number is now an `int` or
140
+ `float` by its real type (`fabric_jsvalue.is_number`, never `__class__`) and is read by the number it stores
141
+ (`real_number`: `float.__float__`, `int.__int__`), in `mandate_spend`, `js_to_number`, `double` and McpToolServer's
142
+ estimate: an honest subclass such as `numpy.float64` keeps its number, a lying one is read as what it holds. A value
143
+ that only claims a number through `__class__` (a mock with `spec=float`, a lazy proxy), a `Decimal` or a
144
+ `numpy.int64` is no estimate: the call is answered and a `perCallMaxUsd` refuses it, in every reader. The core
145
+ `mandate_covers` reads a spend only when it is a number or JSON data as `json.loads` gives it, by exact type all the
146
+ way down, so for its direct callers a tuple, bytes, a `str`, `list` or `dict` subclass, or a list holding a number
147
+ subclass is no estimate either, a stricter reading than 0.60.0's (checked only under a ceiling, without recursion in one
148
+ pass, then read as `Number()` reads it, in time linear in the structure (a list is never joined: two or more items are
149
+ NaN, one is read for itself); a shared or cyclic list or dict, which `json.loads` never gives, is no estimate, and a spend whose lists nest in
150
+ lists more than 100 deep (`MAX_SPEND_DEPTH`, a dict between them ends the count) is no estimate, so the spend a
151
+ refusal carries as `estimateUsd` is one a serialiser writes: that nesting is what `String()` of a list recurses
152
+ through, so 0.60.0 raised RecursionError a few hundred deep (about 500 on Python 3.14, 330 on 3.9) and Node's
153
+ `mandateCovers` and the contract throw RangeError a few thousand deep, and between 100 and those depths the core is
154
+ stricter than all three, a fail-closed twin split that no board, MCP or job reader reaches, since they take no list
155
+ or dict spend; under a dict there is no bound, since `Number()` never descends a dict, NaN at any depth in all of
156
+ them; a dict with an own `toString`, at the top or reached through lists alone, is no estimate too, where 0.60.0
157
+ read it as NaN and Node and the contract throw TypeError). A bool is none in `mandate_spend`, `action_decision` and McpToolServer as before, while
158
+ the core `mandate_covers` reads it as JavaScript's `Number()` does, as the contract's `mandateCovers` does (0.61.0
159
+ seam readings 17 to 22, 24 and 26 to 29, product readings 18 to 20 and 22 to 27, unit readings 19, 21, 24, 26 and 28 to 30). `js_to_number` tested a bool with `isinstance`, which asks
160
+ a number subclass's own `__class__`: one claiming `bool` was read as 1 under a ceiling, and one whose `__class__`
161
+ raised made the core raise (older than 0.61.0); it tests a bool by identity now (0.61.0 unit reading 30). A job's approval step kept a `str`-subclass project (a `(str, Enum)` member included) as given, which
162
+ the start refused by the core's exact-type compare but the job file then wrote as its plain string, so the answer
163
+ covered it, and the same held for a step a service keeps itself through `request_input(steps=)` or Node's
164
+ `requestInput({steps})` (a `String` object project, a `Number` object spend); both stores now keep a step as the gate
165
+ reads it (`kept_step` / `keptStep`: a spend through `mandate_spend`, a project as an exact `str` or a primitive
166
+ string, else `UNREADABLE_PROJECT`), and `job_step`, `action_gate` and McpToolServer read a project as an exact `str`
167
+ too, as the board path does, so the start and the answer agree, and a value that only claims `str` never reaches the
168
+ job file (older than 0.61.0; 0.61.0 seam reading 32, readings 33). Node's `JobStore.answer` matched an answer
169
+ keyed by an inherited name (`constructor`, `toString`) as an asked question and cleared the real one; it matches an
170
+ own key only, as Python does (older than 0.61.0; 0.61.0 seam reading 36). Both stores' `answer` match nothing when
171
+ the answer's `inputResponses` or the job's `inputRequests` is not a plain object (a hand-edited file, an array), and
172
+ McpToolServer answers a `fabric.job.get` whose `inputResponses` is present, not `null` (read as a poll, as before)
173
+ and not a JSON object with `-32602`:
174
+ 0.60.0 raised out of `handle()` for a non-empty one on a job waiting `input_required` (the transport sent no reply)
175
+ and read an empty list or string, `0`, `false`, or any non-object on another job or an unknown id, as no answer or
176
+ `unknown-job` (a client that writes an empty map as `[]` now gets `-32602`; for an unknown id on a server without
177
+ `authorize`, `-32602` comes before `unknown-job`, where a server with `authorize` answers `unknown-job` first). Both stores' `request_input` /
178
+ `requestInput` refuse requests that are not a JSON object (before they read a step), and a request that is
179
+ not a keyed object (under a string key, a dict in Python, in Node an object that is not null, an array or a function), with `InteropError` (0.60.0 Python raised `AttributeError` or `TypeError`, by the shape and whether
180
+ `steps=` was passed; Node kept an array, which an answer keyed by an index or `length` then matched), and McpToolServer's answer gate reads only an object of requests (older
181
+ than 0.61.0; 0.61.0 seam and unit readings 37 and 38). The same split held for an
182
+ effect: Python admitted a `str`-subclass effect (`isinstance`) that the core then refused at the start and the job
183
+ file wrote as a plain string the answer covered; `job_step` (when a mandate travelled), `request_input(steps=)` and
184
+ `add_tool` (with an action) now admit an effect as an exact `str` (an `InteropError` otherwise, as Node's `requestInput` already refused a `String` object),
185
+ and both stores read each member of a step once and keep the value they checked (0.61.0 seam and unit readings 34). `js_to_number`'s decimal
186
+ pattern backtracked over a long digit string that fails to match, quadratic in its length; it now costs the length
187
+ (0.61.0 seam reading 22).
188
+ - `InstanceLock.acquire` (Python, `fabric_lock.py`): when writing the pid failed after the lock was taken (a full disk, an
189
+ I/O error, an interrupt), the descriptor stayed held, and the lock name too, since the caller drops the object
190
+ (`release()` would have freed only the name, on Linux and Windows), so the same process could not start the service
191
+ again. The lock now lets both go and re-raises the first error, even when the
192
+ close itself fails; the Node twin
193
+ (`fabric-lock.mjs`), whose caller drops a lock that failed to start, does the same for its descriptor or its name,
194
+ the first error standing in both.
195
+ Found by a service that vendors the kit, on the board.
196
+
197
+ ### Changed
198
+
199
+ - working-with-fabric's mandate section: every hand-over within a task carries the four parts of the operator's
200
+ approval (`source` only where OA-7 lets it), and each receiver acts on them without asking him again, narrowing only
201
+ (human gates stay, MD-6); a relayed receiver's chain starts with it (OA-2), so the four-agent bound limits one
202
+ mandate, never the task; a mandate lapses within seven days of his words, and a task still open asks him once for a
203
+ new one, his same words with `at` when he confirms (MD-1), then weekly until the contract's CT-205 (its answer
204
+ `7f372925` on board thread `118f95f9`; CT-205 is a row the contract proposes after this kit's pin, on its branch
205
+ `agent/template-parity-20261103`); a link whose chain already holds four agents hands on no mandate and never restates its own: the four
206
+ parts go as a relay through OA-3 (his own Project on the board or his local session), whose receiver states its own (OA-2),
207
+ as the contract confirmed on board thread `118f95f9` (message `adf0c61a`); references/project-board.md and building-fabric-services
208
+ references/interop.md say the relay names the scope in force and the held `expiresAt`, which the receiver's mandate
209
+ does not pass (the kit's bound); and, as the contract proposes until CT-205,
210
+ no agent takes a relay for a task it already leads; references/interop.md says a service has no route past four agents until CT-205. `expiresAt` is counted from `at` and
211
+ covers the task's length (the kit's `operator_mandate` defaults to a day; references/interop.md says to pass it); the
212
+ source and spend sentences are reworded with no change in rule, and Step 5's lane-restart paragraph drops its reason
213
+ sentence ("The stop is usually stale by then.") for room, its rule unchanged; the paragraph for a client without `_meta`
214
+ leaves the mandate section for Step 5's routes (CT-125), and two sentences that restated rules kept elsewhere in the
215
+ section are dropped (FAA-196 (6)). A scenario eval `an-approval-holds-for-the-whole-task` (FAA-195).
216
+ - working-with-fabric references/project-board.md says what a kit service reads from a board request: one `mandate/1`
217
+ block fenced ` ```json ` at the start of its own lines, its chain ending in the service's registry agent id, never a
218
+ decision block; and what comes back: `mandate-not-for-this-call` is handed on again to the right id without asking
219
+ him, `mandate-expired` asks him once for a new one.
220
+
221
+ ### Upgrading
222
+
223
+ - `fabric_board_link` (and `fabric-board-link.mjs`) now imports `fabric_interop_rules`, so a board responder's install
224
+ brings it and its `fabric_delegation` too (`fabric_kit.py install --module fabric_board` copies 33 modules, 31 in
225
+ 0.60.0): reinstall with `fabric_kit.py install` and check with `status`, never by copying `fabric_board_link` alone.
226
+ - A Python service that passes a job or tool project as a `(str, Enum)` member or another `str` subclass, under a
227
+ mandate limited to projects, is now asked at a job's answer as at its start: pass the plain string (`member.value`).
228
+ An effect passed that way to `approval_step`, `job_step` (when the start call carried a mandate), `request_input(steps=)` or `add_tool` with an action now raises
229
+ `InteropError`: pass `member.value` there too. A Node service that keeps a step through `requestInput({steps})` with
230
+ a `String` object project (under a mandate limited to projects) or a `Number` object spend (under a `perCallMaxUsd`)
231
+ is likewise asked at the answer: pass the primitive value.
232
+ - A kept step holds only `effect`, `spendUsd` and `project`, read as the step's accessors answer them (Node property
233
+ access, Python `.get`), where 0.60.0 kept the step's JSON form (Node `JSON.stringify`, Python `json.dumps`, which reads a
234
+ `dict` subclass through its `items()`): a
235
+ service that put its own keys in `steps=` keeps them elsewhere. The two differ for a Node step that is a function
236
+ (0.60.0 kept no step), an array, an object whose members are inherited (a class's getters included) or not
237
+ enumerable, or one with its own `toJSON`, and for a Python `dict` subclass whose `.get` or `items()` answers otherwise
238
+ than its storage: such a step is now kept as its accessors answer, and an accessor that throws for the spend or the project,
239
+ which 0.60.0 never called, now throws from `requestInput` / `request_input` (the job stays `working`).
240
+ - A job waiting `input_required` whose approval step 0.60.0 kept keeps it as 0.60.0 wrote it: a `str`-subclass
241
+ project or effect, or a Node `String` or `Number` object, as its plain value, and the step kinds above in their JSON
242
+ form. A step so kept with its effect but no project is covered at a 0.61.0 answer by a mandate limited to projects;
243
+ one kept with no effect waits for the operator (`effect-missing` where a step is kept, `no-decision` where none is),
244
+ and a Node job whose requests 0.60.0 kept as an array can no longer be answered. Answer or cancel such jobs before
245
+ upgrading.
246
+ - Python McpToolServer answers a `fabric.job.get` whose `inputResponses` is not a JSON object (`[]` and `""`
247
+ included), on any job the caller may read, `-32602` (Node has no tool server): send an object, or leave it out (or `null`) to poll.
248
+
3
249
  ## 0.60.0 (2026-11-03)
4
250
 
5
251
  The board responder is ready to find a slot the operator moves back to it, once Fabric offers the move (FAA-165), and four smaller fixes: working-with-fabric
package/README.md CHANGED
@@ -72,7 +72,8 @@ requests other Projects post to its Project on Fabric's project board uses the b
72
72
  (`scripts/fabric_hub.py` and `scripts/fabric_board.py`, Node `fabric-hub.mjs` and `fabric-board.mjs`): the
73
73
  `board.respond` grant, enroll, claim, accept the exact digest, complete, a checkpoint before every step, and restarts
74
74
  that reconcile or take accepted work back by a transfer, unless the slot was moved to another session (until the
75
- operator's take-back, or a move back once Fabric offers one) (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
75
+ operator's take-back, or a move back once Fabric offers one), and, when the service opts in, the operator's
76
+ `mandate/1` from a trusted request of his own Project (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
76
77
 
77
78
  ```bash
78
79
  python3 plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py example-agent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.60.0",
3
+ "version": "0.62.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.60.0",
6
+ "version": "0.62.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -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.60.0"
8
+ version: "0.62.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "3830a3353923e20f826d466610e1c479f120bec0"
11
11
  ---
@@ -143,6 +143,9 @@ Preserve the chosen ownership boundary:
143
143
  `building-fabric-services`'s `scripts/fabric_interop.py`.
144
144
  - Local runner maps typed input, executable identity, result location, cancellation,
145
145
  heartbeat, and partial results without relying on ambient accounts.
146
+ - A service declares what it uses (`extensions/connections/0.1`: models, other agents, connectors, stores,
147
+ channels) with `connections_extension` / `connectionsExtension`, and its descriptor names `source.repository`;
148
+ the probe names either one missing (FAA-198, FAA-199).
146
149
 
147
150
  Keep model selection and provider-internal reasoning outside the Fabric contract. Expose
148
151
  typed outcomes, evidence, artifacts, and protocol-visible state, not chain-of-thought.
@@ -57,8 +57,10 @@ It refuses, with one sentence that never quotes a value:
57
57
  (`test_provider.SchemaParityTests` compares them with a contract checkout at the pin when `FABRIC_CONTRACT_NEW` names
58
58
  one, as the contract job of the nightly CI does), and the reader is held to every `provider.schema.json` fixture of
59
59
  the pin (`test/fixtures/contract/provider-fixtures.json`, `test_provider.ContractFixtureTests`);
60
- - a `source.repository` with a `user:password@` or a value shaped like a credential: it names the repository, never
61
- reaches it (since 0.49.3, as the service kit's descriptor check);
60
+ - a `source.repository` with a `user:password@`, a user outside a URL's authority, or a value shaped like a credential
61
+ in its user, query or fragment or as a path segment (one in lower case, or of letters with no digit, is the name unless a key's shape starts it or follows a `.`, `_` or `-`, read as written and percent-decoded; a host is read for a key's shape anywhere in it,
62
+ inside a word too, so `task-pipeline-dashboard.example.com` is refused, FAA-200 (32)): it names the
63
+ repository, never reaches it (since 0.49.3; one rule with the service kit's descriptor check since 0.62.0);
62
64
  - a manifest whose name follows a backslash (`C:\Agents\example\fabric-agent.json`). The reader,
63
65
  `validate_provider_entry`, accepts it since DEC-0145 (`MANIFEST_PATTERN`, the schema's), but a reader that has not
64
66
  re-pinned to DEC-0145 refuses the entry, Fabric's registry among them (CT-192), so the writer does not emit it until
@@ -147,7 +147,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
147
147
  problems.append("source is {repository?}")
148
148
  elif "repository" in source and not _is_uri(source["repository"]):
149
149
  problems.append("source.repository is an absolute URI")
150
- elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.secret_argument(source["repository"])):
150
+ elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.repository_credential(source["repository"])):
151
151
  # FAA-120, as the service kit: a repository is named, never reached with a credential in its URI.
152
152
  problems.append("source.repository carries no password or credential: it names the repository")
153
153
  if not isinstance(entry["installedAt"], str) or not _TIMESTAMP.fullmatch(entry["installedAt"]):
@@ -185,3 +185,60 @@ def secret_url(value: str) -> bool:
185
185
  if any((raw != "" and credential_name(name)) or secret_value(raw) for raw in raws):
186
186
  return True
187
187
  return False
188
+
189
+
190
+ _REPOSITORY_URI = re.compile(r"\A([A-Za-z][A-Za-z0-9+.-]*:(?://[^/?#]*)?)([^?#]*)(.*)\Z", re.S)
191
+ _REPOSITORY_NAME = re.compile(r"(?:[a-z0-9._-]+|[A-Za-z._-]+)\Z") # lower case, or letters with no digit (readings 8)
192
+ _REPOSITORY_AUTHORITY = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://")
193
+ _KEY_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))(?:%s)" % SECRET_VALUE.pattern, re.IGNORECASE | re.ASCII)
194
+ _JWT_AT_A_WORD = re.compile(r"(?:\A|(?<=[._-]))%s" % JWT_SHAPE.pattern)
195
+
196
+
197
+ def repository_name(piece: str) -> bool:
198
+ """Whether a path segment reads as a repository's name: lower case, or letters with no digit, with no provider
199
+ key's or JWT's shape (`sk-`, `ghp_`, `xoxb-`, `AKIA`, `eyJ`) at its start or after a `.`, `_` or `-` (0.62.0
200
+ readings 9 and 10), so a letters-only key is still a key while `task-pipeline-dashboard`, which holds an `sk-` shape
201
+ inside a word, is a name."""
202
+ return bool(_REPOSITORY_NAME.match(piece)) and not _KEY_AT_A_WORD.search(piece) and not _JWT_AT_A_WORD.search(piece)
203
+
204
+
205
+ def ascii_decoded(text: str) -> str:
206
+ """Every `%XX` escape of an ASCII character decoded, the rest left as written: how a repository's path and user
207
+ information are read as well as written, since git and hosts decode them (`%73k-` is `sk-`, `u%3Apw` is `u:pw`;
208
+ 0.62.0 readings 11). The Node twin is asciiDecoded."""
209
+ return re.sub(r"%([0-7][0-9A-Fa-f])", lambda escape: chr(int(escape.group(1), 16)), text)
210
+
211
+
212
+ def repository_password(value: str) -> bool:
213
+ """Whether a repository URI's user information holds a password, in any scheme, `file` too: every slash and
214
+ backslash after the colon skipped, then up to the next `/`, `?` or `#`, a backslash kept in the user. That one
215
+ reading holds git's (`https://h\\u:pw@x/r`), curl's (`https:///h\\u:pw@x`) and the URL standard's
216
+ (`https:///u:pw@h`, which stops at a backslash, a prefix of it) (0.62.0 readings 10 to 14). The user information
217
+ ends at the last `@`; a password follows its first `:`, as written or ascii_decoded. The Node twin is
218
+ repositoryPassword."""
219
+ value = re.sub(r"[\t\n\r]", "", value.strip(_C0_OR_SPACE))
220
+ scheme, colon, rest = value.partition(":")
221
+ authorities = [re.match(r"[^/?#]*", rest.lstrip("/\\")).group(0)] if colon else []
222
+ users = [authority.rpartition("@")[0] for authority in authorities]
223
+ return any(user.partition(":")[2] for user in users + [ascii_decoded(user) for user in users])
224
+
225
+
226
+ def repository_credential(value: str) -> bool:
227
+ """FAA-198 (0.62.0 readings 6 and 7): whether a repository URI carries a credential: in its scheme and authority (a
228
+ user), in its query or fragment as secret_url reads them, or as a path segment shaped like a key. A path segment in
229
+ lower case, or of letters with no digit (task-pipeline-dashboard, Task-Pipeline-Dashboard), is the repository's
230
+ name, never a key, though it reads as an `sk-` shape, unless a key's shape starts it or follows a `.`, `_` or `-` in
231
+ it (repository_name); a segment is read as written and ascii_decoded. A
232
+ user outside a URL's authority (an scp form, `https:user@host/path`) counts as one: a user is an authority's."""
233
+ if "@" in value and not _REPOSITORY_AUTHORITY.match(value):
234
+ return True
235
+ match = _REPOSITORY_URI.match(value)
236
+ if not match:
237
+ return secret_argument(value)
238
+ head, path, tail = match.groups()
239
+ if repository_password(value):
240
+ return True
241
+ odd = [piece for written in (path, ascii_decoded(path)) for piece in re.split(r"[/\\:]", written)
242
+ if piece and not repository_name(piece)]
243
+ # the scheme and authority decoded too: git sends `https://%67hp_...@h` with its user decoded (unit reading 12)
244
+ return secret_url(head + tail) or secret_argument(ascii_decoded(head)) or any(secret_argument(piece) for piece in odd)
@@ -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.60.0"
17
+ version: "0.62.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
20
  extension-commit: "3830a3353923e20f826d466610e1c479f120bec0"
@@ -65,16 +65,17 @@ Answer before writing code, and record the answers in the project README:
65
65
  | What it stores | the store, where it lives (Step 2, rule 5), and what survives uninstall |
66
66
  | Data and uptime | kind U or O, kit folders, a durable store or `<id>-data`, uptime A or B; a new agent's `LICENSE` holder, transfer audit per release (creating-fabric-agents) |
67
67
  | What it tells the operator | the events worth a notification (Step 5) |
68
+ | Source | `source.repository`, required (Step 4); connections, `[]` if none |
68
69
 
69
70
  ## Step 1: choose the surfaces
70
71
 
71
72
  | Caller | Surface | Why |
72
73
  |---|---|---|
73
- | Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session; stdio-per-session once left twenty stray servers on this machine |
74
+ | Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session (stdio-per-session once left twenty stray servers) |
74
75
  | The operator or a script | a CLI: `<tool> service status\|start\|stop\|restart`, `<tool> dashboard` (opens only in Fabric Dashboards, below), `<tool> doctor --json` | every verb delegates to launchd and the well-known document, never to its own process table |
75
76
  | A remote agent | A2A `1.0` over HTTPS, or MCP behind an authenticated gateway | loopback reachability is not authorization |
76
77
  | An A2A peer on this computer | `surfaces.a2a` names a public Agent Card, served without the token; its interface URLs are loopback (DEC-0122); the kits read it, serving one is FAA-10 | the card carries no credential, process member or required Fabric extension |
77
- | The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the CSRF defence all four existing dashboards use |
78
+ | The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the existing dashboards' CSRF defence |
78
79
 
79
80
  Read [the surfaces and auth reference](references/surfaces-and-auth.md) when wiring
80
81
  MCP registration into a client config, the login flow or the CSRF header.
@@ -213,8 +214,8 @@ descriptor's `fabricManifest` at a manifest whose service key names this
213
214
  The installer, not the service, owns the plist and the descriptor. Sequence:
214
215
 
215
216
  1. `write_descriptor(descriptor)`: refuses a port or `id.instance` another descriptor
216
- claims. A preview or branch copy is a second **instance**, never a second id, and it is
217
- one more service the operator sees, probes and gets notified by. Verify a release with its
217
+ claims, and a new one without `source.repository` (a rewrite warns). A preview or branch
218
+ copy is a second **instance**, never a second id: one more service the operator sees. Verify a release with its
218
219
  own doctor or self-check before it replaces `default`; a temporary instance is uninstalled
219
220
  (`launchd_uninstall`, `remove_descriptor`) as soon as its check is done, not left running.
220
221
  2. `launchd_plist(...)` then `launchd_install(...)`: writes and lints the plist,
@@ -269,7 +270,7 @@ refusals.
269
270
  **Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
270
271
  agents, models, connectors, stores and channels, each with its state, from
271
272
  `connections_extension()` / `connectionsExtension()` in `fabric_connections`. Never a credential in it, never a call
272
- count (that is the usage report); the details are in [the wire reference](references/protocol.md).
273
+ count (that is the usage report); details: [the wire reference](references/protocol.md).
273
274
 
274
275
  The events feed is a **view over the log you already keep** (a jobs table, a JSONL
275
276
  journal), not a second store. Each event is one sentence a person reads; set
@@ -298,7 +299,8 @@ rule it can reach and says which it could not:
298
299
  python3 <skill-dir>/scripts/check_service.py <id>[.<instance>]
299
300
  ```
300
301
 
301
- It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim, well-known
302
+ It reports `PASS`, `FAIL` or `NOT_RUN` per rule (descriptor, port claim, `descriptor.source-repository`,
303
+ `well-known.connections`, well-known
302
304
  shape, identity, latency, Host/Origin/cross-site guards, loopback bind, token file,
303
305
  events auth and shape, single-use login, state outside code, instance lock, plist,
304
306
  launchd pid equals answering pid, `dashboard.declared` (ED-9), `well-known.actions` (DEC-0121),
@@ -405,8 +407,8 @@ this estate needs.
405
407
  - **Not macOS / no launchd:** the kits write a systemd user unit (Linux) or a per-user Scheduled Task (Windows); with
406
408
  no supervisor at all declare `lifecycle.manager: "none"`, and hosts show the service read-only. Keep every other rule.
407
409
  - **A desktop app with no supervisor** (DEC-0069): its descriptor says `manager: "person"` with `lifecycle.app`, no
408
- origin, `installedBy {by: "person", role, tool}`; its live origin is in its own liveness file
409
- (the lifecycle reference, *An app's own descriptor and the node*).
410
+ origin, `installedBy {by: "person", role, tool}`, `source.repository` (the lifecycle reference, *An app's own
411
+ descriptor and the node*).
410
412
  - **No Python:** use the Node kit; run the probe from any machine that has Python and
411
413
  can reach the port, or mark the probe `NOT_RUN` with the reason.
412
414
  - **Neither kit fits the framework:** implement the four routes and the lock by hand
@@ -71,9 +71,16 @@ names the same file.
71
71
 
72
72
  ## Install into a service
73
73
 
74
+ The service that runs it says what it uses (FAA-199): `agent.connections()` names the two models, and the service adds
75
+ its own hand-off route, in its well-known document:
76
+ `fs.connections_extension(agent.connections() + [...], degraded=degraded)`. The two take the ids `model` and `fallback`
77
+ (a `degraded` entry whose source is `fallback` marks the fallback); the service's own items take other ids, since one id
78
+ named twice is refused.
79
+
74
80
  ```bash
75
81
  python3 <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py \
76
- install <service>/support --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths
82
+ install <service>/support --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths \
83
+ --module fabric_connections
77
84
  cp <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py <service>/support/
78
85
  python3 <adapter>/.../fabric_kit.py status <service>/support # current, older or modified, per file
79
86
  ```
@@ -24,8 +24,9 @@ For each message it:
24
24
  What the agent does not do: send the reply anywhere (the service that calls it owns its channel), judge facts with a
25
25
  model, or answer outside the knowledge file. The model's text is data: a citation it invents fails the check.
26
26
 
27
- The kit modules (fabric_openrouter and its closure) sit beside this file in a service
28
- (`fabric_kit.py install <dir> --module fabric_openrouter`); in this repository they are read from ../../scripts.
27
+ The kit modules sit beside this file in a service, installed as the README's "Install into a service" says
28
+ (`fabric_kit.py install <dir> --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths
29
+ --module fabric_connections`); in this repository they are read from ../../scripts.
29
30
  Standard library only. FAA-65 REQ-09.
30
31
  """
31
32
  from __future__ import annotations
@@ -371,6 +372,22 @@ class SupportAgent:
371
372
  self._local = threading.local()
372
373
  self._log = DecisionLog(self.state_dir)
373
374
 
375
+ def connections(self) -> List[Dict[str, Any]]:
376
+ """FAA-199: what the agent uses, for `connections_extension` in the well-known document of the service that runs
377
+ it (DEC-0096); that service adds its own hand-off route and channels."""
378
+ # A model id that is not one (empty, missing, with a space) is left out, so a misconfigured fallback degrades the
379
+ # replies, never the well-known document (0.62.0 seam reading 1).
380
+ named = [("model", self.model, "drafts replies from the knowledge file"),
381
+ ("fallback", self.fallback, "drafts when the first model fails")]
382
+ import fabric_connections # the service that declares them installs it (README); the agent runs without it
383
+ kept = []
384
+ for ident, target, purpose in named: # by the connections rule itself, never a second copy of it (seam reading 2)
385
+ item = {"id": ident, "kind": "model", "target": target, "purpose": purpose}
386
+ probe = {"extensions": {fabric_connections.CONNECTIONS_EXTENSION_KEY: {"connections": [dict(item, state="unknown")]}}}
387
+ if not fabric_connections.connections_rules(probe):
388
+ kept.append(item)
389
+ return kept
390
+
374
391
  @property
375
392
  def decisions_file(self) -> Path:
376
393
  return self._log.path