@passioncode-ai/fabric-agent-adapter 0.52.0 → 0.54.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 (35) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +8 -4
  3. package/SECURITY.md +18 -3
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +82 -11
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +2 -1
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +24 -8
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +2 -2
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +22 -9
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +33 -3
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +146 -7
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +6 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +14 -6
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +23 -13
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +34 -3
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +149 -45
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +6 -1
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +9 -5
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +16 -5
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +276 -12
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +13 -1
  28. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  29. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +20 -13
  30. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +4 -1
  31. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +38 -9
  32. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +11 -0
  33. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +12 -3
  34. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +11 -1
  35. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +358 -14
package/CHANGELOG.md CHANGED
@@ -1,5 +1,123 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.54.0 (2026-10-27)
4
+
5
+ ### Changed
6
+
7
+ - **Contract pin `00e050ba` (`v0.1.0+2026-10-27.1`, DEC-0141)** (FAA-159, CT-149, CT-177). A job's step under the
8
+ operator's mandate is read as its start was: under the job's own capability, its action the step's request key
9
+ (MJ-1). The kits' `action_decision` / `actionDecision` now read the reference's way: for an action whose tool is
10
+ `fabric.job.get` the mandate is read under `mandateCapability`, and without a usable one (absent, empty, blank, not a
11
+ capability name, or `fabric.job.get` itself) the answer is the service's fault `mandate-capability-missing`; any
12
+ other action reads its own tool, whatever `mandateCapability` says. Before, an absent capability was read as
13
+ `fabric.job.get`, a non-name string as given, and `mandateCapability` was read on any action. `action_gate` /
14
+ `actionGate` answer that fault to the caller as `operator-decision-required` (`data.detail`
15
+ `mandate-capability-missing`), the fault beside it for the service's log; nothing is applied, so the job stays
16
+ `input_required`. `job_step` / `jobStep` read an unusable job capability alike at the start: the step waits
17
+ `input_required`, its message naming `mandate-capability-missing`, and the Python tool server hands that fault (and any
18
+ other `job_step` answers, such as `now-unreadable`) to `on_fault` at the start as at the answer. It is the one service fault a caller reads by name: `on_fault`'s and the
19
+ delegation reference's rule (a caller sees a fault only as `unknown-state`) now names it. So a job whose tool is not a capability name is no
20
+ longer covered by an `actions`-only mandate at either end, and a mandate whose `capabilities` name `fabric.job.get`
21
+ covers no step of another capability. Any service gating a job's answer itself passes the job's capability
22
+ (`actionGate({mandateCapability: private.capability})` in Node, `action_gate(..., mandate_capability=)` in Python
23
+ outside `McpToolServer`, which passes it already); without it every answer a mandate used to cover now waits for the
24
+ operator. `refusal_from_action` / `refusalFromAction` answer the fault the same way, so `action_refusal_tool_result` /
25
+ `actionRefusalToolResult` agree with the gate. The vendored `actions-vectors.json` gains the contract's `jobSteps` group (14
26
+ cases) and its two new `action-call` fixtures (18 in all); both twins run them. Bundles issued under the previous pin
27
+ `1197a90d` stay valid (`adapt_project.py`).
28
+ - **working-with-fabric:** a leading agent covering a job's approval step names the job's capability in
29
+ `capabilities` without `actions` (the step's request key is the service's own), and reads `data.detail`
30
+ `mandate-capability-missing` as a step no mandate covers: only the operator's decision answers it. New scenario eval
31
+ `lead-covers-a-job-step-by-its-capability`. building-fabric-services' interop reference drops the sentence that
32
+ read an answer without a capability under `fabric.job.get`.
33
+
34
+ ## 0.53.1 (2026-10-27)
35
+
36
+ 0.53.0 was tagged and never released on npm or GitHub (the plugin marketplace, which reads `main`, offered it until
37
+ 0.53.1 merged): its release run failed on Windows, where the FAA-140 tests read a Bash
38
+ command's POSIX paths through Windows path rules. 0.53.1 is 0.53.0 with this fix.
39
+
40
+ ### Fixed
41
+
42
+ - **No transcript read on Windows** (FAA-175): working-with-fabric's Stop hook never reads the session transcript on
43
+ Windows, even once Fabric takes `repositories`; a Bash command's paths there are not the host's. Windows sessions send
44
+ their turn facts as before 0.53.0, without `repositories`. `BashPaths` and the four tests that need the hook itself to
45
+ read (names from Edit and Bash paths, at most ten, one checkout named once, another checkout of the start repository)
46
+ are skipped on Windows; the rest run there, the worker's with the read turned on in the test, and a new test checks that
47
+ the mark turns nothing on there.
48
+
49
+ ## 0.53.0 (2026-10-27, not published)
50
+
51
+ ### Added
52
+
53
+ - **The repositories a turn touched** (FAA-140, Fabric CO-367): working-with-fabric's Stop hook names the other Git
54
+ repositories a turn touched, once the running Fabric takes them. Nothing changes for a Fabric before that: the hook
55
+ reads no transcript until a worker has seen `fabric_session_observe` declare `repositories` as an array of items with
56
+ `host`, `owner` and `name` that require nothing else (it then leaves a mark in the hooks' folder, and removes it when
57
+ the schema stops saying so; after a Fabric downgrade, the turns that end before a worker next runs, about a minute, are still read; none of their
58
+ names is sent). With the mark, the
59
+ hook reads the last 512 KB of the session's transcript (earlier turns, message text and tool output included; a secret the session displayed can be there, read in memory only) and uses only the
60
+ current turn's tool names and path arguments: Edit, Write, MultiEdit and NotebookEdit paths, and `cd
61
+ <absolute path>` and `git -C <path>` in Bash commands that hold `cd` or `git`, at most 64 KB of them a turn. A `cd`
62
+ or a read-only `git -C <path> log` names a repository as much as an edit does; a subagent's tool calls are not
63
+ read. Each repository is resolved with one `git rev-parse --show-toplevel` and one `git config` inside the existing
64
+ 1.5 s facts budget, and named `{host, owner, name}` from its `origin` (a credential embedded in the URL is read with it by `git config`; it, the port
65
+ and `.git` are dropped in memory, never spooled, logged or sent). The
66
+ repository of the session's folder when the turn ends (the Stop payload's `cwd`; any checkout of it), a repository without a parsable origin and duplicates are left out;
67
+ at most 10 (or the schema's `maxItems`), and an item whose host, owner or name the schema refuses (type, length,
68
+ pattern, enum) is left out, never the turn; no local path leaves the hook (on a self-hosted ssh remote `owner` is the
69
+ server's folder path). The worker sends them as `repositories` on `fabric_session_observe`. With a Fabric that never
70
+ took them nothing is read and nothing is logged; a turn spooled with the mark and delivered to a Fabric that no
71
+ longer takes them goes without them, and the log says so once per session, naming no repository. Measured with the read on, at `f3765f1` (before the gate and the 64 KB cap): the Stop hook's median is 247 ms on a 4.8 MB transcript naming
72
+ six repositories (0.52.0: 109 ms).
73
+ - **A board slot the operator moves to another session stays moved** (FAA-155; contract DEC-0140, proposed; CT-174,
74
+ CT-175). The kit's board responder records the generation it holds each slot at, beside its checkpoint: when a
75
+ receipt's generation changes and before an enroll or replace that can change it; a live holder's renew writes
76
+ nothing, and a clean leave (the session's DELETE released the slots) forgets the record of a slot not moved and with no request
77
+ left. When `com.status` (at connect, or after a call refused `fenced`) shows a later generation, it stops the
78
+ handler, drops its claims, and leaves accepted work held (`lapsed`) and begun work `unknown`; it neither replaces at
79
+ recovery after a restart nor enrolls again when the new holder's lease lapses. Accepted requests stay held on the
80
+ board, unanswered, until the slot is taken back (CT-175 will hand them to the new holder). One `board.slot.moved`
81
+ event (notify) says so, once across restarts. A first start, an equal generation, or an odd or lower generation
82
+ behaves as before. An enroll or replace is recorded with its key before it is sent, so a lost answer to the
83
+ responder's own call is asked again under that key and never read as a move; a replay the board runs for the first
84
+ time (`repeated: false`) after the slot had moved past the record is read as that move, and the slot it took lapses
85
+ unused; a replay refused `idempotency_window_expired` (a retired epoch) forgets the record, so the slot reads as on a
86
+ first start. A recorded move stays even when `com.status` later names no generation for the slot. The record is best
87
+ effort: one that cannot be written (a full disk) never stops a call or the responder,
88
+ and a restart before the next one is recorded may then read as a move. A holder's renew within its live lease writes
89
+ nothing; the reference service now waits at its stop for the responder to leave its board session. `Responder.take_back(capability)` /
90
+ `takeBack(capability)` is the operator's act that brings a moved slot back: it enrolls once the other session lets
91
+ the slot go and takes the held work back by a transfer; a restart or reconnect before that, with work held, replaces
92
+ the other session at once. Moving the slot back to this service in Fabric does not bring it back (the responder
93
+ skips a moved slot until `take_back`; FAA-165). The kit offers no route for it: a service declares one as an
94
+ operator action with its tool and gate `relayed` (the operator's decision; never `none`; on a server that reads
95
+ mandates it also names its effect class) (`fabric-actions/0.1`; the reference service's is FAA-157). The
96
+ notice says begun work is the new holder's to settle and that the take-back action brings the slot back. Both
97
+ twins.
98
+
99
+ ### Changed
100
+
101
+ - **Step 4 reads Fabric's `spawned_from`** (FAA-156, Fabric 0.3.14): working-with-fabric links a piece filed with
102
+ `fabric_task_create` itself only when the answer says `spawned_from: null` without `proposed: true` (once; a refused link is told to the person
103
+ with `spawned_from_why`), or is filed (`created: true`) with no `spawned_from` (Fabric before 0.3.14) and no `chained: true` with `chained_to`
104
+ the claimed task. `created: false` with a `code` such as `source-unresolved` filed nothing and is never linked. A piece whose `spawned_from` names the claimed task is already linked; one naming another task is
105
+ not linked again, and the person is told which parent Fabric chose (FAA-153). `proposed: true` now says what
106
+ to do: file and link nothing for that piece, tell the person, go on with the rest. The goal display is named as
107
+ Fabric 0.3.14's. New scenario evals `spawned-from-decides-the-link`, `what-the-stop-hook-reads` (FAA-140) and
108
+ `moved-slot-needs-a-take-back-action` (FAA-155).
109
+ - **A responder no longer takes back a slot that moved while it was away** (FAA-155). Before 0.53.0 a restarted
110
+ responder replaced whoever held its slot (at recovery) or enrolled again when that holder lapsed, so a slot taken
111
+ while it was down or after a crash came back by itself. It now stays with the new holder until the operator calls
112
+ `take_back`; a second responder of the same capability is no longer a standby once the slot changed hands. A
113
+ service stopped cleanly with no request left in its checkpoint, rolled back to 0.52 and up again starts as on a first start (the clean leave
114
+ forgot the record); after a crash, a 0.52 run that enrolled the slot and wrote nothing to the checkpoint reads as a move at the next 0.53 start, which
115
+ the take-back action undoes. A `fenced` refusal on accepted work while the slot did not move now keeps the entry `lapsed`
116
+ and the next tick transfers it back and finishes it (0.52 dropped the entry and left the request accepted on the
117
+ board). A refusal raised while the responder recovers at its first connect, before any slot is held, now reconnects
118
+ instead of leaving it `ready` with no slot; one after some slots are held keeps them served (a reconnect still
119
+ counts the previous session's slots as held: FAA-163). A generation written `2.0` now reads as `2` in the Python twin as it always did in Node.
120
+
3
121
  ## 0.52.0 (2026-10-26)
4
122
 
5
123
  ### Changed
package/README.md CHANGED
@@ -38,9 +38,12 @@ resume) puts that preamble into the session when the MCP server `fabric` is regi
38
38
  uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
39
39
  is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
40
40
  project that uses Fabric; Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
41
- and the board's unread count; UserPromptSubmit adds one line only when that count rose, and each reminder Fabric gave on a turn once (a turn `watch-mail` delivered prints there instead); SessionEnd releases what the
41
+ and the board's unread count, and, once the running Fabric takes them (not on Windows), the other repositories the turn touched (it then
42
+ reads the last 512 KB of the session transcript (message text and tool output included, which can hold a secret the session displayed; read in memory only), uses only tool names and path arguments, and sends host/owner/name, never a
43
+ local path or text); UserPromptSubmit adds one line only when that count rose, and each reminder Fabric gave on a turn once (a turn `watch-mail` delivered prints there instead); SessionEnd releases what the
42
44
  session left claimed. Stop, UserPromptSubmit and SessionEnd spool and call Fabric only when the running Fabric
43
- advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched, never block the session, read no token and always
45
+ advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched, never block the session, read no Fabric token (an `origin` URL's embedded credential is dropped in memory, never
46
+ kept or sent) and always
44
47
  exit 0. `fabric_session.py watch-mail` waits for board mail under a background watcher, and `install-host-hooks
