@passioncode-ai/fabric-agent-adapter 0.21.0 → 0.23.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 (25) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +38 -6
  3. package/package.json +1 -1
  4. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -2
  5. package/plugins/fabric-agent-adapter/hooks/hooks.json +43 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +10 -7
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +32 -15
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +46 -14
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -2
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +5 -2
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +35 -20
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +77 -9
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +167 -36
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -19
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +107 -16
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +270 -49
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +8 -2
  21. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  22. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +217 -0
  23. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +99 -0
  24. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +111 -0
  25. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1265 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,117 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.23.0 - 2026-10-13
4
+
5
+ The contract pin moves from `b291bf0` (DEC-0055) to contract `main` `fe55fc6`, and both kits follow every rule in that
6
+ range that touches what they already do (FAA-32; the contract's hand-over of 2026-10-12, its CT-18 row). `b291bf0`
7
+ joins the supported revisions. What the range adds beyond that is its own row: FAA-33…37.
8
+
9
+ ### Changed
10
+
11
+ - **The supervisor marker comes as an argument too** (DEC-0057, CT-01, P4-01): `supervisor_marker` /
12
+ `supervisorMarker` read `FABRIC_SERVICE_SUPERVISOR=<manager>` and `--fabric-supervisor <manager>` /
13
+ `--fabric-supervisor=<manager>` (after the executable, before `--`; a bare flag consumes no `--…` token), with `node`
14
+ among the managers. Markers that disagree — the two forms, the argument twice, a bare or empty argument beside the
15
+ variable — exit 75 at once, before the lock, naming both. `supervised()` and `supervised_by_launchd()` take the
16
+ marker; `hold_single_instance` / `holdSingleInstance` take `env` and `argv`. A holder whose pid cannot be read is
17
+ named unknown (`FAC-SEM-078`).
18
+ - **Writers:** `task_xml` / `taskXml` write `--fabric-supervisor task-scheduler` in the task's `Arguments`, before any
19
+ `--` (a task's action carries no environment, so a Task Scheduler copy now waits like one under launchd);
20
+ `systemd_unit` / `systemdUnit` set the variable unconditionally; every writer refuses a marker in the program
21
+ arguments. `add_supervisor_argument(parser)` lets a service's argparse accept the flag; the sample service uses it.
22
+ - **DL-HUBKEY** (DEC-0060): `hub_key_certified` / `hubKeyCertified(record, kid, x, at, applied, extra)` answer the
23
+ contract's verdict — `{certified, machine, revision}` or `{certified: false, fault, detail}` — instead of a boolean.
24
+ A revision outside its window, unreadable, lower than the applied one or forking it is the trust source's fault
25
+ (`issuer-unavailable`: `estate-outside-window`, `estate-unreadable`, `estate-rolled-back`, `estate-forked`); a `kid`
26
+ on two machines, a missing `x` and a numeric `kid` never certify; revocation refs compare exactly. The verifiers
27
+ take `estate_applied` / `estateApplied` and `estate_revocations` / `estateRevocations`; in the Node kit only an
28
+ absent `estate` skips the check (`null` is an unreadable source). Every verifier refuses a clock that is not a number
29
+ of seconds `unknown-state` `no-clock` before row 1.
30
+ - **The estate record** (DEC-0063, DEC-0066): `verify_estate_record` / `verifyEstateRecord` admit `issuedAt` up to 60 s
31
+ ahead (`CLOCK_SKEW_SECONDS`); the signing passkey must be under a `sign.<domain>` RP (`wrong-rp`) and its origins
32
+ within it (`wrong-origin`); the record's rules add `FAC-SEM-092`.
33
+ - **The Windows token-file reader** (DEC-0057, `FAC-SEM-079`): `windows_token_acl_problem` /
34
+ `windowsTokenAclProblem` decide over the file's flags, owner and ACEs `{type, sid, mask}`: an object or callback ACE
35
+ is refused, an allow with an empty mask and every deny grant nothing, SIDs compare case-insensitively, anything
36
+ unreadable is refused. The Python kit reads the ACEs from SDDL (`acl_from_sddl`), the Node kit through .NET's raw
37
+ descriptor with the mask read unsigned. `windows_acl` / `windowsAcl` now answer ACEs; `token_acl_problem` /
38
+ `tokenAclProblem` are replaced by `token_file_problem` / `tokenFileProblem`.
39
+ - **The descriptor** (DEC-0057 item 5): `label` and `plist` are refused beside `systemd` and `task-scheduler`; a
40
+ `localPath` starting `//` or `/\` is a share and refused.
41
+
42
+ ### Evidence
43
+
44
+ - Vendored from the contract at `fe55fc6` and re-answered by its own functions (`test/fixtures/contract/vendor_vectors.mjs`):
45
+ `hub-key-vectors.json` (51 cases, 14 verifier answers; the hub seeds dropped), `supervisor-vectors.json` (22 marker
46
+ cases, 11 `FAC-SEM-078` records), `windows-token-vectors.json` (30 cases), `descriptor-negatives.json` (DEC-0057's
47
+ seven); `estate-vectors.json` regenerated (49 verify cases). Both kits answer every case as the contract does.
48
+ - The kits' own starts against a held lock — by hand, the node, a task, an unknown holder, three conflicts — are
49
+ recorded and read by a port of `lockOutcomeRules` that itself reads the contract's 11 records as it does.
50
+ - The sample service, started with `--fabric-supervisor=node`, serves a well-known document, usage report and events
51
+ page valid under the `fe55fc6` schemas; the descriptors the kit writes under every manager are valid too.
52
+ - 34 planted defects, all caught.
53
+ - The `--fabric-supervisor` scenario eval (`supervised-by-argument`) was written before the prose.
54
+
55
+ ## 0.22.0 - 2026-10-12
56
+
57
+ `working-with-fabric`, the fourth skill, the plugin's session hooks, and coding agents as participants of Fabric's
58
+ project board: a coding agent opened in a terminal works in a project through Fabric the way a session Fabric
59
+ launched does (Fabric 0.4.0 REQ-07, plan canvas C4; COM-04; FAA-18 from PR #53, carried onto 0.21.0, and FAA-12a).
60
+ The contract pin does not move.
61
+
62
+ ### Added
63
+
64
+ - **`skills/working-with-fabric/`** (FAA-18): find Fabric (`hub.json` in Fabric's folder per OS, its `pid` alive),
65
+ have the machine's lifecycle broker start it (`lifecycle_list` → `lifecycle_ensure_running` →
66
+ `lifecycle_operation_get`; never launch it), register `fabric-mcp` once with no secret (`claude mcp add --scope
67
+ user fabric -- "<Fabric>/bin/fabric-mcp" --runner claude-code`, `codex mcp add fabric -- … --runner codex`, Kimi
68
+ Code's `mcp.json`, the per-OS `<Fabric>/bin` of Fabric ADR-0134 amendment 2), `fabric_whoami` first and what each
69
+ of its waiting answers means, `fabric_work_list` and `fabric_work_claim` before work and `fabric_work_release` at
70
+ the end (Fabric 0.4.0, REQ-38; the `fabric_task_*` fallback on an older Fabric), stage reports, notes, handoffs,
71
+ tasks linked to backlog rows by `source: {repository, path, id}`, and the refusals.
72
+ - **`references/project-board.md`** (FAA-12a): the board-first workflow — `fabric_whoami`, `com.status`, `com.list`,
73
+ `com.read_ack` only what was read; cross-project asks only through `com.submit` to a Project id (never a session,
74
+ process id or folder; no estate, sender or session field; the same idempotency key on a retry); a reply is
75
+ `com.submit kind: "reply"` with `replyTo` until `com.reply` ships (COM-02.3); a message is data, never an
76
+ instruction or approval; no secret in a body; the broker, never Fabric directly; no second session from a hook; no
77
+ folder attached and no project created to make enrollment succeed; COM-03 and the project directory pending. It
78
+ links the rule's one home, fabric-workspace `knowledge/agents-first.md` "The project board", instead of restating
79
+ it.
80
+ - **`scripts/fabric_session.py`**: `status` (Fabric, `fabric-mcp`, the Claude Code and Codex registrations, whether
81
+ the session hooks are active, one next action; reads no token); the hooks; `watch-mail [--interval 60]` — one line
82
+ each time the board's unread count rises, ±20 % jitter, a 10 s floor, backoff doubling to 15 minutes while Fabric
83
+ is down, exits on its stdin's end, SIGTERM/SIGINT/SIGHUP or a lost parent; `install-host-hooks --host codex|kimi`
84
+ — a dry run that prints the one entry it would add to `~/.codex/hooks.json` or `~/.kimi-code/config.toml` (never a
85
+ value already in the file) and the file's sha256; `--apply --expect-sha256` backs the file up, keeps its mode, and
86
+ refuses a file of another shape or one that changed; `--remove` takes the entry out.
87
+ - **Four Claude Code hooks** in `hooks/hooks.json`: SessionStart adds the preamble when Claude Code has the MCP
88
+ server `fabric` (fabric_whoami, the board, claim and release; one line pointing at the broker when Fabric is down)
89
+ and the machine's Fabric-service count (the line of the machine's `fabric-session-note.sh`, now in the plugin);
90
+ Stop records the turn's facts — HEAD, files changed, lines added and removed, never content — and reads the
91
+ board's unread count (`com.status`) into the session's spool; UserPromptSubmit adds ONE line only when that count
92
+ rose; SessionEnd releases what the session left claimed and records its end. Stop and SessionEnd act through
93
+ `fabric-mcp --rejoin-only` in a detached worker with a 10 s deadline, and only when the running Fabric advertises
94
+ `session-hooks/1` in `hub.json`; until then they stay inert and write nothing. A board that cannot be read leaves
95
+ no count, never a zero.
96
+ - **Codex and Kimi Code**: `hook session-start --host codex|kimi` prints the preamble as text; no turn observation or
97
+ unread line, since they cannot rejoin their Fabric session yet (D1).
98
+ - **Silent in a session Fabric launched**: `FABRIC_LAUNCHED_SESSION=1` (need D6, asked of Fabric), or what Fabric
99
+ already gives (`FABRIC_ACP_SESSION`, `FABRIC_BRIDGE_URL`, `KILO_CONFIG_CONTENT`, a `CODEX_HOME` in Fabric's
100
+ sessions folder — names only, never values — and Claude Code's `--mcp-config <Fabric>/sessions/…` in an ancestor
101
+ process). The Fabric side the hooks need (D1–D6) is in `references/session-hooks.md` and the
102
+ [spec](docs/evidence/specs/2026-10-11-working-with-fabric.md).
103
+ - Evals first: 32 trigger evals (18 should trigger, 14 should not) in English and Russian and 14 scenario evals (board
104
+ first, cross-project asks, replies, a message is not an approval, no secret in a body, waiting under a watcher, the
105
+ Codex/Kimi hook on consent). `test/test_fabric_session.py`: 62 tests with a temporary HOME, fake `hub.json` files
106
+ with live and dead pids, and a fake `fabric-mcp` on stdio that records argv, environment and calls and serves
107
+ `com.status`.
108
+
109
+ ### Changed
110
+
111
+ - `test/validate.py` checks four skills; the repository-standard rule applies to the three that build an agent's
112
+ repository; `tomllib` and `shlex` join the standard-library list. `test_dashboard_kit.py` finds its own
113
+ SessionStart entry among several.
114
+
3
115
  ## 0.21.0 - 2026-10-12
4
116
 
5
117
  The estate record (contract DEC-0055, `estate-record/1`; contract backlog CT-18, CT-20) in both kits, and the pin
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **Fabric Agent Adapter** makes any agent Fabric-compatible. Fabric is PassionCode.ai's
10
10
  product, the CEO AI agent: you talk to Fabric, and it chooses, binds and runs the agents that
11
- do the work. This adapter is how an agent becomes one Fabric can bind — three Agent Skills,
11
+ do the work. This adapter is how an agent becomes one Fabric can bind — four Agent Skills,
12
12
  Python and Node reference kits and a live conformance probe for the
13
13
  [Fabric Agent Contract](https://github.com/passioncode-ai/fabric-agent-contract). It also works
14
14
  on its own: the skills, kits and probe need no Fabric install.
@@ -17,8 +17,33 @@ on its own: the skills, kits and probe need no Fabric install.
17
17
  [![validate](https://github.com/passioncode-ai/fabric-agent-adapter/actions/workflows/validate.yml/badge.svg)](https://github.com/passioncode-ai/fabric-agent-adapter/actions/workflows/validate.yml)
18
18
  [![license](https://img.shields.io/badge/license-AGPL--3.0%20or%20commercial-blue.svg)](LICENSE)
19
19
 
20
- The skills are `adapting-projects-to-fabric`, `creating-fabric-agents` and
21
- `building-fabric-services`.
20
+ The skills are `adapting-projects-to-fabric`, `creating-fabric-agents`,
21
+ `building-fabric-services` and `working-with-fabric`.
22
+
23
+ `working-with-fabric` is for the other side: a coding agent (Claude Code, Codex, Kimi Code, Kilo, Hermes) opened in
24
+ a terminal that works in a project through Fabric. It finds Fabric (`hub.json`, its `pid`), has the machine's
25
+ lifecycle broker start it when it is down, registers `fabric-mcp` once with no secret, calls `fabric_whoami`
26
+ first, reads the project board (`com.status`, `com.list`, `com.read_ack` only what was read) and asks other
27
+ Projects only through `com.submit`, asks who is working on what and claims its task before it starts
28
+ (`fabric_work_list`, `fabric_work_claim`, Fabric 0.4.0), links tasks to backlog rows instead of copying them, and
29
+ releases at the end (`fabric_work_release`). `scripts/fabric_session.py status` prints where Fabric stands and one
30
+ next action:
31
+
32
+ ```bash
33
+ python3 plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py status
34
+ ```
35
+
36
+ In Claude Code the plugin adds four session hooks: SessionStart puts that preamble into the session when the MCP
37
+ server `fabric` is registered (one line pointing at the broker when Fabric is down), with the machine's
38
+ Fabric-service count; Stop records each turn's facts (HEAD, files changed, lines added and removed — never content)
39
+ and the board's unread count; UserPromptSubmit adds one line only when that count rose; SessionEnd releases what the
40
+ session left claimed. Stop, UserPromptSubmit and SessionEnd act only when the running Fabric advertises
41
+ `session-hooks/1`. All are silent in a session Fabric launched, never block the session, read no token and always
42
+ exit 0. `fabric_session.py watch-mail` waits for board mail under a background watcher, and `install-host-hooks
43
+ --host codex|kimi` adds the session-start preamble to Codex or Kimi Code (a dry run unless `--apply`). The
44
+ [hooks reference](plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md) lists what
45
+ they need from Fabric; the [board reference](plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md)
46
+ is the board-first workflow.
22
47
 
23
48
  `building-fabric-services` makes an agent a long-lived local service with a dashboard
24
49
  that is always alive, runs as one copy, keeps its state through a reinstall and appears
@@ -40,12 +65,12 @@ box, sticky chrome at most 120 px — with seven WCAG AA themes (light and dark)
40
65
  `fabric_appearance` cookie. `scripts/dashboard_kit.py install|status` copies and checks it,
41
66
  `scripts/check_dashboard_layout.mjs` measures a rendered page in WebKit at 360, 480, 768 and 1280 px
42
67
  (Playwright; `NOT_RUN` without it), and the probe's `dashboard.declared` checks the declaration. The
43
- plugin's one hook (SessionStart) prints a single line in a repository whose dashboard has no kit or an
68
+ plugin's dashboard-kit hook (SessionStart) prints a single line in a repository whose dashboard has no kit or an
44
69
  older one, and nothing anywhere else; the [kit reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md)
45
70
  has the upgrade steps, taken only with the operator's go-ahead.
46
71
 
47
72
  Dashboard handoffs use the host-generated `open_link` as the primary action. The
48
- three skills carry the same [consumer procedure](plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md)
73
+ three builder skills carry the same [consumer procedure](plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md)
49
74
  and a portable `scripts/check_dashboard_link.py` gate for generated renderers.
50
75
  It rejects primary HTTP when the host is available, failed-host browser fallback,
51
76
  and local links addressed to another device. A skill install alone does not filter
@@ -98,6 +123,7 @@ Generic Agent Skills clients:
98
123
  npx skills add passioncode-ai/fabric-agent-adapter --skill adapting-projects-to-fabric
99
124
  npx skills add passioncode-ai/fabric-agent-adapter --skill creating-fabric-agents
100
125
  npx skills add passioncode-ai/fabric-agent-adapter --skill building-fabric-services
126
+ npx skills add passioncode-ai/fabric-agent-adapter --skill working-with-fabric
101
127
  ```
102
128
 
103
129
  ### Configure
@@ -178,6 +204,12 @@ Or, for an agent that does not exist yet:
178
204
  Create a new fabric-compatible agent for <capability>; its consumer is <who calls it>.
179
205
  ```
180
206
 
207
+ Or, in a coding agent opened in a project folder:
208
+
209
+ ```text
210
+ Work on backlog row FAA-18 through Fabric: check who is working here, claim it, and release it when done.
211
+ ```
212
+
181
213
  Or run the deterministic helper directly:
182
214
 
183
215
  ```bash
@@ -203,7 +235,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
203
235
  The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
204
236
 
205
237
  - version: `0.1.0`
206
- - commit: `b291bf09e0a9f9fd3c16cf540832cd021203b7d1`
238
+ - commit: `fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0`
207
239
 
208
240
  Every other mention of the default contract revision — this section, the skills' metadata, the
209
241
  skill card, `adapt_project.py` — must equal it; `python3 test/validate.py` fails a tree
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/fabric-agent-adapter",
3
- "version": "0.21.0",
3
+ "version": "0.23.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'"
@@ -2,8 +2,8 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
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.",
6
- "version": "0.21.0",
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.23.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "One line at session start in a repository whose Fabric dashboard has no dashboard kit or an older one (fabric-dashboard/0.1, DEC-0036). Silent everywhere else; reads only, never writes, always exits 0.",
2
+ "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start in a repository whose Fabric dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036). The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start the preamble when Claude Code has the MCP server `fabric` (fabric_whoami, then the project board through com.status and com.list, claim and release), and the machine's Fabric-service count; 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. 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.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -11,6 +11,48 @@
11
11
  "timeout": 5
12
12
  }
13
13
  ]
