@passioncode-ai/fabric-agent-adapter 0.38.0 → 0.39.1

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 (53) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +20 -16
  3. package/bin/fabric-agent-adapter.js +6 -6
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +15 -15
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +1 -1
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -4
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +44 -43
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -1
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +12 -12
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +4 -4
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +1 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -4
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +19 -9
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +19 -9
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +6 -6
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +8 -6
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +26 -26
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -1
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -4
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +10 -10
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +14 -14
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +4 -4
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +3 -3
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -5
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +8 -3
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +20 -43
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +1 -1
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -1
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +9 -2
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +33 -0
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +1 -1
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +40 -13
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -1
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -2
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +32 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +1 -1
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +52 -18
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +1 -1
  46. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +35 -35
  47. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +1 -1
  48. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +27 -27
  49. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
  50. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +31 -31
  51. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +9 -9
  52. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +8 -8
  53. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -6
@@ -3,7 +3,7 @@ name: building-fabric-services
3
3
  description: >-
4
4
  Use when handing out or opening a Fabric service dashboard, building a local agent service on a
5
5
  macOS machine, making an ONLINE agent or dashboard (https origin, the remote placement) a
6
- Fabric service, or letting a service answer a Project's board requests — «сделай агенту дашборд»,
6
+ Fabric service, or letting a service answer a Project's board requests: «сделай агенту дашборд»,
7
7
  «локальный сервис агента», «подключи агента к Fabric Dashboards», «отвечать на запросы с доски проекта»,
8
8
  "make this agent a local service", "make an online dashboard a Fabric service", "add the
9
9
  well-known endpoint", «расходы агента на модели». Covers fabric-service/0.1:
@@ -18,10 +18,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
18
18
  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.
19
19
  metadata:
20
20
  author: PassionCode.ai
21
- version: "0.38.0"
21
+ version: "0.39.1"
22
22
  contract-version: "0.1.0"
23
23
  extension: "fabric-service/0.1"
24
- extension-commit: "07042181bfae2a481aa0da1a5651f24c76669c48"
24
+ extension-commit: "c55f2d5a11c9a0197e958e87212fca7ae1154769"
25
25
  ---
26
26
 
27
27
  # Building Fabric services
@@ -29,7 +29,7 @@ metadata:
29
29
  A **service** is an agent that keeps running on the operator's computer: it holds
30
30
  state, answers other agents, and shows a dashboard. This skill makes one that is
31
31
  always alive, comes back after a restart, never runs twice, keeps its state through a
32
- reinstall, and appears in Fabric Dashboards by itself — the `fabric-service/0.1`
32
+ reinstall, and appears in Fabric Dashboards by itself: the `fabric-service/0.1`
33
33
  extension of the Fabric Agent Contract (DEC-0015).
34
34
 
35
35
  Every rule below exists because a real service on this estate broke it. Treat them as
@@ -41,15 +41,15 @@ re-deriving a rule.
41
41
  Use it to create a service, to bring an existing local server under the protocol, or
42
42
  to review one. Do not use it for:
43
43
 
44
- - a script, a cron job or a one-shot CLI — nothing stays running, so there is nothing
44
+ - a script, a cron job or a one-shot CLI: nothing stays running, so there is nothing
45
45
  to supervise; a launchd `StartInterval` job needs no descriptor;
46
46
  - a hosted product with no agent behind it. An online agent or dashboard that should appear in