45
48
  --host codex|kimi|hermes|opencode|openclaw|pi` installs each host's native context bridge (dry run unless `--apply`).
46
49
  Codex receives structured SessionStart JSON; Hermes, OpenCode, OpenClaw and Pi use native plugin APIs.
@@ -67,7 +70,8 @@ a complete `scripts/sample_service.py`; and `scripts/check_service.py`, a live p
67
70
  requests other Projects post to its Project on Fabric's project board uses the board responder
68
71
  (`scripts/fabric_hub.py` and `scripts/fabric_board.py`, Node `fabric-hub.mjs` and `fabric-board.mjs`): the
69
72
  `board.respond` grant, enroll, claim, accept the exact digest, complete, a checkpoint before every step, and restarts
70
- that reconcile or take accepted work back by a transfer (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
73
+ that reconcile or take accepted work back by a transfer, unless the slot was moved to another session (until the
74
+ operator's take-back) (the [board reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md)):
71
75
 
72
76
  ```bash
73
77
  python3 plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py example-agent
@@ -280,7 +284,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
280
284
  The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
281
285
 
282
286
  - version: `0.1.0`
283
- - commit: `1197a90dc4b57152cabcc71ad17db7559f35a27f`
287
+ - commit: `00e050ba7429c0c60261cec871779e0fd6aeeb28`
284
288
 