14
+ },
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "python3 -S \"${CLAUDE_PLUGIN_ROOT}/skills/working-with-fabric/scripts/fabric_session.py\" hook session-start",
20
+ "timeout": 5
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "Stop": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "python3 -S \"${CLAUDE_PLUGIN_ROOT}/skills/working-with-fabric/scripts/fabric_session.py\" hook stop",
31
+ "timeout": 5
32
+ }
33
+ ]
34
+ }
35
+ ],
36
+ "SessionEnd": [
37
+ {
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "python3 -S \"${CLAUDE_PLUGIN_ROOT}/skills/working-with-fabric/scripts/fabric_session.py\" hook session-end",
42
+ "timeout": 5
43
+ }
44
+ ]
45
+ }
46
+ ],
47
+ "UserPromptSubmit": [
48
+ {
49
+ "hooks": [
50
+ {
51
+ "type": "command",
52
+ "command": "python3 -S \"${CLAUDE_PLUGIN_ROOT}/skills/working-with-fabric/scripts/fabric_session.py\" hook user-prompt-submit",
53
+ "timeout": 5
54
+ }
55
+ ]
14
56
  }
15
57
  ]
16
58
  }
@@ -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.21.0"
8
+ version: "0.23.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "b291bf09e0a9f9fd3c16cf540832cd021203b7d1"
10
+ contract-commit: "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -70,7 +70,7 @@ Use exactly:
70
70
 