47
- Fabric IS in scope: it is the remote placement — read [Online services](#online-services--the-remote-placement);
48
- - the provider manifest and admission bundle — that is `adapting-projects-to-fabric`
47
+ Fabric IS in scope: it is the remote placement; read [Online services](#online-services-the-remote-placement);
48
+ - the provider manifest and admission bundle: that is `adapting-projects-to-fabric`
49
49
  (a service that is also a provider does both);
50
50
  - the Fabric Dashboards app itself.
51
51
 
52
- ## Step 0 — is it a service?
52
+ ## Step 0: is it a service?
53
53
 
54
54
  It is a service when at least one is true: other agents call it while the operator is
55
55
  away; it runs jobs longer than one request; it owns a store other tools read; the
@@ -65,7 +65,7 @@ Answer before writing code, and record the answers in the project README:
65
65
  | What it stores | the store, where it lives (Step 2, rule 5), and what survives uninstall |
66
66
  | What it tells the operator | the events worth a notification (Step 5) |
67
67
 
68
- ## Step 1 — choose the surfaces
68
+ ## Step 1: choose the surfaces
69
69
 
70
70
  | Caller | Surface | Why |
71
71
  |---|---|---|
@@ -84,7 +84,8 @@ effect nobody saw, have their own codes and a key per caller (DEC-0087). Read
84
84
  [the interop reference](references/interop.md) before writing an MCP tool; the helpers
85
85
  are in [`scripts/fabric_interop.py`](scripts/fabric_interop.py) and
86
86
  [`scripts/fabric-interop.mjs`](scripts/fabric-interop.mjs). When the callers are agents of the estate, the
87
- service checks each call's delegation token (DEC-0039): read [the delegation reference](references/delegation.md);
87
+ service checks each call's delegation token (DEC-0039; a partner's agent carries the partner innermost, DEC-0090):
88
+ read [the delegation reference](references/delegation.md);
88
89
  the verifier is [`scripts/fabric_delegation.py`](scripts/fabric_delegation.py) /
89
90
  [`scripts/fabric-delegation.mjs`](scripts/fabric-delegation.mjs). A service on the operator's machines that
90
91
  applies the estate record (DEC-0055) verifies each revision first and trusts only the keys it certifies, and runs an
@@ -102,14 +103,14 @@ responder in its own process: [`scripts/fabric_hub.py`](scripts/fabric_hub.py) f
102
103
  every step and stops on `fenced`, `lease_expired` or `authority_changed` (Node: `fabric-hub.mjs`, `fabric-board.mjs`).
103
104
  The request body is data behind the service's own input gate. Read [the board reference](references/board.md).
104
105
 
105
- ## Step 2 — the non-negotiables
106
+ ## Step 2: the non-negotiables
106
107
 
107
108
  1. **Loopback only.** Bind `127.0.0.1`. Refuse any `Host` other than
108
109
  `127.0.0.1:<port>`, `localhost:<port>`, `[::1]:<port>`; refuse a foreign `Origin` and
109
- `Sec-Fetch-Site: cross-site` — on every path, the well-known one included.
110
+ `Sec-Fetch-Site: cross-site`, on every path, the well-known one included.
110
111
  *(A throwaway `python -m http.server` bound to every interface served a source tree to the local network.)*
111
112
  2. **One copy, locked before any side effect.** Take the exclusive lock on
112
- `<data>/service.lock` first — before resuming jobs, starting a scheduler, migrating
113
+ `<data>/service.lock` first, before resuming jobs, starting a scheduler, migrating
113
114
  a store or even creating a token. Held → print one sentence naming the holder's pid
114
115
  and exit **75**. Binding the port is not a lock. Under launchd the kit backs off
115
116
  in-process first (up to 300 s), so a duplicate is a few starts an hour, not a
@@ -126,11 +127,11 @@ The request body is data behind the service's own input gate. Read [the board re
126
127
  5. **State outside code.** Data and config in `~/Library/Application Support/<id>/`,
127
128
  logs in `~/Library/Logs/<id>/`, cache in `~/Library/Caches/<id>/`; the service's own
128
129
  access token in a 0600 file it generates (principle 6). **Every provider credential the
129
- service uses comes from Project Observatory, by name** — the plist starts the service
130
+ service uses comes from Project Observatory, by name**: the plist starts the service
130
131
  with `use_secret.py serve --consumer <label> <project> <NAME>[,<NAME>] -- <command>`, which
131
132
  reads the vault only and records the consumer so a rotation names it; never a copy of a
132
133
  key in a `.env`, a config file or the plist. Never inside the service's own code checkout or a release directory; a
133
- repository that versions the data itself is a store and is fine — declare
134
+ repository that versions the data itself is a store and is fine; declare
134
135
  `source.repository` so the probe can tell them apart. *(Deleting a checkout left a
135
136
  plist launchd retried every 10 s, and the data went with it.)*
136
137
  6. **Tokens stay in files and headers.** Mode 0600, owner-checked, never a symlink;
@@ -138,16 +139,16 @@ The request body is data behind the service's own input gate. Read [the board re
138
139
  one-time login code, never the token.
139
140
  7. **`degraded` is always present.** An empty list asserts full health; `ready` with a
140
141
  non-empty list is a lie the kit corrects to `degraded`.
141
- 8. **Atomic writes.** Temporary file, fsync, rename — for the store's side files, the
142
+ 8. **Atomic writes.** Temporary file, fsync, rename, for the store's side files, the
142
143
  descriptor and the token.
143
144
 
144
- ## Step 3 — build it with the kit
145
+ ## Step 3: build it with the kit
145
146
 
146
147
  Copy the kit into the service (each is one self-contained file): Python
147
148
  [`scripts/fabric_service.py`](scripts/fabric_service.py), Node
148
149
  [`scripts/fabric-service.mjs`](scripts/fabric-service.mjs). Both share file formats, and
149
150
  their locks exclude each other. The complete worked example is
150
- [`scripts/sample_service.py`](scripts/sample_service.py) — read it before writing a
151
+ [`scripts/sample_service.py`](scripts/sample_service.py); read it before writing a
151
152
  handler.
152
153
 
153
154
  Startup order is the part that goes wrong; keep it exactly:
@@ -156,7 +157,7 @@ Startup order is the part that goes wrong; keep it exactly:
156
157
  import fabric_service as fs
157
158
 
158
159
  dirs = fs.service_dirs("example-agent")
159
- lock = fs.hold_single_instance(dirs["data"]) # 1. lock — exits 75 if held (backs off first under launchd)
160
+ lock = fs.hold_single_instance(dirs["data"]) # 1. lock: exits 75 if held (backs off first under launchd)
160
161
  token = fs.ensure_token(dirs["data"] / "service.token") # 2. only now touch state
161
162
  log = fs.JsonlEventLog(dirs["data"] / "events.jsonl") # (or a view over your own log)
162
163
  codes = fs.LoginCodes(dirs["data"] / "auth")
@@ -171,12 +172,12 @@ On every request call `check_request(port, host, origin, sec_fetch_site)` and an
171
172
 
172
173
  | Route | Auth | Kit |
173
174
  |---|---|---|
174
- | `GET /.well-known/fabric-service` | none, guard still applies | `build_well_known(...)` — from memory, under 100 ms |
175
+ | `GET /.well-known/fabric-service` | none, guard still applies | `build_well_known(...)`, from memory, under 100 ms |
175
176
  | `GET /fabric/v1/events?after=&limit=` | service token | `events_page(fetch, after, parse_limit(limit))` |
176
177
  | `POST /fabric/v1/login-code` | service token | `LoginCodes.issue()` |
177
178
  | `GET /fabric/v1/login?code=` | the code | `LoginCodes.redeem(code)` → `Set-Cookie: session_cookie_header(...)`, 302 to the dashboard |
178
179
 
179
- `SIGTERM` drains in-flight work, then exits — 10 s at most, inside `ExitTimeOut`
180
+ `SIGTERM` drains in-flight work, then exits: 10 s at most, inside `ExitTimeOut`
180
181
  (`fs.Drain` / `new Drain()`: `with drain.work():` refuses new work once a stop began);
181
182
  interrupted work resumes on the next start. Log through `RotatingLog` (5 × 5 MB, 0600)
182
183
  and call `cap_stdout_log` once at start.
@@ -186,18 +187,18 @@ in Python; jobs live in `fabric_interop.JobStore(dirs["data"] / "jobs")`, create
186
187
  the lock like every other piece of state. Add `"mcp": {"path": "/mcp", "transport":
187
188
  "streamable-http", "capabilities": [...]}` to the well-known surfaces, and point the
188
189
  descriptor's `fabricManifest` at a manifest whose service key names this
189
- `<id>.<instance>` — `sample_service.py register` shows both.
190
+ `<id>.<instance>`; `sample_service.py register` shows both.
190
191
 
191
- ## Step 4 — install it
192
+ ## Step 4: install it
192
193
 
193
194
  The installer, not the service, owns the plist and the descriptor. Sequence:
194
195
 
195
- 1. `write_descriptor(descriptor)` — refuses a port or `id.instance` another descriptor
196
- claims. A preview or branch copy is a second **instance**, never a second id — and it is
196
+ 1. `write_descriptor(descriptor)`: refuses a port or `id.instance` another descriptor
197
+ claims. A preview or branch copy is a second **instance**, never a second id, and it is
197
198
  one more service the operator sees, probes and gets notified by. Verify a release with its
198
199
  own doctor or self-check before it replaces `default`; a temporary instance is uninstalled
199
200
  (`launchd_uninstall`, `remove_descriptor`) as soon as its check is done, not left running.
200
- 2. `launchd_plist(...)` then `launchd_install(...)` — writes and lints the plist,
201
+ 2. `launchd_plist(...)` then `launchd_install(...)`: writes and lints the plist,
201
202
  `bootout` and waits for the unload, `bootstrap` with retries on the transient I/O
202
203
  error, then polls the well-known document until it answers with **this** identity.
203
204
  A label the operator disabled stays disabled: only a first install enables.
@@ -216,7 +217,7 @@ rewrites the plist and restarts. Read [the lifecycle reference](references/lifec
216
217
  for the plist fields, the release layout, log rotation, behaving under a host lifecycle broker
217
218
  and the Linux and Windows supervisors.
218
219
 
219
- ## Step 5 — the dashboard and the events
220
+ ## Step 5: the dashboard and the events
220
221
 
221
222
  The dashboard is a same-origin page: no inline script, strict CSP, polling that pauses
222
223
  while the operator types or a dialog is open, one panel's failure never blanks the
@@ -234,10 +235,10 @@ and declare it with `extensions=fs.dashboard_extension()`. Read
234
235
  declaration and how to upgrade a dashboard built before the kit: an upgrade you may make yourself,
235
236
  which the plugin's session-start notice offers in a repository that owns the service.
236
237
 
237
- The events feed is a **view over the log you already keep** — a jobs table, a JSONL
238
- journal — not a second store. Each event is one sentence a person reads; set
238
+ The events feed is a **view over the log you already keep** (a jobs table, a JSONL
239
+ journal), not a second store. Each event is one sentence a person reads; set
239
240
  `notify: true` only when the operator must decide, or something failed or degrades their
240
- work — once per episode, with a `subject` and a `link` to the page that resolves it. A
241
+ work, once per episode, with a `subject` and a `link` to the page that resolves it. A
241
242
  question's kind says so (`*.awaiting_*`, `*.approval_*`, `human_step.opened`). The service
242
243
  raises no banners of its own: the host is the one channel. Read
243
244
  [the events reference](references/events-and-notifications.md) for mapping an existing
@@ -252,9 +253,9 @@ A service whose store holds what the operator decided keeps **settings backups**
252
253
  directory, restores only on a fresh database and never overwrites. Read
253
254
  [the settings backups reference](references/settings-backups.md).
254
255
 
255
- ## Step 6 — verify
256
+ ## Step 6: verify
256
257
 
257
- Run the probe against the live service — it reads the descriptor, then checks every
258
+ Run the probe against the live service: it reads the descriptor, then checks every
258
259
  rule it can reach and says which it could not:
259
260
 
260
261
  ```bash
@@ -267,7 +268,7 @@ events auth and shape, single-use login, state outside code, instance lock, plis
267
268
  launchd pid equals answering pid, `dashboard.declared` (ED-9), and the `interop.*` rules:
268
269
  manifest link, capability list, tools equal to the manifest, job tools, unknown job, trace
269
270
  propagation, event trace pairs, and `board.responder` for a service that declares the project board) and exits 1 on
270
- any `FAIL`. A `NOT_RUN` is not a pass —
271
+ any `FAIL`. A `NOT_RUN` is not a pass;
271
272
  name it in the report. A service with a dashboard also runs the layout probe,
272
273
  `node <skill-dir>/scripts/check_dashboard_layout.mjs --service <id>` (WebKit at 360, 480, 768
273
274
  and 1280 px; needs Playwright, else `NOT_RUN`).
@@ -276,7 +277,7 @@ Then prove the lock: start a second copy by hand against the same data directory
276
277
  must exit 75 while the first keeps serving. Prove recovery: `kill -9` the launchd pid;
277
278
  the well-known document must answer again with a new pid within about 15 seconds.
278
279
 
279
- ## Packing and signing it — `fabric-package/1`
280
+ ## Packing and signing it: `fabric-package/1`
280
281
 
281
282
  To hand an agent to another machine or an organization, pack it as one folder (contract DEC-0050, DEC-0051). Write the
282
283
  author's `fabric.package.json` (the descriptive fields; for an agent `resultSignal` and `evals` with the receipt of the
@@ -312,11 +313,11 @@ first commit:
312
313
  org-index `scripts/check_format.py` reports 0 findings for it before it is called done. **An agent
313
314
  someone builds for themselves is not a PassionCode.ai repository:** its licence is its owner's
314
315
  choice, nothing about it is published or listed by the organization, and none of these files is
315
- required of it — though the verified MCP quick start is still how anyone learns to drive it.
316
+ required of it, though the verified MCP quick start is still how anyone learns to drive it.
316
317
 
317
- ## Online services — the remote placement
318
+ ## Online services: the remote placement
318
319
 
319
- An agent or a dashboard that runs online — on a platform, a server, a hosted app — becomes a
320
+ An agent or a dashboard that runs online (on a platform, a server, a hosted app) becomes a
320
321
  Fabric service with the same four routes and one descriptor on the operator's computer
321
322
  (`placement: "remote"`, DEC-0019). Nothing about launchd, the lock or loopback applies; three
322
323
  things change and the kits implement each:
@@ -327,7 +328,7 @@ things change and the kits implement each:
327
328
  | Well-known | only for the token; otherwise `401` with an **empty** body | `wellKnownAllowed('remote', …)` / `well_known_allowed` |
328
329
  | Session | `__Host-fabric_session`, `Secure`; codes in memory, the key from a platform secret so sessions survive a deploy | `remoteSessionCookieHeader`, `new LoginCodes(null, 120, { store: new MemoryCodeStore(), key })` |
329
330
 
330
- 1. **Serve the four routes** in the app that already exists — not beside it. The complete worked
331
+ 1. **Serve the four routes** in the app that already exists, not beside it. The complete worked
331
332
  example is [`scripts/sample-remote-service.mjs`](scripts/sample-remote-service.mjs): it runs
332
333
  with its own TLS or behind a platform router.
333
334
  2. **Secrets live on the platform:** the service token and the session key (32 bytes) are
@@ -335,7 +336,7 @@ things change and the kits implement each:
335
336
  remote service needs is issued and kept in Project Observatory, pushed to the platform, and
336
337
  the movement recorded with `vault.py moved --at <provider>`, so the platform's copy is
337
338
  known rather than invisible.
338
- 3. **Register it on the operator's computer** — the installer side, run there:
339
+ 3. **Register it on the operator's computer**, the installer side, run there:
339
340
  `registerRemote({ id, name, origin, token })` / `register_remote(...)` writes the token file
340
341
  (0600) and the descriptor; pass the token from a file or stdin, never in argv. Public names
341
342
  only in public artifacts: a private service is registered by a local descriptor and nowhere
@@ -349,7 +350,7 @@ behaviour a host shows, rotation, a multi-process platform, and what a host will
349
350
 
350
351
  ## Migrating an existing service
351
352
 
352
- Read [the migration reference](references/migrating-a-service.md) — the order of
353
+ Read [the migration reference](references/migrating-a-service.md): the order of
353
354
  changes that keeps the service answering throughout, and what each existing service on
354
355
  this estate needs.
355
356
 
@@ -366,14 +367,14 @@ this estate needs.
366
367
  against [the protocol reference](references/protocol.md); the probe is the judge.
367
368
  - **No contract checkout:** the protocol reference is pinned to the extension commit
368
369
  in this skill's metadata; do not reconstruct fields from memory.
369
- - **Fabric Dashboards not installed:** nothing changes — the service is complete
370
+ - **Fabric Dashboards not installed:** nothing changes; the service is complete
370
371
  without a host; the descriptor waits for one.
371
372
 
372
373
  ## Gotchas
373
374
 
374
375
  - `lsof` shows `*:<port>` or `0.0.0.0` → the bind is wrong even if the Host check works.
375
376
  - A health probe that only checks HTTP 200 accepts any program on the port. Compare
376
- `service.id` and `service.instance` — two services here once defaulted to ports
377
+ `service.id` and `service.instance`: two services here once defaulted to ports
377
378
  another service already held.
378
379
  - `launchctl bootstrap` right after `bootout` fails with I/O error 5 until the unload
379
380
  finishes; wait, then retry.
@@ -396,7 +397,7 @@ the probe table verbatim with every `NOT_RUN` explained; the lock and kill-9 res
396
397
  with pids; and the one next command. Never call a service conformant while the probe
397
398
  reports a `FAIL`.
398
399
 
399
- ## Dashboard handoff — also for consumers
400
+ ## Dashboard handoff: also for consumers
400
401
 
401
402
  When handing a registered service dashboard to a person, or writing the generated
402
403
  agent's completion/notification instructions, read and apply
@@ -65,7 +65,7 @@
65
65
  <tbody>
66
66
  <tr><td class="fd-mono">job-7f3a91</td><td>example.summarize</td><td><span class="fd-chip fd-s-info"><span class="fd-dot"></span>Running</span></td><td><time>2026-10-11 17:02</time></td><td class="fd-num">12 480</td><td class="fd-num">$0.031</td><td class="fd-mono">4bf92f3577b34da6a3ce929d0e0e4736</td></tr>
67
67
  <tr><td class="fd-mono">job-7f3a90</td><td>example.translate</td><td><span class="fd-chip fd-s-ok"><span class="fd-dot"></span>Done</span></td><td><time>2026-10-11 16:40</time></td><td class="fd-num">8 002</td><td class="fd-num">$0.019</td><td class="fd-mono">00f067aa0ba902b74bf92f3577b34da6</td></tr>
68
- <tr><td class="fd-mono">job-7f3a8f</td><td>example.review</td><td><span class="fd-chip fd-s-warn"><span class="fd-dot"></span>Needs you</span></td><td><time>2026-10-11 15:12</time></td><td class="fd-num">2 210</td><td class="fd-num">—</td><td class="fd-mono">a3ce929d0e0e47364bf92f3577b34da6</td></tr>
68
+ <tr><td class="fd-mono">job-7f3a8f</td><td>example.review</td><td><span class="fd-chip fd-s-warn"><span class="fd-dot"></span>Needs you</span></td><td><time>2026-10-11 15:12</time></td><td class="fd-num">2 210</td><td class="fd-num">none</td><td class="fd-mono">a3ce929d0e0e47364bf92f3577b34da6</td></tr>
69
69
  </tbody>
70
70
  </table>
71
71
  </div>
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.0 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
1
+ /* fabric-dashboard-kit@1.1.1 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
2
2
  Load it same-origin, after fabric-dashboard.css, with the service it opens Fabric on:
3
3
  <script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
4
4
  data-project="project-a" data-project-name="Project A"></script>
@@ -10,7 +10,7 @@
10
10
  (function (root) {
11
11
  'use strict';
12
12
 
13
- var VERSION = '1.1.0';
13
+ var VERSION = '1.1.1';
14
14
  var COOKIE = 'fabric_button';
15
15
  var GET_FABRIC = 'https://passioncode.ai/fabric';
16
16
  var ABSENT_AFTER_MS = 1500;
@@ -212,7 +212,7 @@
212
212
  return a;
213
213
  });
214
214
  var note = el(doc, 'p', { 'class': 'fd-fabric-button-note', role: 'note' });
215
- note.appendChild(doc.createTextNode('Fabric is not installed — get it at '));
215
+ note.appendChild(doc.createTextNode('Fabric is not installed. Get it at '));
216
216
  note.appendChild(el(doc, 'a', { href: GET_FABRIC, target: '_blank', rel: 'noopener', 'class': 'fd-fabric-button-get' }, 'passioncode.ai/fabric'));
217
217
  note.hidden = !absent;
218
218
  menu.appendChild(note);
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.1 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
3
3
  reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
4
4
  Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.1.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.1 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  Load it same-origin with <script src="fabric-dashboard.js" defer></script>: no inline script, no
3
3
  eval, nothing from a CDN. It applies the host's appearance (ED-6, ED-8), folds navigation that
4
4
  does not fit into a More menu (ED-3), makes wide boxes keyboard-reachable (ED-4) and keeps
@@ -8,7 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  var KIT = 'fabric-dashboard-kit';
11
- var VERSION = '1.1.0';
11
+ var VERSION = '1.1.1';
12
12
  /* fabric-dashboard-kit:theme-ids:begin */
13
13
  var THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
14
14
  /* fabric-dashboard-kit:theme-ids:end */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabric-dashboard-kit",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "profile": "fabric-dashboard/0.1",
5
5
  "decision": "DEC-0036",
6
6
  "declarationKey": "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1",
@@ -15,9 +15,9 @@
15
15
  "contrast"
16
16
  ],
17
17
  "files": {
18
- "fabric-dashboard.css": "498571903fac6dfd2d6645283da2f703fa945f410f2fafa269a8c9c322735c3b",
19
- "fabric-dashboard.js": "100f55d940d4ed60aa8511cee4c65219c93114eb7d7a66639b0e4e4d7aa05f22",
20
- "fabric-button.js": "4f1e100936ae65af5539594681e66092ae1834a4952da547a67d0558c9fadbbe",
18
+ "fabric-dashboard.css": "a8847c8e2f386572318842ec1d1613fcb29e4aec060e033c060d8dba6b7c6f02",
19
+ "fabric-dashboard.js": "d0fc41144a6bd413383bd53bbfa6e01a29547c7ec9c01a3927d69fb36cd71a7d",
20
+ "fabric-button.js": "f5599fabfeadfb5c69d30f7ea500846521a166470078a736291c7753014bc25c",
21
21
  "themes.json": "511839f1a1a83eb37fadfd132791474497d5c28c5091700b9aa31ad076e359b5"
22
22
  },
23
23
  "history": {
@@ -29,6 +29,11 @@
29
29
  "fabric-dashboard.css": "498571903fac6dfd2d6645283da2f703fa945f410f2fafa269a8c9c322735c3b",
30
30
  "fabric-dashboard.js": "100f55d940d4ed60aa8511cee4c65219c93114eb7d7a66639b0e4e4d7aa05f22",
31
31
  "fabric-button.js": "4f1e100936ae65af5539594681e66092ae1834a4952da547a67d0558c9fadbbe"
32
+ },
33
+ "1.1.1": {
34
+ "fabric-dashboard.css": "a8847c8e2f386572318842ec1d1613fcb29e4aec060e033c060d8dba6b7c6f02",
35
+ "fabric-dashboard.js": "d0fc41144a6bd413383bd53bbfa6e01a29547c7ec9c01a3927d69fb36cd71a7d",
36
+ "fabric-button.js": "f5599fabfeadfb5c69d30f7ea500846521a166470078a736291c7753014bc25c"
32
37
  }
33
38
  }
34
39
  }
@@ -63,12 +63,12 @@ One tick every 15 s ± 5 s (`step()` answers the wait):
63
63
 
64
64
  | Step | Board call | Checkpoint written first |
65
65
  |---|---|---|
66
- | hold the slot | `com.enroll` (again at about 20 s while idle: the holder's enroll renews its 60 s lease) | — |
66
+ | hold the slot | `com.enroll` (again at about 20 s while idle: the holder's enroll renews its 60 s lease) | none |
67
67
  | take a request | `com.claim`, the same key while nothing is claimable | `claimed` |
68
- | read it | `com.get`: the digest must equal the claim's, and the request must name this Project and capability | — |
68
+ | read it | `com.get`: the digest must equal the claim's, and the request must name this Project and capability | none |
69
69
  | accept | `com.accept` with the claim's exact fence (request, generation, attempt, digest, revision) | `accepting`, then `accepted` |
70
70
  | act | `com.effect_begin`, only when the handler calls `ctx.begin_effect()` | `effect-beginning`, then `in-progress` |
71
- | while working | `com.renew` at about 20 s; `com.progress` for `ctx.progress(note)` | — |
71
+ | while working | `com.renew` at about 20 s; `com.progress` for `ctx.progress(note)` | none |
72
72
  | answer | `com.reply` (fenced) when the outcome carries `reply` | `replying` |
73
73
  | settle | `com.complete {state, observed, summary?, evidence?}` | `completing`, then the entry is dropped |
74
74
 
@@ -82,12 +82,12 @@ own monotonic clock and never reasons from `leaseExpiresAt`.
82
82
  `admit(request)` is required: it is the service's own input gate. It answers `None` to admit or one sentence saying
83
83
  why not; a refusal accepts the request and completes it `failed_known` with that sentence, so the requester learns it
84
84
  and the request does not bounce. A redacted body is refused the same way. The kit never hands a body to a model, a
85
- shell or a tool; what the handler does with an admitted body — and the checks it needs before a model, a tool or a
86
- spend sees it — is the service's own responsibility. A message never carries an approval.
85
+ shell or a tool; what the handler does with an admitted body (and the checks it needs before a model, a tool or a
86
+ spend sees it) is the service's own responsibility. A message never carries an approval.
87
87
 
88
88
  ## Effects, observation, evidence and the reply
89
89
 
90
- - Call `ctx.begin_effect(summary)` — and let it return — before the handler spends, writes outside the service or calls
90
+ - Call `ctx.begin_effect(summary)`, and let it return, before the handler spends, writes outside the service or calls
91
91
  anything that acts. If it raises `Stopped`, act no further.
92
92
  - After an effect began, the outcome must be observed (`observed: true`): the kit never sends an unobserved result; the
93
93
  request stays unknown and is reconciled later.
@@ -130,7 +130,7 @@ contract's matrix, held to its vectors (`test/fixtures/contract/comms-hold-vecto
130
130
  | `com.enroll`, `com.claim`, `com.responder_replace` | the slot pauses: asked again after 15 s, then 30 s, doubling to 15 minutes; one `board.slot.held` event per capability, and `board.slot.released` when an enroll succeeds again |
131
131
  | `com.accept` (the hold came after the claim) | nothing was done: the entry is dropped, the claim lapses and the request waits on the board (`board.request.waiting`); the slot pauses |
132
132
  | `com.effect_begin` | the effect has not begun: the handler waits (same idempotency key, the pause doubling) while the tick keeps renewing the lease, then begins once the hold is lifted; `ctx.stopped` ends the wait |
133
- | accepted work at restart under a hold | kept as `lapsed`, then taken back with `com.responder_replace` and a transfer once the hold is lifted — never claimed back |
133
+ | accepted work at restart under a hold | kept as `lapsed`, then taken back with `com.responder_replace` and a transfer once the hold is lifted, never claimed back |
134
134
 
135
135
  A hold is not an outage: no `board.down`, no notify. Work already accepted keeps running and completes.
136
136
 
@@ -143,7 +143,7 @@ whose responder was gone. Read it as the owner's attestation, not the responder'
143
143
  A board session (`fabric.board.join`) is served the respond scope only: `com.status`, `com.list`, `com.get`,
144
144
  `com.reply` (the fenced form), `com.enroll`, `com.claim`, `com.renew`, `com.accept`, `com.progress`,
145
145
  `com.effect_begin`, `com.complete`, `com.reconcile`, `com.responder_replace`. `BoardSession.call` refuses anything else
146
- locally (`not-in-scope`) — never `com.submit`, `com.cancel`, `com.read_ack` or `com.directory`.
146
+ locally (`not-in-scope`): never `com.submit`, `com.cancel`, `com.read_ack` or `com.directory`.
147
147
 
148
148
  The idempotency epoch comes from `com.status`. A Fabric built before its board-session fix tells a board session none:
149
149
  the kit fails closed (`no-epoch`, `board.blocked`) instead of guessing one.
@@ -152,13 +152,13 @@ the kit fails closed (`no-epoch`, `board.blocked`) instead of guessing one.
152
152
 
153
153
  The directory belongs to a Project session (a coding agent's, or a launched one). Before looking a Project up in a
154
154
  `com.directory` page, `find_project(page, project_id, capability=None)` / `findProject` checks the page: closed shapes
155
- (no count, holder or generation) and FAC-SEM-107 — each Project once, each capability once per Project. Project ids and
155
+ (no count, holder or generation) and FAC-SEM-107: each Project once, each capability once per Project. Project ids and
156
156
  capability names compare **exactly as written** (DEC-0077; they are not URIs, so not by normal form). A page that fails
157
157
  raises `directory-invalid` rather than answering; an unlisted Project may still be addressed by an id already held.
158
158
 
159
159
  ## Events, the probe and the live smoke
160
160
 
161
- Events go to `emit(kind, level, text, notify=…, data=…)` — `JsonlEventLog.append` fits — with `data {schema, value}`
161
+ Events go to `emit(kind, level, text, notify=…, data=…)` (`JsonlEventLog.append` fits) with `data {schema, value}`
162
162
  checked by FAC-SEM-101 (no credential in data): `board.request.completed`, `board.request.refused`,
163
163
  `board.request.skipped`, `board.request.reconciled`, `board.request.held`, `board.attempt.lost`,
164
164
  `board.reconcile.needed`, `board.slot.contested`, `board.access.needed`, `board.blocked`, `board.down`, each
@@ -167,10 +167,10 @@ notifying episode said once.
167
167
  `check_service.py <id> [--board-kit <the service's fabric_board.py or fabric-board.mjs>]` rule **board.responder**:
168
168
  `NOT_RUN` when the well-known `protocols` names no `fabric-project-comms` revision; `FAIL` for a revision this kit does
169
169
  not read or a fixture conversation that goes wrong; `PASS` when the kit's `self_check()` / `selfCheck()` passes (the
170
- whole conversation, a fenced accept, a session without an epoch — no Fabric touched).
170
+ whole conversation, a fenced accept, a session without an epoch; no Fabric touched).
171
171
 
172
172
  The live smoke runs only against a disposable Fabric (built app and its own stack), never an operator's estate: Fabric's
173
- COM-03 packet (`docs/evidence/plans/2026-10-04-project-communications.md`) has the exact commands — register
173
+ COM-03 packet (`docs/evidence/plans/2026-10-04-project-communications.md`) has the exact commands: register
174
174
  `fabric-mcp` for Claude Code and Codex, the operator allows `session.enroll` and `board.respond`
175
175
  (`board:<example-agent project>/<capability>`), Claude submits a request, the responder enrolls, claims, accepts and
176
176
  completes, then is killed mid-claim and replaced. Without that Fabric, report it `NOT_RUN`.
@@ -1,4 +1,4 @@
1
- # The dashboard kit — `fabric-dashboard/0.1`
1
+ # The dashboard kit: `fabric-dashboard/0.1`
2
2
 
3
3
  Fabric Dashboards shows a service's page in a pane beside its sidebar and the agent console, and the
4
4
  operator drags the console wider whenever they need it. The page never knows how much room it will
@@ -97,12 +97,12 @@ service. Load it after the stylesheet, with ids only:
97
97
  scroll. The mark and the word *Fabric*, the mark alone below 480 px; the accessible name is always *Open Fabric*;
98
98
  `--ink` on `--panel` with an `--edge` boundary, the pairs the themes are measured on.
99
99
  - **Links (FL-1…FL-6):** built with the kit's port of the contract's `buildFabricLink`; a value outside the grammar
100
- — or one shaped like a credential — means no button, never a bad link. A press opens a view in Fabric and starts
100
+ (or one shaped like a credential) means no button, never a bad link. A press opens a view in Fabric and starts
101
101
  nothing (FB-8).
102
102
  - **The host (FB-6):** when the host's `fabric_button` cookie says `owner=host`, the page hides its own button.
103
103
  - **Fabric absent (FB-5):** when the cookie says `fabric=absent`, or `data-fabric="absent"`, or when the page is still
104
- visible and focused 1.5 s after a link was followed, the menu says "Fabric is not installed — get it at
105
- passioncode.ai/fabric". Nothing downloads, nothing retries. A local service's server decides `data-fabric` with
104
+ visible and focused 1.5 s after a link was followed, the menu says that Fabric is not installed and names
105
+ passioncode.ai/fabric to get it. Nothing downloads, nothing retries. A local service's server decides `data-fabric` with
106
106
  `fabric_button_presence()` / `fabricButtonPresence()`: `present` while Fabric's own descriptor
107
107
  (`services/fabric.default.json`, Fabric ADR-0139) is registered and, for an app's descriptor (`manager: "person"`,
108
108
  DEC-0069), its app is still installed; `absent` otherwise. A press only follows a link, so the button never reads