285
289
  Every other mention of the default contract revision (this section, the skills' metadata, the
286
290
  skill card, `adapt_project.py`) must equal it; `python3 test/validate.py` fails a tree
package/SECURITY.md CHANGED
@@ -33,10 +33,25 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
33
33
  reads by name at run time; the kit itself calls no outside host on its own. A board
34
34
  responder talks only to Fabric's hub on loopback.
35
35
  - The working-with-fabric session hooks run as the person: Stop and SessionEnd run `git`
36
- in the session's folder (counts only, at most 1.5 s), read the process table to tell a
37
- session Fabric launched, write their spool and log under the user's cache folder (0600),
36
+ in the session's folder (counts only, at most 1.5 s; with the mark below and not on Windows, also that folder's own `origin`, only to leave its repository out), read the process table to tell a
37
+ session Fabric launched. Stop also, never on Windows and only once the running Fabric's `fabric_session_observe` takes
38
+ `repositories` (FAA-140; a worker sees it in the tool's schema and leaves a mark), reads the
39
+ last 512 KB of the session's transcript (earlier turns included; only the entries after the
40
+ last prompt the person typed are used; the tail holds message text and tool output too, file contents and
41
+ command output that can carry a secret the session displayed, read in memory only and never spooled, logged or sent)
42
+ and runs `git rev-parse --show-toplevel` and `git config --get remote.origin.url` in the
43
+ other repositories the turn's tool calls name, within the same 1.5 s. Of the transcript they
44
+ use only tool names and path arguments; what leaves the machine is each repository's
45
+ host, owner and name, never a local path, a command or message text (on a self-hosted ssh
46
+ remote, `owner` is the folder path on that server before the repository's name). The mark is
47
+ removed only by a worker that sees a schema without the field, so after Fabric is downgraded
48
+ the turns that end before a worker next runs (about a minute) are still read; none of their
49
+ names is sent. They write their spool and log under the user's cache folder (0600),
38
50
  and the delivery worker starts `fabric-mcp` in its own process group and ends that group
39
- (`killpg`, or `taskkill /T /F` on Windows) at its 10 s deadline. They read no token.
51
+ (`killpg`, or `taskkill /T /F` on Windows) at its 10 s deadline. They read no Fabric token. An
52
+ `origin` URL that embeds a credential (`https://user:token@host/...`) is read by `git config`
53
+ with it; the credential, the port and `.git` are dropped in memory before anything is spooled,
54
+ logged or sent.
40
55
  - `adapting-projects-to-fabric/scripts/adapt_project.py` reads the target project without
41
56
  executing it and writes only the seven documented bundle paths (`scaffold --dry-run`
42
57
  writes nothing); `check --contract` runs the contract checkout's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.52.0",
3
+ "version": "0.54.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.52.0",
6
+ "version": "0.54.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no token, prints at most one block, always exits 0, and hands any Fabric call to a detached worker with a 10 s deadline.",
2
+ "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count, and, once the running Fabric's fabric_session_observe takes them (never on Windows), the other repositories the turn touched (the last 512 KB of the session transcript, message text and tool output included (a secret the session displayed can be there; read in memory only), is then read and only the current turn's tool names and path arguments are used; host/owner/name leave the machine, never a local path or text) and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no Fabric token (a credential embedded in an origin URL is read by git config and dropped in memory, never spooled or sent), prints at most one block, always exits 0, and hands any Fabric call to a detached worker with a 10 s deadline.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.52.0"
8
+ version: "0.54.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "1197a90dc4b57152cabcc71ad17db7559f35a27f"
10
+ contract-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -76,7 +76,7 @@ Use exactly:
76
76
 
77
77
  - contract version `0.1.0`;
78
78
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
79
- - commit `1197a90dc4b57152cabcc71ad17db7559f35a27f`.
79
+ - commit `00e050ba7429c0c60261cec871779e0fd6aeeb28`.
80
80
 
81
81
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
82
82
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -22,10 +22,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
22
22
 
23
23
  CONTRACT_VERSION = "0.1.0"
24
24
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
25
- CONTRACT_COMMIT = "1197a90dc4b57152cabcc71ad17db7559f35a27f"
25
+ CONTRACT_COMMIT = "00e050ba7429c0c60261cec871779e0fd6aeeb28"
26
26
  # #region supported-contract-revisions — docs: README.md#contract-pin
27
27
  # Only this declaration may name a legacy revision in live files.
28
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "1197a90dc4b57152cabcc71ad17db7559f35a27f", "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
29
29
  # #endregion supported-contract-revisions
30
30
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
31
31
  MCP_REVISION = "2026-07-28"
@@ -14,10 +14,10 @@ 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.52.0"
17
+ version: "0.54.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
- extension-commit: "1197a90dc4b57152cabcc71ad17db7559f35a27f"
20
+ extension-commit: "00e050ba7429c0c60261cec871779e0fd6aeeb28"
21
21
  ---
22
22
 
23
23
  # Building Fabric services
@@ -102,7 +102,10 @@ When the service answers requests other Projects post to its Project on Fabric's
102
102
  responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) finds the hub and joins,