71
71
  - contract version `0.1.0`;
72
72
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
73
- - commit `b291bf09e0a9f9fd3c16cf540832cd021203b7d1`.
73
+ - commit `fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0`.
74
74
 
75
75
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
76
76
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -21,10 +21,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
21
21
 
22
22
  CONTRACT_VERSION = "0.1.0"
23
23
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
24
- CONTRACT_COMMIT = "b291bf09e0a9f9fd3c16cf540832cd021203b7d1"
24
+ CONTRACT_COMMIT = "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0"
25
25
  # #region supported-contract-revisions — docs: README.md#contract-pin
26
26
  # Only this declaration may name a legacy revision in live files.
27
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
27
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
28
  # #endregion supported-contract-revisions
29
29
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
30
30
  MCP_REVISION = "2026-07-28"
@@ -16,10 +16,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
16
16
  compatibility: Python 3.9+ or Node.js 20+ for the kits; 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.
17
17
  metadata:
18
18
  author: PassionCode.ai
19
- version: "0.21.0"
19
+ version: "0.23.0"
20
20
  contract-version: "0.1.0"
21
21
  extension: "fabric-service/0.1"
22
- extension-commit: "b291bf09e0a9f9fd3c16cf540832cd021203b7d1"
22
+ extension-commit: "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0"
23
23
  ---
