@passioncode-ai/passioncode 0.2.44 → 0.2.46
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.
- package/CHANGELOG.md +17 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +9 -7
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +43 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +28 -7
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +222 -24
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +139 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md
CHANGED
|
@@ -42,7 +42,9 @@ operator gives in Fabric (Agents & access): callee `fabric`, capability `board.r
|
|
|
42
42
|
|
|
43
43
|
The responder runs inside the service process, started after the instance lock like every other piece of state
|
|
44
44
|
(SKILL.md, Step 3). [`sample_service.py`](../scripts/sample_service.py) does it behind `serve --board-project <uuid>`,
|
|
45
|
-
off by default
|
|
45
|
+
off by default (register it with the same `--board-project`, so its manifest lists the take-back tool below; a serve
|
|
46
|
+
whose board mode differs from the registered manifest at its start logs `service.manifest.mismatch` and leaves the
|
|
47
|
+
manifest as it is):
|
|
46
48
|
|
|
47
49
|
```python
|
|
48
50
|
import fabric_board as fbd, fabric_hub as fhb
|
|
@@ -53,12 +55,13 @@ responder = fbd.Responder(
|
|
|
53
55
|
handler=render, admit=gate, observe=look_on_disk, emit=log.append, service="example-agent")
|
|
54
56
|
board = threading.Thread(target=responder.run, args=(stop_event, 5.0), daemon=True) # SIGTERM: set stop_event
|
|
55
57
|
board.start()
|
|
56
|
-
# ... at stop, after stop_event is set: wait for it to leave its board session (a clean leave forgets
|
|
57
|
-
# records, FAA-155), bounded inside the kit's hard exit, as scripts/sample_service.py does
|
|
58
|
+
# ... at stop, after stop_event is set: wait for it to leave its board session (a clean leave forgets the slot
|
|
59
|
+
# records it confirmed, FAA-155), bounded inside the kit's hard exit, as scripts/sample_service.py does
|
|
58
60
|
board.join(timeout)
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
Node: `new Responder({...same names in camelCase})`, then `responder.run({ signal, drain })
|
|
63
|
+
Node: `new Responder({...same names in camelCase})`, then `const running = responder.run({ signal, drain })`; at stop,
|
|
64
|
+
abort the signal and `await running`, bounded inside the hard exit (the run leaves its board session before it settles). A service that lists
|
|
62
65
|
`fabric-project-comms/0.1` in its well-known `protocols` (DEC-0067: only once its hosts are pinned at or after it)
|
|
63
66
|
declares that it answers board requests; the probe checks that declaration.
|
|
64
67
|
|
|
@@ -144,6 +147,28 @@ answers `not_available` or `capacity_exceeded`, which do not say whether the rep
|
|
|
144
147
|
in line and the next tick asks again, and a restart asks with the same old fence. A request leaves the resume queue only
|
|
145
148
|
once it has resumed: when the `com.complete` that sends a known outcome loses its answer, the next tick sends the same
|
|
146
149
|
key again in the same session (FAA-86). `shutdown()` drains, then leaves the session (its DELETE releases the slot).
|
|
150
|
+
An idle responder waits 3 s at most for that DELETE (`IDLE_LEAVE_SECONDS`), so an idle stop ends within the org's 5 s
|
|
151
|
+
(LC-01, FAA-173); one with work waits up to the session's 15 s client timeout. A board call already in flight when the
|
|
152
|
+
stop comes (a poll against a slow hub) runs first; when the service's bounded join ends before the DELETE is sent, the
|
|
153
|
+
slot lapses on the board's clock as before. The slot records are forgotten only after
|
|
154
|
+
a DELETE Fabric answered with a 2xx (a 404, the session already gone, keeps them: a move made before it ended is read at the next `com.status`), also when a session is dropped on a live board (FAA-163): an unanswered one keeps
|
|
155
|
+
them, and the slot lapses in 60 s.
|
|
156
|
+
|
|
157
|
+
**One capability at a time (FAA-163).** At connect (and every reconnect) the responder starts with no slot. Each
|
|
158
|
+
capability's enroll or replace is settled on its own: a refusal of that capability (`capacity_exceeded`,
|
|
159
|
+
`invalid_arguments`, any code that is not about the board session) leaves it `retry`, asked again alone after 15 s,
|
|
160
|
+
30 s, doubling to 15 minutes, with one `board.slot.refused` event (warning; said again if it is refused again after
|
|
161
|
+
the slot was held); its accepted work waits `lapsed` and goes back
|
|
162
|
+
by a transfer once the slot is held. The capabilities after it are recovered as before. A refusal of the board session
|
|
163
|
+
(`unreachable`, `session-ended`, `not_available`, `not_authorized` and the other codes in `SESSION_CODES`) leaves the
|
|
164
|
+
rest `retry` too, says no `board.slot.refused`, and goes to the responder's fault, which says it by code:
|
|
165
|
+
`board.access.needed` for `not_authorized`, `authority_changed` and `credential` (the session is left),
|
|
166
|
+
`board.blocked` for a defect (`no-epoch`, `schema-unsupported`, `checkpoint`, `receipt-shape`,
|
|
167
|
+
`idempotency_conflict`), and `board.down` for the rest: `unreachable` keeps the session while a held slot's lease runs
|
|
168
|
+
(so a lost enroll of one capability is asked again at its next retry), `not_available` keeps it except at connect or recovery with no slot held,
|
|
169
|
+
and `session-ended`, `not-running` and the other hub codes drop it (as a fault during connect or recovery does) and
|
|
170
|
+
reconnect. A slot whose lease lapsed (`lease_expired`) holds
|
|
171
|
+
no lease for that decision.
|
|
147
172
|
|
|
148
173
|
## When the operator holds work (DEC-0079)
|
|
149
174
|
|
|
@@ -178,9 +203,16 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
178
203
|
{project, generation, moved?, asking?}}`). The record is written when a receipt's generation changes and before an
|
|
179
204
|
enroll or a replace that can change it; a live holder's renew (about every 20 s) cannot, and writes nothing. At a
|
|
180
205
|
clean leave (the service stopped; the session's DELETE released its slots) the record of a slot with no request left
|
|
181
|
-
in the checkpoint and not moved is forgotten
|
|
182
|
-
|
|
183
|
-
|
|
206
|
+
in the checkpoint and not moved is forgotten when this session confirmed its generation (held it, paused under the
|
|
207
|
+
operator's hold included, or read it unmoved in `com.status`) and no fenced call has put in doubt since: the slot is
|
|
208
|
+
free, and whoever takes it next is no move. An unconfirmed record stays, since the slot may have moved while the
|
|
209
|
+
service was down, and a move this session saw keeps its record, so a clean stop never undoes it. A paused or
|
|
210
|
+
retrying slot reads `com.status` before each new ask. A move made after the slot's last renew or check and before
|
|
211
|
+
the stop, with no call since, is not seen at the leave: its record is forgotten, and the next start reads the slot
|
|
212
|
+
as free and may enroll it once the other holder lets it go (FAA-187). A file without the record
|
|
213
|
+
reads as before; a kit before 0.53.0 reads the file, ignores the record and drops it at its first checkpoint write. So a service stopped cleanly with no request left in its checkpoint, its DELETE answered 2xx and every slot confirmed
|
|
214
|
+
in that session, rolled back to 0.52 and up again starts as on a first start. Otherwise (a 404, the idle wait running
|
|
215
|
+
out, a slot this session never confirmed, one left retrying before `com.status` answered for it included) its records stay, and the case is the crash case below. After a crash the record stays: if 0.52 then ran, enrolled the slot and
|
|
184
216
|
wrote nothing to the checkpoint, the next 0.53 start reads that enroll as a move; bring it back with the service's take-back action (below).
|
|
185
217
|
- An enroll or a replace is written into that record with its key before it is sent (`asking`). When its answer is
|
|
186
218
|
lost, the next connect (or the next enroll) sends the same call again under the same key; the board answers the
|
|
@@ -205,8 +237,8 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
205
237
|
holder, or the estate owner, reconciles it now). No `com.claim`, `com.renew`, `com.complete`, `com.enroll` or
|
|
206
238
|
`com.responder_replace` is sent for that capability again: not at recovery after a restart, and not when the new
|
|
207
239
|
holder's lease lapses, nor when com.status later names no generation for the slot (a recorded move stays until
|
|
208
|
-
take-back). Accepted requests stay
|
|
209
|
-
will hand them to the new holder). One `board.slot.moved` event (
|
|
240
|
+
take-back). Accepted requests stay `accepted` on the board, unanswered, until the slot is taken back (CT-175
|
|
241
|
+
will hand them to the new holder). One `board.slot.moved` event (warning, notify) says so, once, also across restarts
|
|
210
242
|
(the record keeps `moved`).
|
|
211
243
|
- A responder that never held the slot (a first start: no record, or a record of another Project), or whose record
|
|
212
244
|
equals the current generation, behaves as before: it enrolls, recovers and replaces its own lapsed work. A recorded or
|
|
@@ -216,18 +248,28 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
216
248
|
records its own generation. Only a replay the board refuses, or one that ran for the first time (`repeated: false`)
|
|
217
249
|
after the slot had moved past the record, reads as a move: the safe side; a slot such a replay took is left to lapse
|
|
218
250
|
unused. A generation written `2.0` is `2`.
|
|
219
|
-
- Only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record
|
|
251
|
+
- Only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record, and wins over a move
|
|
252
|
+
being read at the same time: the move is written under the responder's lock (Node: on the fence queue, after any fenced
|
|
253
|
+
call in flight) and only while the record is unchanged (FAA-158). Never edit the
|
|
220
254
|
checkpoint's `slots` record by hand to undo a move. In the same board
|
|
221
255
|
session the next tick enrolls once the other session lets the slot go (a live holder is waited for) and takes the work
|
|
222
|
-
left `lapsed` back with a transfer. A restart or reconnect before that, with accepted work
|
|
256
|
+
left `lapsed` back with a transfer. A restart or reconnect before that, with accepted work waiting for the slot, takes it
|
|
223
257
|
back at once by `com.responder_replace`, as a restart did before 0.53.0: the operator asked for the slot back, so the
|
|
224
258
|
other session is replaced. In recovery, a refusal that leaves no slot held reconnects; one that leaves some held keeps
|
|
225
|
-
the session and serves them. The kit gives it no route of its own
|
|
226
|
-
FAA-157): a
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`
|
|
230
|
-
|
|
259
|
+
the session and serves them. The kit gives it no route of its own. The reference service (`sample_service.py`) offers
|
|
260
|
+
it on its dashboard (FAA-157): a moved slot shows a Take back button that posts `POST /api/board/take-back` from the
|
|
261
|
+
operator's signed-in session with the `X-Fabric-Request` header; the service token alone is refused. Since 0.56.0 the
|
|
262
|
+
button has its tool, `board.take-back` with `{capability}`, declared under `extensions/actions/0.1` with gate
|
|
263
|
+
`relayed` and target `capability`, and a capability of the manifest like every tool the well-known lists
|
|
264
|
+
(`interop.well-known-capabilities`): an agent whose MCP client sets `_meta` calls it over `/mcp` with the operator's
|
|
265
|
+
decision in `_meta.decision` (a call without one is `operator-decision-required`); Claude Code and Codex send
|
|
266
|
+
arguments only, so until CT-125 they hand the operator the page as Fabric Dashboards' `open_link`. Either door records
|
|
267
|
+
the take-back as a `board.slot.returned` event (info, the capability as its subject) whose sentence says the route in
|
|
268
|
+
words and whose `data` holds `by`, `via` and the `caller` (none when the feed refuses the caller), never the decision's words; a repeat after a lost answer is answered `{capability, moved: false}`. A service built on the kit declares it as an operator action, a button with its tool ([Every button has a
|
|
269
|
+
tool](../SKILL.md), `fabric-actions/0.1`) with gate `relayed` (the operator's decision), never `none`, so a call
|
|
270
|
+
without the operator's relayed decision is refused; on a server that reads mandates (`agent_id`) it also names its effect class, for example
|
|
271
|
+
`change-policy` (DEC-0125); the operator reaches it through an agent that relays their decision in `_meta`, the
|
|
272
|
+
service's page or its elicitation (Claude Code and Codex, which cannot set `_meta`, wait for CT-125's forwarding), and the service names it in its docs; otherwise a moved slot stays moved. A move back to this service's own session is not
|
|
231
273
|
detected yet: the receiving side and the hand-over of accepted requests (contract CT-175) wait for Fabric's session
|
|
232
274
|
surface.
|
|
233
275
|
|
|
@@ -251,11 +293,19 @@ raises `directory-invalid` rather than answering; an unlisted Project may still
|
|
|
251
293
|
|
|
252
294
|
## Events, the probe and the live smoke
|
|
253
295
|
|
|
254
|
-
Events go to `emit(kind, level, text, notify=…, data=…)` (`JsonlEventLog.append` fits
|
|
296
|
+
Events go to `emit(kind, level, text, notify=…, subject=…, data=…)` (`JsonlEventLog.append` fits: every level is one
|
|
297
|
+
of its `info`, `notice`, `warning`, `error`; `subject` is `{type, id}`, the notice's request, else its capability, else
|
|
298
|
+
its episode (such as a block's cause), else the Project; in Python it goes only to an `emit` whose signature names a `subject`
|
|
299
|
+
parameter with no value bound, as `JsonlEventLog.append` does, and any other `emit`, one with only `**kwargs`, one with
|
|
300
|
+
a subject bound by `functools.partial` (which then stands) or one whose signature cannot be read, is called once, without
|
|
301
|
+
one, as before 0.56.0; the Node responder always passes it in its options, which an `emit` that does not read it ignores) with
|
|
302
|
+
`data {schema, value}`
|
|
255
303
|
checked by FAC-SEM-101 (no credential in data): `board.request.completed`, `board.request.refused`,
|
|
256
304
|
`board.request.skipped`, `board.request.reconciled`, `board.request.held`, `board.attempt.lost`,
|
|
257
|
-
`board.reconcile.needed`, `board.slot.contested`, `board.slot.moved`, `board.access.needed`, `board.blocked`,
|
|
258
|
-
`board.down`, each notifying episode said once.
|
|
305
|
+
`board.reconcile.needed`, `board.slot.contested`, `board.slot.moved`, `board.slot.refused`, `board.access.needed`, `board.blocked`,
|
|
306
|
+
`board.down`, each notifying episode said once, and `board.outcome.invalid` (warning) when a handler's outcome fails
|
|
307
|
+
its checks and is not sent. The operator's hold (below) adds `board.held`, `board.slot.held`,
|
|
308
|
+
`board.slot.released` and `board.request.waiting`. The reference service adds `board.slot.returned` at a take-back.
|
|
259
309
|
|
|
260
310
|
`check_service.py <id> [--board-kit <the service's fabric_board.py or fabric-board.mjs>]` rule **board.responder**:
|
|
261
311
|
`NOT_RUN` when the well-known `protocols` names no `fabric-project-comms` revision; `FAIL` for a revision this kit does
|
|
@@ -45,7 +45,12 @@ const answer = await request('GET', 'https://api.example/v1/items', { headers, d
|
|
|
45
45
|
|
|
46
46
|
Every answer comes back as a `Response`, whatever its status; `HttpError` means no usable answer came, with `kind`
|
|
47
47
|
`connection`, `timeout`, `too-large` or `refused`, the status when there was one, and the URL redacted to scheme,
|
|
48
|
-
host, port and path (`redact_url`)
|
|
48
|
+
host, port and path (`redact_url`); a URL that is not http or https keeps only its scheme (`htps:<redacted>`), and
|
|
49
|
+
one that does not parse as a URL is `<unparseable url>`. The path is not redacted (Node writes it as `new URL`
|
|
50
|
+
normalises it, Python as given), so a key never belongs in it. Python's `check_url` refuses a host
|
|
51
|
+
with a backslash, space, control character (C0, DEL or C1) or `%`, a URL with a space or ASCII control character before its
|
|
52
|
+
first `#` (the default transport sends a URL up to there, as Node's does; a transport you pass receives the URL as given), or a character outside ASCII in its path or query (ASCII whitespace at its ends is trimmed, as urllib and `new URL` trim it; a Unicode space ending the part before the first `#` is refused, where Node keeps it percent-encoded), a bracketed host that is not an IPv6 address with no zone, and a port that is not
|
|
53
|
+
ASCII digits, all as kind `refused` before sending; it checks a host's shape, not its name. `redact_url` on Python reads a host holding `%` as `<unparseable url>`; Node writes it decoded. An error never carries a request header or body.
|
|
49
54
|
|
|
50
55
|
## fabric_openrouter
|
|
51
56
|
|
|
@@ -77,6 +82,7 @@ const answer = await client.chat(messages, { model, fallback, maxTokens: 800 });
|
|
|
77
82
|
| The fallback | required, of another vendor (the part before `/` differs) |
|
|
78
83
|
| Content | text only: a string, null, or parts that are all `{"type": "text"}`; an image, audio or file part is refused (`config`), because what it is billed cannot be bounded from its bytes |
|
|
79
84
|
| What the ceiling does not cover | the `provider` routing param can send a call to a provider priced above the catalog; two `Client` objects sharing one ledger each hold only their own calls in flight, so give an agent one client per key |
|
|
85
|
+
| The base URL | `base_url` (`baseUrl`), by default `https://openrouter.ai/api/v1`: checked by `check_url` when the client is built; ASCII whitespace and trailing `/` at its ends are trimmed (Node also trims every C0 control at the ends and removes a tab or line break anywhere, as `new URL` reads them); a base that is not a string is refused as `check_url` refuses it (`HttpError` kind `refused`, both twins); a query or a fragment is refused (kind `config`, both twins, before `check_url`). Python also refuses a space, a control or a non-ASCII character in the path (`check_url`'s `HttpError` kind `refused`); Node sends it percent-encoded, as `new URL` writes it (FAA-187 (18)) |
|
|
80
86
|
| Attribution | off unless an `attribution` setting is given (FAC-SEM-098, `fabric_usage.attribution_headers`) |
|
|
81
87
|
| Request body | `model`, `messages`, `max_tokens`, `usage: {include: true}` and your params (temperature, reasoning, response_format, tools); `stream`, `model`, `messages`, `max_tokens` and `usage` as params are refused |
|
|
82
88
|
|
|
@@ -124,6 +124,15 @@ page as Fabric Dashboards' `open_link`; both kits carry the same sentence. The g
|
|
|
124
124
|
`JwksCache` holds the estate's key set at most 10 minutes and, on a `kid` it does not hold, re-reads at most once
|
|
125
125
|
a minute (DL-15), and waits 10 s after a failed read before trying again; after 10 minutes without a successful read it answers nothing, and the verifier says
|
|
126
126
|
`issuer-unavailable`. It reads https, http on loopback only, or a file; a URL with credentials is refused. The
|
|
127
|
+
Python kit also refuses, https included, a URL holding a space or ASCII control character before its first `#` (it
|
|
128
|
+
reads the URL up to there, as Node does), or a character outside ASCII in its path or query (urllib would send it
|
|
129
|
+
and every read fail; a trailing ASCII space or line break is trimmed as urllib reads it; a Unicode space ending the
|
|
130
|
+
part before the first `#` is refused, as `check_url` refuses it, because Node keeps it percent-encoded (urllib would
|
|
131
|
+
trim it and read the keys when it ends the URL); a leading space is refused as not a URL), a host holding `%`, a backslash, a Unicode space or a control character (urllib
|
|
132
|
+
would decode or dial it as another host), a bracketed host that is no IPv6 address, a port that is not ASCII digits or is above
|
|
133
|
+
65535, no host, and an empty userinfo. The Node kit accepts several of these as `new URL` reads them, among them a `%`
|
|
134
|
+
host decoded, a backslash as `/`, a tab or line break removed, an empty userinfo dropped and an extra slash before the
|
|
135
|
+
host skipped. The
|
|
127
136
|
Python kit verifies Ed25519 itself (RFC 8032, standard library only, strict as OpenSSL: `S` below the group order,
|
|
128
137
|
canonical points, no small-order key); it follows no redirect when it reads the key set; the Node kit uses `node:crypto` and refuses small-order keys
|
|
129
138
|
itself (`isSmallOrderKey`), because Node before 26 lets OpenSSL accept a signature under one.
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md
CHANGED
|
@@ -149,7 +149,8 @@ operator's own coding agents: Claude Code and Codex cannot set `_meta`, so until
|
|
|
149
149
|
a `relayed` gate turns "approve it" in chat into `operator-decision-required` with the page as the only route.
|
|
150
150
|
Until then a service keeps its tools as they answer today, or gates a `relayed` tool only where it offers its own
|
|
151
151
|
elicitation (the `elicited` hook). The one exception is the board responder's take-back action: it is declared
|
|
152
|
-
`relayed` now
|
|
152
|
+
`relayed` now (the reference service's `board.take-back`); an agent whose MCP client sets `_meta` calls it with the
|
|
153
|
+
operator's decision, and Claude Code and Codex reach it through the page until CT-125 (`board.md`). This is D8's reason, which keeps `input_actions` off, applied to tools.
|
|
153
154
|
|
|
154
155
|
**Parity (AP-1).** Every action the dashboard, app or page lets a person take (approve, reject, choose, edit, pause,
|
|
155
156
|
resume, cancel, retry, send, publish, restore, set a policy, any other) has a tool on the MCP surface with the same
|
|
@@ -201,8 +202,11 @@ well_known = fs.build_well_known(..., surfaces=surfaces, actions=server.action_t
|
|
|
201
202
|
`caller-missing`: an action-gated server must pass `tier`, `identify` or `authorize`,
|
|
202
203
|
since `handle` defaults to T2 and a T2 call names nobody without them). One fault reaches the caller by name:
|
|
203
204
|
`mandate-capability-missing` (DEC-0141 MJ-1), read as `operator-decision-required` with that `data.detail`.
|
|
204
|
-
`on_fault` hears it
|
|
205
|
-
|
|
205
|
+
`on_fault` hears it under the job's capability (the name the job was created under) both from `ctx.approval_step`
|
|
206
|
+
at a job's start, where the caller sees no refusal: the job waits `input_required`, its message naming the fault,
|
|
207
|
+
and at an answer through `fabric.job.get`, where the call is refused; at the start it hears any fault `job_step`
|
|
208
|
+
answers (also `now-unreadable` from the server's clock). A capability name is what `^[a-z][a-z0-9._-]{1,127}$`
|
|
209
|
+
matches (`reviews.approve`, never `reviewsApprove`); a job created under another name is read under that name.
|
|
206
210
|
- **Arguments with no canonical form** (a lone surrogate, a NaN or an infinity that Python's `json.loads` lets in,
|
|
207
211
|
nesting past the recursion limit) have no digest to bind: a relayed tool answers JSON-RPC `-32602`, never an exception.
|
|
208
212
|
- **The handler** reads `ctx.record` (`"caller"`, or `{by, via, caller}`) and writes it into the service's audit
|
|
@@ -305,7 +309,10 @@ server.add_tool(approve_tool, approve_reply, action=reply_action, target="reply_
|
|
|
305
309
|
step is kept in the job's private part (`request_input(..., steps=)`). An answer's `_meta.mandate` that covers that
|
|
306
310
|
step is then read when `input_actions=True`, beside a decision for the key. The rule alone is `job_step(mandate, ...)`
|
|
307
311
|
/ `jobStep({mandate, tier, now, self, capability, key, effect, callerAgent, spendUsd, project})`; Node's
|
|
308
|
-
`requestInput(jobId, inputRequests, statusMessage, {steps})` keeps the same private part.
|
|
312
|
+
`requestInput(jobId, inputRequests, statusMessage, {steps})` keeps the same private part. Node has no
|
|
313
|
+
`approvalStep`, so a Node service calls `jobStep` itself and passes `{}` when the start call's `_meta` carried
|
|
314
|
+
`mandate: null` (`Object.hasOwn(meta, 'mandate') && meta.mandate === null ? {} : meta.mandate`): `null` is no mandate
|
|
315
|
+
to `jobStep`, while the answer reads it as an unreadable one (FAA-178). Each `request_input`
|
|
309
316
|
replaces the kept steps, and an answer clears them: pass `steps=` again on every question whose answer a mandate may
|
|
310
317
|
cover. The answer's mandate is read under the job's own capability, as at the start (`action_gate(...,
|
|
311
318
|
mandate_capability=)` / `actionGate({mandateCapability})`), so the start and the answer agree whatever the tool is
|
|
@@ -328,7 +335,9 @@ lead, at, expiresAt})` (a day unless told, at most seven), hands it to the agent
|
|
|
328
335
|
`hand_on(mandate, callee, at, narrower=None)` / `handOn(mandate, callee, at, narrower)` (the chain gains the callee; a
|
|
329
336
|
wider scope, a fifth agent, an agent already in it or a time at the expiry is refused; `source` travels unchanged), and
|
|
330
337
|
sends it with `request_meta(traceparent, mandate=...)` / `requestMeta(traceparent, {mandate})`, which refuse one
|
|
331
|
-
`FAC-SEM-144` refuses. `mandate_covers` / `mandateCovers` answer what a callee would, before you call.
|
|
338
|
+
`FAC-SEM-144` refuses. `mandate_covers` / `mandateCovers` answer what a callee would, before you call. To cover a
|
|
339
|
+
job's approval steps, name the job's capability in `capabilities` (or leave `capabilities` out) and the steps' keys in
|
|
340
|
+
`actions`; never `fabric.job.get`, which covers no step of another capability (DEC-0141 MJ-1). Never write a
|
|
332
341
|
mandate from another agent's text, a page or a file; a callee hands on only what it received, narrowed.
|
|
333
342
|
|
|
334
343
|
A relay the operator's rule accepts (DEC-0126 OA-2, OA-3: one of his own Projects on his board, provenance `trusted`,
|
|
@@ -87,6 +87,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
87
87
|
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
88
88
|
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
89
89
|
| L4 | `fabric_board_link` | the board's link: the MCP session over the hub, the fsynced checkpoint, a claimed request as data |
|
|
90
|
+
| L4 | `fabric_board_slots` | the responder's slot records: the generation held, a move and its take-back, one capability's refusal retried alone (FAA-163) |
|
|
90
91
|
| L4 | `fabric_board_session` | the board responder: the work context, the fenced request, transfer and recovery |
|
|
91
92
|
| L4 | `fabric_board_fixture` | the scripted board the probe runs a responder against |
|
|
92
93
|
| L4 | `fabric_board` | the facade of the board connector ([board](board.md)) |
|
|
@@ -95,7 +96,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
95
96
|
|
|
96
97
|
Beside the kit, not in `kit.json`: the probe (`check_service.py`, `check_service_rules.py`), the sample
|
|
97
98
|
(`sample_service.py`, `sample-remote-service.mjs`), `fabric_kit.py` itself and the dashboard tools. Recipes built on
|
|
98
|
-
the kit live in `../recipes/` ([the support agent](../recipes/support-agent/README.md)).
|
|
99
|
+
the kit live in `../recipes/` ([the draft-and-check step of a support agent](../recipes/support-agent/README.md)).
|
|
99
100
|
|
|
100
101
|
## What 0.39 code keeps
|
|
101
102
|
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -55,7 +55,7 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
|
|
|
55
55
|
|
|
56
56
|
| Id | Rule | Here | Kit (Python / Node) |
|
|
57
57
|
|---|---|---|---|
|
|
58
|
-
| LC-01 | **Quit reaches exit within a deadline.** Every trigger (`SIGTERM`, `SIGINT`, logout, the app's own quit) ends the process: within 5 s idle, within 10 s with work in flight, then a hard exit (
|
|
58
|
+
| LC-01 | **Quit reaches exit within a deadline.** Every trigger (`SIGTERM`, `SIGINT`, logout, the app's own quit) ends the process: within 5 s idle, within 10 s with work in flight, then a hard exit (an idle board responder waits 3 s at most for its session's DELETE, FAA-173; one with work can take up to that hard exit). Nothing starts after quit begins | [Shutdown](#shutdown) | `Drain` / `Drain` |
|
|
59
59
|
| LC-02 | **Every child has an owner, a deadline and a process group.** No subprocess without a timeout; on timeout or the parent's exit the whole group goes; no orphan survives. Locks and shared resources are released in `finally` | your own code | none |
|
|
60
60
|
| LC-03 | **A job is bounded and exclusive.** One copy holds `service.lock`; a scheduled job has a watchdog shorter than its interval, shares its lock with every manual entry point, bounds every network call and records each run's outcome. Long work is journalled before it starts | [One copy under a supervisor](#one-copy-under-a-supervisor) | `hold_single_instance` / `holdSingleInstance` |
|
|
61
61
|
| LC-04 | **Credentials are silent after setup.** No background prompt; a credential read that is denied or absent is cached and backs off until a person acts | [Directories](#directories) (secret files 0600) | none |
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md
CHANGED
|
@@ -162,7 +162,10 @@ path on the service origin, never an absolute URL. Optional SSE stream at
|
|
|
162
162
|
**Machine-readable `data` (DEC-0070).** An event may carry `data {schema, value}` beside its sentence: `schema` is the
|
|
163
163
|
absolute URI of a payload schema the service publishes; `value` is an object of at most 32 members, each a scalar (a
|
|
164
164
|
string of at most 500 characters, a number, a boolean, null), a list of at most 32 scalars, or an object of at most 32
|
|
165
|
-
scalar members
|
|
165
|
+
scalar members: two levels at most, so `value.cited` may be a list of ids or an object of counts, never a list of
|
|
166
|
+
objects (FAA-122). Flatten a nested record into members of its own, `{"citedIds": ["a1", "b2"], "citedCoverage":
|
|
167
|
+
[0.8, 0.4]}` in place of `{"cited": [{"id": "a1", "coverage": 0.8}, ...]}`; the refusal names the first place that
|
|
168
|
+
nests too deep (`data.value.cited[0] is an object`). `text` stays the event: a host that ignores `data` loses nothing, and one that does not know the schema
|
|
166
169
|
treats `value` as opaque. No string in `value` sits under a credential's name or has a credential's shape
|
|
167
170
|
(`FAC-SEM-101`); personal data is the service's own duty. `make_event` / `makeEvent` (and `JsonlEventLog.append`) take
|
|
168
171
|
`data=` and refuse one that breaks either rule, naming the place, never the value; `check_service.py` checks the feed it
|
|
@@ -10,7 +10,7 @@ import { BoardError, RESPOND_TOOLS, isObj, refusalProblems } from './fabric-boar
|
|
|
10
10
|
/** MCP over HTTP with a board session's one-session bearer; calls are serialised; close() sends the session's DELETE. */
|
|
11
11
|
export class BoardSession {
|
|
12
12
|
constructor(grant, { timeout = 15000, post = postMcp } = {}) {
|
|
13
|
-
Object.assign(this, { grant, timeout, post });
|
|
13
|
+
Object.assign(this, { grant, timeout: timeout > 0 ? timeout : 15000, post }); // not above 0 is the default
|
|
14
14
|
this.mcpSession = null;
|
|
15
15
|
this.protocolVersion = MCP_PROTOCOL_VERSION;
|
|
16
16
|
this.rpc = 0;
|
|
@@ -23,9 +23,9 @@ export class BoardSession {
|
|
|
23
23
|
return run;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
async #send(body, method = 'POST') {
|
|
26
|
+
async #send(body, method = 'POST', timeout = null) {
|
|
27
27
|
try {
|
|
28
|
-
return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined });
|
|
28
|
+
return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: timeout > 0 ? timeout : this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined });
|
|
29
29
|
} catch (e) {
|
|
30
30
|
if (e instanceof HubError) throw new BoardError('unreachable', 'Fabric\'s hub did not answer; a write may have been stored, so send it again with the same idempotency key.', true);
|
|
31
31
|
throw e;
|
|
@@ -66,15 +66,19 @@ export class BoardSession {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/** Sends the DELETE (waiting `timeout` ms at most); true when Fabric answered it with a 2xx, so the slots are free now (FAA-163). A 404 (already gone) is false: a move made before it ended is read at the next com.status, so the slot records stay. */
|
|
70
|
+
close(timeout = null) {
|
|
70
71
|
return this.#serial(async () => {
|
|
71
|
-
if (!this.mcpSession) return;
|
|
72
|
+
if (!this.mcpSession) return false;
|
|
73
|
+
let status = 0;
|
|
72
74
|
try {
|
|
73
|
-
await this.#send({}, 'DELETE');
|
|
75
|
+
const answer = await this.#send({}, 'DELETE', timeout);
|
|
76
|
+
status = Number.isInteger(answer?.status) ? answer.status : 0; // an answer it cannot read is no release, as in Python
|
|
74
77
|
} catch {
|
|
75
78
|
// best effort: the lease lapses in 60 s anyway
|
|
76
79
|
}
|
|
77
80
|
this.mcpSession = null;
|
|
81
|
+
return status >= 200 && status < 300;
|
|
78
82
|
});
|
|
79
83
|
}
|
|
80
84
|
}
|
|
@@ -135,14 +139,17 @@ export class Checkpoint {
|
|
|
135
139
|
this.slots = slots;
|
|
136
140
|
}
|
|
137
141
|
/**
|
|
138
|
-
* At a clean leave (the session's DELETE released the slots): forget the record of each slot this
|
|
139
|
-
* `project
|
|
142
|
+
* At a clean leave (the session's DELETE released the slots): forget the record of each slot this session confirmed
|
|
143
|
+
* (`held`, required: capability to the generation it held, or com.status showed unmoved) at that generation or below, for `project`, with no request left and not moved away, so a later generation is another session taking a free slot, and
|
|
140
144
|
* a kit rolled back to before 0.53.0 leaves nothing that reads as a move. A record that cannot be written stays.
|
|
141
145
|
*/
|
|
142
|
-
leave(project) {
|
|
146
|
+
leave(project, held) {
|
|
143
147
|
const busy = new Set(Object.values(this.entries).map((e) => e.capability));
|
|
144
148
|
for (const [capability, record] of Object.entries(this.slots)) {
|
|
145
149
|
if (!isObj(record) || record.project !== project || 'moved' in record || busy.has(capability)) continue;
|
|
150
|
+
// Only a slot this session confirmed, at or below that generation: an unconfirmed record may be a move (0.56.0 seam r2, r3).
|
|
151
|
+
const confirmed = Object.hasOwn(held, capability) ? held[capability] : null;
|
|
152
|
+
if (!Number.isInteger(record.generation) || record.generation < 1 || !Number.isInteger(confirmed) || record.generation > confirmed) continue;
|
|
146
153
|
try {
|
|
147
154
|
this.slot(capability, null);
|
|
148
155
|
} catch {
|