103
103
  [`scripts/fabric_board.py`](scripts/fabric_board.py) enrolls, claims, accepts the exact digest, checkpoints before
104
104
  every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
105
- The request body is data behind the service's own input gate. Read [the board reference](references/board.md).
105
+ The request body is data behind the service's own input gate. From 0.53.0 a slot the operator moved to another
106
+ session stays moved until `take_back` (`takeBack`): give it an operator action with its tool and gate `relayed`, or it
107
+ stays moved for good (until Fabric's hub forwards decisions, CT-125, the operator reaches it through the service's page
108
+ or its elicitation). Read [the board reference](references/board.md).
106
109
 
107
110
  ## Step 2: the non-negotiables
108
111
 
@@ -256,7 +259,8 @@ or `human` with its `humanStep`) and declared with `build_well_known(actions=ser
256
259
  `extensions/actions/0.1`. A `relayed` action called by the operator's own agent with their decision in
257
260
  `_meta.decision` is the operator's act: never answer it "operator session required" or send them to the page.
258
261
  A service adopts the relayed gate once Fabric's hub forwards decisions (CT-125), as the contract says; until then its tools
259
- answer as today. Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
262
+ answer as today, except the board responder's take-back action, declared `relayed` now and reached through the
263
+ page or elicitation until CT-125 ([board.md](references/board.md)). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
260
264
  refusals.
261
265
 
262
266
  **Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
@@ -16,6 +16,7 @@ skill's business (working-with-fabric, `references/project-board.md`).
16
16
  - Effects, observation, evidence and the reply
17
17
  - When the board says stop, and restarts
18
18
  - When the operator holds work (DEC-0079)
19
+ - When the operator moves a slot to another session (FAA-155)
19
20
  - What a board session may call
20
21
  - Looking a Project up in the directory (FAC-SEM-107)
21
22
  - Events, the probe and the live smoke
@@ -50,7 +51,11 @@ responder = fbd.Responder(
50
51
  hub=fhb.HubClient(), project_id=PROJECT, capabilities=["video.render"],
51
52
  credential_file=dirs["data"] / "board.credential", checkpoint_file=dirs["data"] / "board-checkpoint.json",
52
53
  handler=render, admit=gate, observe=look_on_disk, emit=log.append, service="example-agent")
53
- threading.Thread(target=responder.run, args=(stop_event, 5.0), daemon=True).start() # SIGTERM: set stop_event
54
+ board = threading.Thread(target=responder.run, args=(stop_event, 5.0), daemon=True) # SIGTERM: set stop_event
55
+ board.start()
56
+ # ... at stop, after stop_event is set: wait for it to leave its board session (a clean leave forgets its slot
57
+ # records, FAA-155), bounded inside the kit's hard exit, as scripts/sample_service.py does
58
+ board.join(timeout)
54
59
  ```