24
24
 
25
25
  # Building Fabric services
@@ -95,16 +95,19 @@ itself (`isSmallOrderKey`), because Node before 26 lets OpenSSL accept a signatu
95
95
 
96
96
  ## Keys the estate record certifies
97
97
 
98
- A verifier that also reads the estate's record (DEC-0055, [the estate record reference](estate-record.md)) trusts a
99
- `kid` only while a current revision certifies it for a machine and does not revoke it (DL-14). Pass the current
100
- verified revision as `estate=` / `{ estate }`, as a value or as a function returning it, to `delegation_gate`,
98
+ A verifier that also reads the estate's record (DEC-0055, DEC-0060, [the estate record reference](estate-record.md))
99
+ trusts a `kid` only while the revision it reads certifies it (DL-14, row 4a, right after the signature). Pass the
100
+ current verified revision as `estate=` / `{ estate }`, as a value or as a function returning it, with
101
+ `estate_applied` / `estateApplied` and `estate_revocations` / `estateRevocations`, to `delegation_gate`,
101
102
  `verify_delegation` or `verify_approval`.
102
- - An uncertified `kid` is refused `unauthenticated` (`approval-invalid` for an approval), with detail
103
+ - An uncertified `kid` is the token's fault: `unauthenticated` (`approval-invalid` for an approval), detail
103
104
  `kid-not-certified`.