@@ -1,4 +1,4 @@
1
- # Dashboard links — producer and consumer rule
1
+ # Dashboard links: producer and consumer rule
2
2
 
3
3
  Read when an agent creates a dashboard, implements `<tool> dashboard`, or hands
4
4
  an existing Fabric service dashboard to a person. This also applies to generated
@@ -4,10 +4,10 @@ The dashboard is where the operator decides; it must stay truthful when parts of
4
4
  service are not.
5
5
 
6
6
  1. **Same origin, static assets.** HTML shell plus hashed `app.js`/`app.css` from a
7
- whitelist; no inline script; a strict CSP. Nothing from a CDN — the page must work
7
+ whitelist; no inline script; a strict CSP. Nothing from a CDN: the page must work
8
8
  offline on a plane.
9
9
  2. **State first, then detail.** The top of every page answers "is it working, and
10
- does it need me?" — the same `status`, `degraded` and attention tiles the
10
+ does it need me?": the same `status`, `degraded` and attention tiles the
11
11
  well-known document publishes, so the page and Fabric Dashboards never disagree.
12
12
  3. **Calm refresh.** Poll the page's own API every 5 s while visible, back off when
13
13
  hidden, and pause while a field is focused, a form has unsaved input or a dialog is
@@ -16,7 +16,7 @@ service are not.
16
16
  4. **Partial failure stays partial.** Load panels independently (`Promise.allSettled`);