55
60
 
56
61
  Node: `new Responder({...same names in camelCase})`, then `responder.run({ signal, drain })`. A service that lists
@@ -63,7 +68,7 @@ One tick every 15 s ± 5 s (`step()` answers the wait):
63
68
 
64
69
  | Step | Board call | Checkpoint written first |
65
70
  |---|---|---|
66
- | hold the slot | `com.enroll` (again at about 20 s while idle: the holder's enroll renews its 60 s lease) | none |
71
+ | hold the slot | `com.enroll` (again at about 20 s while idle: the holder's enroll renews its 60 s lease) | none for a first enroll or a live holder's renew; otherwise the slot's `asking` record (FAA-155) |
67
72
  | take a request | `com.claim`, the same key while nothing is claimable | `claimed` |
68
73
  | read it | `com.get`: the digest must equal the claim's, and the request must name this Project and capability | none |
69
74
  | accept | `com.accept` with the claim's exact fence (request, generation, attempt, digest, revision) | `accepting`, then `accepted` |
@@ -73,7 +78,8 @@ One tick every 15 s ± 5 s (`step()` answers the wait):
73
78
  | settle | `com.complete {state, observed, summary?, evidence?}` | `completing`, then the entry is dropped |
74
79
 
75
80
  The checkpoint is fsynced before each call it announces. A write that fails stops the step (`board.blocked`): nothing
76
- is sent that the checkpoint does not know about. A lost answer is sent again with the same idempotency key, so the
81
+ is sent that the checkpoint does not know about. The one exception is the slot record: an `asking` or generation
82
+ record that cannot be written is skipped and the enroll or replace still goes out (see below). A lost answer is sent again with the same idempotency key, so the
77
83
  board returns the first receipt; while the lease still holds, the session and the running work stay and the call goes
78
84
  again before the lease would lapse (FAA-77). One request is in flight at a time, and one fenced call: the fence is
79
85
  built, sent and its receipt applied under one lock, so the tick's `com.renew` never carries a revision the handler's
@@ -109,10 +115,10 @@ once the kit reads it (FAA-135).
109
115
 
110
116
  | Answer | What the kit does |
111
117
  |---|---|
112
- | `fenced` | the attempt is another responder's: stop the handler, drop the entry (nothing outside had begun) |
118
+ | `fenced` | the attempt is another responder's: stop the handler, drop the entry (nothing outside had begun); accepted work on a slot this responder recorded holding stays as `lapsed`, and the next tick asks `com.status` whether the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155) |
113
119
  | `lease_expired` on accepted work | stop the handler; the board holds the request, and the kit takes it back with `com.responder_replace` and a transfer |
114
120
  | `authority_changed`, `not_authorized` | stop claiming; one `board.access.needed` event (notify) saying what to ask for |
115
- | `fenced` on enroll | another live holder: wait for it to lapse; never replace it on its own initiative |
121
+ | `fenced` on enroll or claim | another live holder: wait for it to lapse; never replace it on its own initiative. When this responder recorded holding the slot, it first reads `com.status`: a newer generation means the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155), and it is not taken again even after that holder lapses |
116
122
  | lost after an effect began | keep the entry as `unknown`; `observe(entry)` is asked to settle it with `com.reconcile` |
117
123
  | Fabric down, hub missing | bounded backoff (2 s doubling to 5 minutes, jittered), one `board.down` event, no spinning |
118
124
  | one call without an answer while the lease holds | `board.down`, but the session and the running work stay; the call goes again, with the same key, before the lease lapses |
@@ -124,7 +130,8 @@ On restart the checkpoint is read back: settled, queued, claimed or `answered` r
124
130
  the target Project's participant reply answered it, DEC-0098 and DEC-0102; not settled, the requester may still cancel
125
131
  it, but no longer this responder's); `in_progress` and `outcome_unknown` are
126
132
  reconciled from what `observe(entry)` answers (observed only; with no observation they stay unknown and notify);
127
- accepted ones go back to this responder with `com.responder_replace {expectedGeneration, transfer}`, whose receipt's
133
+ accepted ones go back to this responder with `com.responder_replace {expectedGeneration, transfer}` (unless the slot
134
+ moved to another session meanwhile: they stay `lapsed` until take-back, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)), whose receipt's
128
135
  `transferred` (DEC-0078: `{request, revision, attempt}`) carries the new fence. **A Fabric that does not answer
129
136
  `transferred` yet leaves the request held** (`board.request.held`, notify): the kit never claims it back, because the
130
137
  contract has no `com.claim` read-back and `com.claim` never moves an accepted request. A known outcome is sent after the
@@ -160,6 +167,70 @@ A receipt's request may carry `settledBy: "owner"` (DEC-0081): the estate owner
160
167
  whose responder was gone. Read it as the owner's attestation, not the responder's; it appears only on `completed` or
161
168
  `failed_known`, and its absence means the responder settled it.
162
169
 