104
- - An unreadable record is refused `issuer-unavailable`, with detail `estate-unreadable`.
105
+ - A revision that cannot be read, is outside its window, is lower than the applied one or forks it is the trust
106
+ source's fault: `issuer-unavailable`, detail `estate-unreadable`, `estate-outside-window`, `estate-rolled-back` or
107
+ `estate-forked`.
105
108
  - Without `estate`, nothing changes.
106
- - Pass only a revision `verify_estate_record` accepted, and the highest one the machine has applied. The rule trusts
107
- what it is given, so an unverified or older revision would certify a key the current one revoked.
109
+ - Every verifier refuses a clock that is not a number of seconds `unknown-state` `no-clock` before row 1: a NaN clock
110
+ would make every time row false and pass an expired token.
108
111
 
109
112
  ## What the kit does not do
110
113
 
@@ -43,12 +43,16 @@ It checks, in the order of the ER table:
43
43
  parent, and no revocation dropped.
44
44
  5. **The signer.** The signing passkey must be among the trusted ones and not revoked by the parent. A revision may
45
45
  revoke its own signer: that is rotation.
46
- 6. **The assertion.**
46
+ 6. **The assertion.** The passkey is a signing credential (DEC-0063): an RP ID that is not a dedicated signing RP,
47
+ `sign.<domain>` in lower case, is `wrong-rp` before the assertion is read, and an origin outside the RP ID is not
48
+ pinned, so a ceremony that ran there is `wrong-origin`.
47
49
  - Client data: type, challenge, origin, no cross-origin use.