17
17
  a failed panel shows its own one-sentence error and a retry, and the rest render.
18
18
  5. **Every error is a sentence and an action.** "Store listing push failed: App Store
19
- Connect rejected the key. Settings → Keys" — never a status code, a stack trace or a
19
+ Connect rejected the key. Settings → Keys", never a status code, a stack trace or a
20
20
  blank area. Show when data was last fresh.
21
21
  6. **Server down is a state, not a crash.** When the API stops answering, keep the
22
22
  last data greyed with "Service not answering since 17:02", and recover by itself
@@ -30,4 +30,4 @@ service are not.
30
30
  9. **Links into the page are stable.** Event `link`s point at hash routes that survive
31
31
  a reload (`/dashboard#/approvals/77`) so a notification opens the exact item.
32
32
  10. **No second browser tab required.** With Fabric Dashboards installed, the page is
33
- shown inside it; do not open windows or tabs on your own — use in-page dialogs.
33
+ shown inside it; do not open windows or tabs on your own; use in-page dialogs.
@@ -1,4 +1,4 @@
1
- # Delegation — checking who acts for whom (DEC-0039)
1
+ # Delegation: checking who acts for whom (DEC-0039)
2
2
 
3
3
  A service that other agents of the estate call checks each call offline with the kit's verifier: the estate hub