170
+ ## When the operator moves a slot to another session (FAA-155)
171
+
172
+ The contract's DEC-0140 PR-2 (proposed, after this kit's pin; Fabric plans it for 0.3.15) lets the operator move a
173
+ (Project, capability) slot to another live session of the Project: one transition, as `com.responder_replace` would make
174
+ it for the receiving session (generation up, `claimed` back to `queued`, `in_progress` to `outcome_unknown`, `accepted`
175
+ held and not transferred). The responder keeps that move instead of undoing it:
176
+
177
+ - It records the generation it holds each slot at, in the checkpoint file beside the requests (`slots: {<capability>:
178
+ {project, generation, moved?, asking?}}`). The record is written when a receipt's generation changes and before an
179
+ enroll or a replace that can change it; a live holder's renew (about every 20 s) cannot, and writes nothing. At a
180
+ 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 (a moved slot's record stays, so a clean stop never undoes a move): the slot is free, and whoever takes it next is no move. A file without the record
182
+ 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, rolled back to 0.52 and up again starts as on a first
183
+ start. After a crash the record stays: if 0.52 then ran, enrolled the slot and
184
+ 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
+ - An enroll or a replace is written into that record with its key before it is sent (`asking`). When its answer is
186
+ lost, the next connect (or the next enroll) sends the same call again under the same key; the board answers the
187
+ stored receipt, whose generation is this service's own, and the slot is then asked for once more for the new board
188
+ session. A refusal is an answer and drops the `asking` record, so a move that happened meanwhile is still seen; except
189
+ `idempotency_window_expired` (a retired epoch refuses even a repeat), which leaves it unknown whether the call
190
+ landed: the whole record is forgotten and the slot reads as on a first start, never as a move. An
191
+ `asking` record that cannot be written (a full disk) is skipped, as the generation record is: the call goes out
192
+ unrecorded, and a lost answer to it, or a restart before the next receipt is recorded, may then read as a move.
193
+ - It reads the slot's generation from `com.status` at every connect (a restart), and after any call refused `fenced`:
194
+ at once for a claim or an enroll, at the next tick for a fenced call on running work. Only a responder with a record
195
+ asks, so a first start sends no extra call. A generation past the recorded one, with no own call of
196
+ this service left unanswered, is a move, whatever made it: the operator, or another session that took the slot while
197
+ this one was away or after it crashed. Before 0.53.0 a restarted responder took such a slot back by itself. So a
198
+ second responder of the same capability is no longer a standby once the slot changed hands: after any change of
199
+ holder it waits for take-back, not for the other's lapse.
200
+ - A `fenced` refusal on accepted work while the slot did not move (the generation unchanged) keeps the entry
201
+ `lapsed`; the next tick takes it back with a transfer and goes on (before 0.53.0 the entry was dropped and the
202
+ request left accepted on the board).
203
+ - Then the slot is `moved`: the running handler is told to stop (`ctx.stopped`); a `claimed` entry is dropped (the move
204
+ returned it to the queue); accepted work stays as `lapsed` and work whose effect began as `unknown` (only the slot's
205
+ holder, or the estate owner, reconciles it now). No `com.claim`, `com.renew`, `com.complete`, `com.enroll` or
206
+ `com.responder_replace` is sent for that capability again: not at recovery after a restart, and not when the new
207
+ 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 held on the board, unanswered, until the slot is taken back (CT-175
209
+ will hand them to the new holder). One `board.slot.moved` event (warn, notify) says so, once, also across restarts
210
+ (the record keeps `moved`).
211
+ - A responder that never held the slot (a first start: no record, or a record of another Project), or whose record
212
+ equals the current generation, behaves as before: it enrolls, recovers and replaces its own lapsed work. A recorded or
213
+ reported generation that is not an integer of at least 1, or one not past the record (lower: the record is not this
214
+ board's), is not a move either: today's behaviour, not a stop, and the next receipt rewrites the record. A crash
215
+ between a receipt and its record's write leaves the `asking` record: the next start asks again under the same key and
216
+ records its own generation. Only a replay the board refuses, or one that ran for the first time (`repeated: false`)
217
+ 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
+ unused. A generation written `2.0` is `2`.
219
+ - Only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record; never edit the
220
+ checkpoint's `slots` record by hand to undo a move. In the same board
221
+ 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 held for the slot, takes it
223
+ 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
+ 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 (the reference service has none yet,
226
+ FAA-157): a service built on the kit declares it as an operator action, a button with its tool ([Every button has a
227
+ tool](../SKILL.md), `fabric-actions/0.1`) with gate `relayed` (the operator's decision), never `none`, so no other
228
+ agent can call it on its own; on a server that reads mandates (`agent_id`) it also names its effect class, for example
229
+ `change-policy` (DEC-0125); until Fabric's hub forwards decisions (CT-125) the operator reaches it through the
230
+ service's page or its elicitation, and names it in its docs; otherwise a moved slot stays moved. A move back to this service's own session is not
231
+ detected yet: the receiving side and the hand-over of accepted requests (contract CT-175) wait for Fabric's session
232
+ surface.
233
+
163
234
  ## What a board session may call
164
235
 
165
236
  A board session (`fabric.board.join`) is served the respond scope only: `com.status`, `com.list`, `com.get`,
@@ -183,8 +254,8 @@ raises `directory-invalid` rather than answering; an unlisted Project may still
183
254
  Events go to `emit(kind, level, text, notify=…, data=…)` (`JsonlEventLog.append` fits) with `data {schema, value}`
184
255
  checked by FAC-SEM-101 (no credential in data): `board.request.completed`, `board.request.refused`,
185
256
  `board.request.skipped`, `board.request.reconciled`, `board.request.held`, `board.attempt.lost`,
186
- `board.reconcile.needed`, `board.slot.contested`, `board.access.needed`, `board.blocked`, `board.down`, each
187
- notifying episode said once.
257
+ `board.reconcile.needed`, `board.slot.contested`, `board.slot.moved`, `board.access.needed`, `board.blocked`,
258
+ `board.down`, each notifying episode said once.
188
259
 
189
260
  `check_service.py <id> [--board-kit <the service's fabric_board.py or fabric-board.mjs>]` rule **board.responder**:
190
261
  `NOT_RUN` when the well-known `protocols` names no `fabric-project-comms` revision; `FAIL` for a revision this kit does
@@ -208,12 +279,12 @@ message, retryable, data)`.
208
279
 
209
280
  Python (`fabric_board`): `Responder(*, hub, project_id, capabilities, credential_file, checkpoint_file, handler,
210
281
  admit, observe=None, emit=None, service, now, rand, spawn, session_factory)` with `step()`, `run(stop, drain)`,
211
- `shutdown(drain)`; `WorkContext.progress(note)`, `.begin_effect(summary)`, `.stopped`; `BoardSession(grant)`;
282
+ `shutdown(drain)`, `take_back(capability)`; `WorkContext.progress(note)`, `.begin_effect(summary)`, `.stopped`; `BoardSession(grant)`;
212
283
  `Checkpoint(path)`; `comms_canonical(value)`, `comms_digest(operation, value)`, `receipt_problems(value, op)`,
213
284
  `refusal_problems(value)`, `directory_findings(page)`, `directory_problems(page)`, `find_project(page, project_id,
214
285
  capability)`, `artifact_ref(uri, content|path, label)`, `evidence_problems(list)`, `outcome_problems(outcome,
215
286
  effect_began)`, `self_check()`; `BoardError`, `Stopped`.
216
287
 
217
288
  Node: the same names in camelCase (`locateHub(root, {alive, estate, now})`, `new HubClient(root, {alive, timeout,
218
- estate, post})`, `new Responder({...})` with async `step()`, `run({signal, drain})`, `shutdown(drain)`; `await
219
- ctx.progress(note)`, `await ctx.beginEffect(summary)`; `selfCheck()`). Both kits read and write the same checkpoint file.
289
+ estate, post})`, `new Responder({...})` with async `step()`, `run({signal, drain})`, `shutdown(drain)`,
290
+ `takeBack(capability)`; `await ctx.progress(note)`, `await ctx.beginEffect(summary)`; `selfCheck()`). Both kits read and write the same checkpoint file.
@@ -112,7 +112,8 @@ operator's decision), `decision-invalid` (a decision for another action or objec
112
112
  and `human-step-required`, which carries `data.humanStep` (`sign-in`, `consent`, `payment`, `secret`,
113
113
  `spend-above-limit`, `passkey`) and `data.url` (an https URL or a path on the service's origin) beside `detail`.
114
114
  `refusal_from_action` / `refusalFromAction` turn an action decision into that refusal; a service's own fault never
115
- reaches the caller and reads as `unknown-state`. Since 0.51.0 the `operator-decision-required` message (free text in
115
+ reaches the caller and reads as `unknown-state`, but for `mandate-capability-missing` (DEC-0141 MJ-1): the caller
116
+ reads `operator-decision-required` with that `data.detail`, since no mandate covers the step and only a decision does. Since 0.51.0 the `operator-decision-required` message (free text in
116
117
  DL-19, at most 500 characters) also names, in order, the routes for a client that cannot set `_meta`: Fabric's
117
118
  `agent.call` with `call.decision` once the hub forwards it (CT-125), the service's own elicitation, else the action's
118
119
  page as Fabric Dashboards' `open_link`; both kits carry the same sentence. The gate that answers them is in
@@ -148,7 +148,8 @@ and Fabric's hub forwarding decisions (CT-125) have landed (DEC-0121 *Compatibil
148
148
  operator's own coding agents: Claude Code and Codex cannot set `_meta`, so until the hub forwards `call.decision`
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
- elicitation (the `elicited` hook). This is D8's reason, which keeps `input_actions` off, applied to tools.
151
+ elicitation (the `elicited` hook). The one exception is the board responder's take-back action: it is declared
152
+ `relayed` now and reached through the page or elicitation until CT-125 (`board.md`). This is D8's reason, which keeps `input_actions` off, applied to tools.
152
153
 
153
154
  **Parity (AP-1).** Every action the dashboard, app or page lets a person take (approve, reject, choose, edit, pause,
154
155
  resume, cancel, retry, send, publish, restore, set a policy, any other) has a tool on the MCP surface with the same
@@ -198,7 +199,10 @@ well_known = fs.build_well_known(..., surfaces=surfaces, actions=server.action_t
198
199
  `unknown-state` (`approval-outcome-unreadable` when `approve` answers a refusal whose code or detail DL-18 does not know; an answer
199
200
  that is neither `None` nor a dict is read as no approval, which fails closed; or
200
201
  `caller-missing`: an action-gated server must pass `tier`, `identify` or `authorize`,
201
- since `handle` defaults to T2 and a T2 call names nobody without them).
202
+ since `handle` defaults to T2 and a T2 call names nobody without them). One fault reaches the caller by name:
203
+ `mandate-capability-missing` (DEC-0141 MJ-1), read as `operator-decision-required` with that `data.detail`.
204
+ `on_fault` hears it from `ctx.approval_step` at a job's start under the job's tool, and at the answer under
205
+ `fabric.job.get`; at the start it hears any fault `job_step` answers (also `now-unreadable` from the server's clock).
202
206
  - **Arguments with no canonical form** (a lone surrogate, a NaN or an infinity that Python's `json.loads` lets in,
203
207
  nesting past the recursion limit) have no digest to bind: a relayed tool answers JSON-RPC `-32602`, never an exception.
204
208
  - **The handler** reads `ctx.record` (`"caller"`, or `{by, via, caller}`) and writes it into the service's audit
@@ -285,10 +289,14 @@ server.add_tool(approve_tool, approve_reply, action=reply_action, target="reply_
285
289
  when it reads `source`; a caller that sees no such entry sends the call without `source`, and it is taken as before.
286
290
  - `action_gate(..., self_id=, effect=, caller_agent=, spend_usd=, project=)` / `actionGate({..., self, effect,
287
291
  callerAgent, spendUsd, project})` is the same gate for a server of your own; `action_decision` / `actionDecision`
288
- take `mandate`, `self`, `callerAgent`, `effect`, `spendUsd` and `project` as the contract's function does
289
- (`effect-missing`, `self-missing` are the service's own faults), and the kit's own `mandateCapability` (a job's kept
290
- step: the capability the mandate is read under), which the contract's function does not take; like `effect`, it is
291
- the service's own input, never the caller's.
292
+ take `mandate`, `self`, `callerAgent`, `effect`, `spendUsd`, `project` and `mandateCapability` as the contract's
293
+ function does (DEC-0141 MJ-1): for an action whose tool is `fabric.job.get` (a job's input request) the mandate is
294
+ read under `mandateCapability`, the job's own capability, with the request key as the action; any other action reads
295
+ its own tool. `effect-missing`, `self-missing` and `mandate-capability-missing` (a job's step with no usable
296
+ capability: absent, empty, blank, not a capability name, or `fabric.job.get` itself) are the service's own faults;
297
+ like `effect`, `mandateCapability` is the service's own input, never the caller's. The gate answers
298
+ `mandate-capability-missing` to the caller as `operator-decision-required` (`data.detail` names it), the fault kept
299
+ beside it for the service's log, and nothing is applied, so the job stays `input_required`.
292
300
  - **A job's approval step (interop C3.3).** A job reads the mandate its start call carried at its own approval step:
293
301
  `ctx.approval_step(job_id, key, request, effect=, spend_usd=, project=, message=)` on the start call's context (a
294
302
  server with `agent_id`) answers the record when the mandate covers the step `{capability: the job's, action: key,
@@ -301,10 +309,18 @@ server.add_tool(approve_tool, approve_reply, action=reply_action, target="reply_
301
309
  replaces the kept steps, and an answer clears them: pass `steps=` again on every question whose answer a mandate may
302
310
  cover. The answer's mandate is read under the job's own capability, as at the start (`action_gate(...,
303
311
  mandate_capability=)` / `actionGate({mandateCapability})`), so the start and the answer agree whatever the tool is
304
- named; a mandate can name only capability names, so one that names capabilities never covers a tool named otherwise.
312
+ named: a job whose tool is not a capability name has no capability a mandate can read its step under, and both its
313
+ start and its answer wait for the operator (`mandate-capability-missing` in the message). A mandate whose
314
+ `capabilities` name `fabric.job.get` covers no step of another capability.
305
315
  Node has no tool server: a Node service reads `store.load(jobId).private.steps` and `.private.capability` BEFORE
306
316
  `store.answer` (which clears the steps), gates the answer with `actionGate({..., mandateCapability:
307
- private.capability})`, then answers. Without a string capability the answer is read under `fabric.job.get`.
317
+ private.capability})`, then answers. It logs the `fault` that `jobStep` and `actionGate` return beside their
318
+ answer, as `on_fault` does in Python: `mandate-capability-missing` at the start means the job's tool is not a
319
+ capability name; at the answer it also means `mandateCapability` was not passed. Any service that gates a job's
320
+ answer itself (Node `actionGate`, or Python `action_gate(..., mandate_capability=)` outside `McpToolServer`, whose
321
+ own gate passes it) passes the job's capability; without it every answer a mandate used to cover now waits for the
322
+ operator. `refusal_from_action` / `refusalFromAction` and so `actionRefusalToolResult` answer that fault as
323
+ `operator-decision-required` too.
308
324
 
309
325
  **As a caller.** The agent leading a task states the mandate from the operator's own words with
310
326
  `operator_mandate(task, instruction, scope, lead, at=, expires_at=)` / `operatorMandate({task, instruction, scope,
@@ -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. Nothing starts after quit begins | [Shutdown](#shutdown) | `Drain` / `Drain` |
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 (a board responder leaving its session can take an idle stop 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 |
@@ -1,6 +1,6 @@
1
1
  # fabric-service/0.1: wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `1197a90dc4b57152cabcc71ad17db7559f35a27f`
3
+ Pinned to `fabric-agent-contract` commit `00e050ba7429c0c60261cec871779e0fd6aeeb28`
4
4
  (`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
5
5
  DEC-0032 and DEC-0033; the operator channel DEC-0034 and DEC-0035; the embedded dashboard DEC-0036;
6
6
  delegation and the operator session's lifetime DEC-0039; the meter's call window DEC-0041; session handoff DEC-0042;
@@ -23,7 +23,7 @@ DEC-0124; the operator's mandate `mandate/1`, handed on narrowed with a task DEC
23
23
  operator's agents, its `source` on a mandate and a decision DEC-0126; context hub keys and the organizations an estate
24
24
  record pins DEC-0127 and DEC-0128; the family and the agent network named DEC-0130 and DEC-0137; a reply delivered to
25
25
  the sender it answers, or to whom it names DEC-0135; a hand-over adds no naming list the scope in force leaves out
26
- DEC-0136. The organization's member list, draft exchange, relay and the machines' report, DEC-0129, DEC-0131 to DEC-0134,
26
+ DEC-0136; a job's step under the mandate read under the job's own capability DEC-0141. The organization's member list, draft exchange, relay and the machines' report, DEC-0129, DEC-0131 to DEC-0134,
27
27
  reach the kit only as protocol register entries). The contract's schemas are normative;
28
28
  this page is the working summary. Extension key:
29
29
  `https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.