48
50
  - The RP ID hash, and the user-presence and user-verification flags.
49
51
  - The signature: ES256 or EdDSA, under a key validated first (a valid P-256 point; an Ed25519 key of full order).
50
- 7. **The window.** At most 30 days.
51
- 8. **The record's own rules.** A violation is `rule-violation` with its `codes`.
52
+ 7. **The window.** At most 30 days. A revision may be issued up to 60 s ahead of the verifier's clock (DEC-0066,
53
+ `CLOCK_SKEW_SECONDS`); earlier than that it is `not-yet-valid`.
54
+ 8. **The record's own rules.** A violation is `rule-violation` with its `codes`, `FAC-SEM-072…076` and `FAC-SEM-092`
55
+ (each passkey the record lists is a signing credential, its origins within its RP ID).
52
56
 
53
57
  Dates are strict RFC 3339: a `T`, an offset, and a day that exists. `:60` and a space are refused. Passkey JWK `x`
54
58
  and `y` must be canonical base64url. The Python kit verifies ES256 in pure Python (P-256, strict DER), and the Node
@@ -56,22 +60,35 @@ kit through `node:crypto`.
56
60
 
57
61
  ## Trusting a delegation key
58
62
 
59
- A delegation verifier that reads the estate record trusts a token's `kid` only while a current revision certifies it
60
- for a machine and does not revoke it (DL-14, contract CT-20). Give the gate the current verified revision, or a
61
- function that returns it:
63
+ A delegation verifier that reads the estate record trusts a token's `kid` only while the revision it reads certifies
64
+ it (DL-14, contract DEC-0060): exactly one machine's `hubKey` has that `kid` and the published key's `x`, the clock is
65
+ inside the revision's window (from 60 s before `issuedAt`, DEC-0066, to `validUntil`), and neither the revision nor the
66
+ revocation delta's entries applied since it revoke `hub-key <kid>` or `machine <id>`. It is checked right after the
67
+ signature (DL-13 row 4a). Give the gate the current verified revision, or a function that returns it, the highest
68
+ revision the machine applied, and the delta's revocations:
62
69
 
63
70
  ```python
64
- gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., estate=lambda: current)
71
+ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., estate=lambda: current,
72
+ estate_applied={"revision": applied_number, "digest": applied_digest},
73
+ estate_revocations=lambda: delta_entries)
65
74
  ```
66
75
 
67
- - A `kid` the record does not certify is refused like an unknown key: `unauthenticated` (for an approval,
68
- `approval-invalid`) with detail `kid-not-certified`. The same holds when the record names another key under that
69
- kid, revokes it, or is outside its window.
70
- - A record that cannot be read certifies nothing: `issuer-unavailable` with detail `estate-unreadable`.
71
- - **What you pass in.** Pass only a revision that `verify_estate_record` accepted, and the highest revision the
72
- machine has applied. The rule trusts the record it is given. A record nobody verified, or an older revision still
73
- inside its window, would certify a key the current revision revoked (a rollback).
74
- - `hub_key_certified(record, kid, x, at)` / `hubKeyCertified(...)` answers the question on its own.
76
+ - **The token's fault** — no such certification, another key under that `kid`, a `kid` on two machines, a revoked hub
77
+ key or machine — is `unauthenticated` (for an approval, `approval-invalid`) with detail `kid-not-certified`.
78
+ - **The trust source's fault** is `issuer-unavailable`: `estate-unreadable` (a source that throws or answers nothing
79
+ readable, a revision without a number or digest, a window of zero or over 30 days, dates read strictly, `machines` or
80
+ `revocations` not arrays), `estate-outside-window`, `estate-rolled-back` (lower than the applied revision) and
81
+ `estate-forked` (the applied number with another digest). A caller cannot cure these by minting again.
82
+ - An unreadable clock — not a finite number of seconds a `Date` can hold — is refused `unknown-state` `no-clock`
83
+ before row 1, in every verifier.
84
+ - Only an absent `estate` skips the check. In the Node kit `null` is an unreadable source; in Python, where `None` is
85
+ the absent value, pass a function returning `None` for a source that has nothing.
86
+ - **What you pass in.** Pass only a revision that `verify_estate_record` accepted and whose number is not lower than
87
+ the highest the machine applied; a higher one only when you verified its chain from the applied revision.
88
+ - `hub_key_certified(record, kid, x, at, applied, extra_revocations)` /
89
+ `hubKeyCertified(record, kid, x, at, applied, {extraRevocations})` answers the question on its own:
90
+ `{certified: true, machine, revision}` or `{certified: false, fault, detail}`. Both kits are held to the contract's
91
+ `fixtures/vectors/hub-key.json` (`test/fixtures/contract/hub-key-vectors.json`).
75
92
 