4
4
  signs a delegation token naming the person (`sub`), the actor chain (`act`), this service (`aud`), the
@@ -22,7 +22,7 @@ contract's own verifier does.
22
22
  ## Declaring it
23
23
 
24
24
  In the well-known document: `surfaces.mcp.auth: "delegation"` (only delegation tokens) and
25
- `auth.delegation: {issuer, jwks}` — the `iss` you trust (`urn:fabric:estate:<id>`) and where the key set is read.
25
+ `auth.delegation: {issuer, jwks}`: the `iss` you trust (`urn:fabric:estate:<id>`) and where the key set is read.
26
26
  The descriptor's token stays the host's: it is never a caller's credential on `/mcp` (DL-12). The probe checks
27
27
  the declaration (`auth.delegation-declared`) and that `/mcp` refuses the descriptor token.
28
28
 
@@ -34,7 +34,7 @@ keys = fd.JwksCache(well_known["auth"]["delegation"]["jwks"]) # https,
34
34
  gate = fd.delegation_gate(audience="example-agent", jwks=keys, issuer=well_known["auth"]["delegation"]["issuer"], # required
35
35
  effects={"example.write": "draft", "example.publish": "publish"}) # each tool's manifest effect
36
36
  server = fi.McpToolServer("example-agent", VERSION, authorize=gate) # pass the request headers to handle()
37
- # in a handler: ctx.delegation["principal"], ["caller"], ["chain"] — record the chain with the work
37
+ # in a handler: ctx.delegation["principal"], ["caller"], ["chain"]; record the chain with the work
38
38
  ```
39
39
 
40
40
  ```js
@@ -50,7 +50,7 @@ if (!verdict.ok) return refusalToolResult(verdict);
50
50
  DL-25 places each caller in a tier by its origin: **T0** a loopback caller under the owner's account that the
51
51
  service's own local floor (the descriptor token, the Host guard) has authenticated; **T1** an enrolled machine of
52
52
  the same owner; **T2** everyone else, and any call relayed onto loopback from off the machine (a tunnel, a proxy, the
53
- hub forwarding a T2 caller). DL-26: a T0 or T1 caller is never required to present a token — pass the tier,
53
+ hub forwarding a T2 caller). DL-26: a T0 or T1 caller is never required to present a token; pass the tier,
54
54
  `gate(tool, headers, tier="T0")` or `server.handle(message, headers, tier="T0")`, and the call passes with no
55
55
  delegation. A token such a caller presents is still verified and refused when invalid. A T2 caller takes the whole
56
56
  profile. Money limits and human gates bind at every tier.
@@ -63,24 +63,34 @@ job's own capability and effect, and only for the principal and caller that star
63
63
  `scope-not-granted`); `on_input` gets the checked delegation on its context. Start jobs with `ctx.start_job()` so the