76
93
  ## Checking a record you produce
77
94
 
@@ -73,12 +73,36 @@ operator's real jobs.
73
73
  ## One copy under a supervisor
74
74
 
75
75
  Started by hand, a copy that finds `service.lock` held prints one sentence naming the
76
- holder and exits 75 at once. Under launchd that exit is a respawn every `ThrottleInterval`
77
- (10 s), forever, and under systemd one every `RestartSec`. So a copy whose plist or unit
78
- carries `FABRIC_SERVICE_SUPERVISOR=launchd` (or `systemd`) backs off in-process — 0.5 s doubling to 30 s, 300 s in all — takes over if the holder leaves,
79
- and exits 75 only when the wait runs out: a few starts an hour instead of 360. It touches
80
- nothing while it waits. Two installs sharing one data directory are still the defect to
81
- remove; the back-off only stops the loop from costing the machine.
76
+ holder (or saying the holder is unknown, when its pid cannot be read) and exits 75 at once.
77
+ Under launchd that exit is a respawn every `ThrottleInterval` (10 s), forever, and under
78
+ systemd one every `RestartSec`. So a copy its supervisor started, and that says so with the
79
+ **supervisor marker**, backs off in-process — 0.5 s doubling to 30 s, 300 s in all — takes over
80
+ if the holder leaves, and exits 75 only when the wait runs out: a few starts an hour instead of
81
+ 360. It touches nothing while it waits. Two installs sharing one data directory are still the
82
+ defect to remove; the back-off only stops the loop from costing the machine.
83
+
84
+ The marker (contract DEC-0057, CT-01) is the variable `FABRIC_SERVICE_SUPERVISOR=<manager>` or
85
+ the argument `--fabric-supervisor <manager>` (also `--fabric-supervisor=<manager>`), after the
86
+ executable and before any `--`; `<manager>` is `launchd`, `systemd`, `task-scheduler` or `node`
87
+ (Project Observatory's node, which supervises the services it starts). The writer of the
88
+ supervisor file writes it: `launchd_plist` and `systemd_unit` / `systemdUnit` set the variable,
89
+ always; `task_xml` / `taskXml` put the argument in the task's `Arguments`, because a task's action
90
+ carries no environment. Each writer refuses a marker already in the program arguments. A copy
91
+ never infers supervision from its parent process.
92
+
93
+ - `supervisor_marker(env, argv)` / `supervisorMarker({env, argv})` reads both forms and answers
94
+ `supervised` (with the manager), `manual` or `conflict`. Only an empty variable counts as no
95
+ marker; a bare `--fabric-supervisor` (followed by nothing, by `--` or by another `--…` flag,
96
+ none of which it consumes) names no manager.
97
+ - Markers that disagree — the variable and the argument with different values, the argument
98
+ twice, or a bare or empty argument beside the variable — exit 75 at once, before the lock and
99
+ whether or not it is held, naming both markers.
100
+ - A service's own argument parser accepts the flag in both spellings and ignores it: in Python,
101
+ `add_supervisor_argument(parser)` (argparse otherwise exits 2 on an unknown flag); the sample
102
+ service adds it to the parser and to each command.
103
+ - The kits are held to the contract's `fixtures/vectors/service-supervisor.json`, and their
104
+ starts against a held lock are recorded and read as `FAC-SEM-078` reads them
105
+ (`test/*supervisors*`).
82
106
 
83
107
  ## Directories
84
108
 
@@ -151,7 +175,7 @@ control character (a line break would end a unit line early).
151
175
  **Linux — a systemd user unit.** `systemd_unit(unit, argv, working_directory=…)` /
152
176
  `systemdUnit(unit, argv, {workingDirectory})` writes `Restart=always`, `RestartSec=10`,
153
177
  `TimeoutStopSec` above the drain (15 s default), `KillMode=mixed`, `WantedBy=default.target`
154
- and `FABRIC_SERVICE_SUPERVISOR=systemd`, quoting every argument (`%` → `%%`, `$` → `$$`).
178
+ and `FABRIC_SERVICE_SUPERVISOR=systemd` (always, whatever the caller passed), quoting every argument (`%` → `%%`, `$` → `$$`).
155
179
  `WorkingDirectory=` is written unquoted: systemd reads it as the rest of the line, so quotes would
156
180
  become part of the path and the unit would be refused (`systemd-analyze verify` checks it in CI). The
157
181
  descriptor says `"lifecycle": {"manager": "systemd", "unit": "<id>.<instance>.service"}`.
@@ -179,9 +203,10 @@ CI job has the real `schtasks` create, read back and delete such a task. The des
179
203
  | stop, keep off | `schtasks /End /TN <task>`, then `schtasks /Change /TN <task> /DISABLE` |
180
204
  | uninstall | `task_uninstall` / `taskUninstall`: `/End`, then `/Delete /F` |
181
205
 
182
- A task cannot set an environment variable, so a copy Task Scheduler restarts does not back off
183
- in-process; `IgnoreNew` keeps the scheduler from starting a second copy, and a hand-started copy
184
- holding the lock makes the scheduled one exit 75 once a minute until it leaves.
206
+ A task cannot set an environment variable, so `task_xml` / `taskXml` write the marker as an
207
+ argument, `--fabric-supervisor task-scheduler`, before any `--`: a copy the Task Scheduler starts
208
+ backs off in-process like one under launchd or systemd. `IgnoreNew` keeps the scheduler from
209
+ starting a second copy.
185
210
 
186
211
  **One copy on each system.** The Python kit takes `flock` (Linux) or a locked byte
187
212
  (Windows); the Node kit cannot, so it holds a name instead — an abstract unix socket on Linux,
@@ -192,7 +217,14 @@ holds the same name beside its file lock, so the two kits exclude each other eve
192
217
 
193
218
  **Token files on Windows.** The privacy of `service.token` is its ACL (service.md *Windows
194
219
  token files*): the kits write a protected list — the user, SYSTEM and Administrators, set before the
195
- token's bytes go in, any entry already there dropped — and refuse
196
- on read a link, a file outside the profile, another owner, or any other granted SID, naming the
197
- SID. The Node kit reads the list with Windows PowerShell 5.1 (.NET, no modules) and writes it
198
- with `icacls`; the path goes through the environment, never a command line.
220
+ token's bytes go in, any entry already there dropped. On read, `windows_token_acl_problem` /
221
+ `windowsTokenAclProblem` decide over what was read — the file's flags, its owner SID and each ACE
222
+ as `{type, sid, mask}` (DEC-0057, `FAC-SEM-079`, held to the contract's
223
+ `fixtures/vectors/windows-token.json`): a reparse point, a directory, a real path outside the
224
+ profile, another owner, an allow ACE with a non-zero mask for any other SID, an ACE of another type
225
+ than allow or deny (an object or callback ACE), or anything unreadable is refused, naming the SID;
226
+ deny ACEs and an allow with an empty mask grant nothing; SIDs compare case-insensitively. The Python
227
+ kit reads the ACEs from the file's SDDL (`acl_from_sddl`); the Node kit reads them with Windows
228
+ PowerShell 5.1 (.NET's raw descriptor, no modules), converting .NET's signed mask to the unsigned
229
+ one, and writes the list with `icacls`; the path goes through the environment, never a command line.
230
+ An inherit-only ACE applies to children, not to the file, and is not read.
@@ -1,11 +1,13 @@
1
1
  # fabric-service/0.1 — wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `b291bf09e0a9f9fd3c16cf540832cd021203b7d1`
3
+ Pinned to `fabric-agent-contract` commit `fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0`
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;
7
7
  canonical forms DEC-0043; `degraded[].setup` and `run.mcp.stdio.scope` DEC-0044; delegation sealed and trust tiers
8
- DEC-0045; runner and model profiles DEC-0046). The contract's schemas are normative;
8
+ DEC-0045; runner and model profiles DEC-0046; the estate record DEC-0055; one copy under any supervisor and its marker,
9
+ the Windows token-file reader DEC-0057; the hub key the estate certifies DEC-0060; signing credentials DEC-0063; the
10
+ estate record's 60 s skew DEC-0066). The contract's schemas are normative;
9
11
  this page is the working summary. Extension key:
10
12
  `https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
11
13
 
@@ -172,8 +172,11 @@ class Probe:
172
172
  self.add("descriptor.valid", "FAIL" if problems else "PASS", "; ".join(problems) or str(self.path))
173
173
  if fs.WINDOWS:
174
174
  # DEC-0032: on Windows the file's ACL is its privacy.
175
- owner, granting = fs.windows_acl(self.path)
176
- problem = fs.token_acl_problem(self.path, owner, granting, fs.windows_user_sid())
175
+ owner, aces = fs.windows_acl(self.path)
176
+ info = os.lstat(self.path)
177
+ problem = fs.windows_token_acl_problem({"isRegularFile": stat.S_ISREG(info.st_mode), "isReparsePoint": stat.S_ISLNK(info.st_mode),
178
+ "realPathInProfile": True, "ownerSid": owner, "aces": aces}, fs.windows_user_sid())
179
+ problem = problem and problem["message"]
177
180
  self.add("descriptor.private", "FAIL" if problem else "PASS", problem or "owner-only ACL")
178
181
  else:
179
182
  mode = stat.S_IMODE(os.stat(self.path).st_mode)