64
64
  job records who started it.
65
65
 
66
+ **A partner in the chain** (DEC-0090). A token a partner's agent presents for a customer organization carries the
67
+ partner as the innermost actor: `{partner: "org:<id>"}`, exactly that member, under at least one agent. It counts
68
+ toward the depth limit and ends the chain; outermost, with an `act` beneath, or as `personal` it is
69
+ `malformed-chain`. A node with a `sub` beside `partner` is checked as an agent, yet `ctx.delegation["chain"]` lists it
70
+ by its `org:` id, as the contract's `chainOf` does; the schema refuses such a node, so a correct issuer never mints one
71
+ (raised with the contract, FAA-64). Job ownership and idempotency keys follow `(principal, caller)` and do not include
72
+ the partner (FAA-64). `chain_depth(act)` /
73
+ `chainDepth(act)` answer the contract's depth, infinity for a chain that is not one. Minting a partner's token (its
74
+ grant, FAC-SEM-124) is the issuer's, not the kit's.
75
+
66
76
  ## Approvals
67
77
 
68
78
  An action with an external effect may need the operator's approval (DL-17, DL-18). The order is fixed: the token,
69
79
  then the grant check where there is one, then `verify_approval` / `verifyApproval`, which consumes the approval's
70
80
  `jti` last, then the action. Without an approval, answer `approval-required` with
71
- `action_digest(action)` / `actionDigest(action)` — the SHA-256 of the action's RFC 8785 form (`jcs`) — so the caller
81
+ `action_digest(action)` / `actionDigest(action)` (the SHA-256 of the action's RFC 8785 form, `jcs`) so the caller
72
82
  can ask the hub. When the meter denies a grant check, answer `refusal_from_denial(denial)` /
73
83
  `refusalFromDenial(denial)`: the meter's code unchanged, its window and level in `error.data` (DL-21 step 6). Keep consumed ids in `FileJtiStore` (0600, survives a restart); `MemoryJtiStore` is for tests
74
84
  and `UNSAFE_NO_JTI_STORE` exists only by name. No store at all, or a store file that cannot be read, is
75
- `unknown-state` — never a pass.
85
+ `unknown-state`, never a pass.
76
86
 
77
87
  ## What a refusal looks like
78
88
 
79
- DL-19: an MCP tool answers `refusal_tool_result(r)` / `refusalToolResult(r)` — `isError: true` and
89
+ DL-19: an MCP tool answers `refusal_tool_result(r)` / `refusalToolResult(r)`: `isError: true` and
80
90
  `structuredContent: {error: {code, message, data: {detail}}}`; an HTTP route answers `refusal_http(r)` /
81
91
  `refusalHttp(r)` with the code's status (401 for `unauthenticated`, `token-expired`, `principal-unproven`, with
82
92
  `WWW-Authenticate: Bearer error="invalid_token"`; 503 for `issuer-unavailable` and `unknown-state`; 403 for the
83
- rest). A published key the service cannot use — of small order, for another use, or published twice — is the
93
+ rest). A published key the service cannot use (of small order, for another use, or published twice) is the
84
94
  issuer's fault, `issuer-unavailable` (DEC-0045); a gate without a trusted issuer refuses everything as
85
95
  `no-trusted-issuer`. `detail` is a short machine word; the token, the approval and the keys never appear in a result or a log.
86
96
 
@@ -112,4 +122,4 @@ current verified revision as `estate=` / `{ estate }`, as a value or as a functi
112
122
  ## What the kit does not do
113
123
 
114
124
  It does not mint tokens, check attenuation against a parent (the issuer's rule, DL-5), check a grant or a spend
115
- window (the meter's, DL-21), or decide which actions need an approval — the service decides that.
125
+ window (the meter's, DL-21), or decide which actions need an approval; the service decides that.