@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
@@ -1,7 +1,8 @@
1
- # The estate record — what the operator's machines are, signed by their passkey (DEC-0055)
1
+ # The estate record: what the operator's machines are, signed by their passkey (DEC-0055)
2
2
 
3
3
  Fabric Agent Contract DEC-0055, `docs/specification/estate-record.md`, schema `schemas/estate-record.schema.json`,
4
- rules `FAC-SEM-072…076`, with contexts (DEC-0082, `docs/specification/contexts.md`, `FAC-SEM-110`, `FAC-SEM-111`).
4
+ rules `FAC-SEM-072…076`, with contexts (DEC-0082, `docs/specification/contexts.md`, `FAC-SEM-110`, `FAC-SEM-111`) and a
5
+ partner's scoped publisher key (DEC-0090, `docs/specification/partner-channel.md`, `FAC-SEM-122`).
5
6
  One revision of one person's estate lists:
6
7
  - the passkeys that sign it;
7
8
  - the machines, with their device keys and a certificate of each machine's hub key;
@@ -56,7 +57,7 @@ It checks, in the order of the ER table:
56
57
  7. **The window.** At most 30 days. A revision may be issued up to 60 s ahead of the verifier's clock (DEC-0066,
57
58
  `CLOCK_SKEW_SECONDS`); earlier than that it is `not-yet-valid`.
58
59
  8. **The record's own rules.** A violation is `rule-violation` with its `codes`, `FAC-SEM-072…076`, `FAC-SEM-110`,
59
- `FAC-SEM-111` and `FAC-SEM-092`
60
+ `FAC-SEM-111`, `FAC-SEM-122` and `FAC-SEM-092`
60
61
  (each passkey the record lists is a signing credential, its origins within its RP ID).
61
62
 
62
63
  Dates are strict RFC 3339: a `T`, an offset, and a day that exists. `:60` and a space are refused. Passkey JWK `x`
@@ -78,13 +79,13 @@ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., esta
78
79
  estate_revocations=lambda: delta_entries)
79
80
  ```
80
81
 
81
- - **The token's fault** — no such certification, another key under that `kid`, a `kid` on two machines, a revoked hub
82
- key or machine — is `unauthenticated` (for an approval, `approval-invalid`) with detail `kid-not-certified`.
82
+ - **The token's fault** (no such certification, another key under that `kid`, a `kid` on two machines, a revoked hub
83
+ key or machine) is `unauthenticated` (for an approval, `approval-invalid`) with detail `kid-not-certified`.
83
84
  - **The trust source's fault** is `issuer-unavailable`: `estate-unreadable` (a source that throws or answers nothing
84
85
  readable, a revision without a number or digest, a window of zero or over 30 days, dates read strictly, `machines` or
85
86
  `revocations` not arrays), `estate-outside-window`, `estate-rolled-back` (lower than the applied revision) and
86
87
  `estate-forked` (the applied number with another digest). A caller cannot cure these by minting again.
87
- - An unreadable clock — not a finite number of seconds a `Date` can hold — is refused `unknown-state` `no-clock`
88
+ - An unreadable clock (not a finite number of seconds a `Date` can hold) is refused `unknown-state` `no-clock`
88
89
  before row 1, in every verifier.
89
90
  - Only an absent `estate` skips the check. In the Node kit `null` is an unreadable source; in Python, where `None` is
90
91
  the absent value, pass a function returning `None` for a source that has nothing.
@@ -113,6 +114,14 @@ of `[A-Za-z0-9][A-Za-z0-9_-]{0,127}`. Absent is `personal`; `null` is not absent
113
114
  - **A share grant is never personal** (FAC-SEM-110). A grant to a principal outside the estate (`principalKind:
114
115
  external`) is used in that principal's organization; `context: "personal"` on it is a finding, and an absent one
115
116
  means not stated. `grant_context_findings(grant, at)` / `grantContextFindings(grant, at)` check one grant on its own.
117
+ - **A partner's publisher key is trusted only where its scope says** (DEC-0090, FAC-SEM-122). `publisherScopes[]`
118
+ names the keys that are a partner's (`kid`, `partner: org:<id>`, `contexts`); a placement under such a key is in one
119
+ of the organization contexts listed, never `personal` or another organization. A key is scoped once, and only a key
120
+ `publisherKeys` pins. A key no scope names keeps DEC-0051's meaning, trusted in any context.
121
+ - **Update a copied kit before a revision carries `publisherScopes`.** A service runs the copy of the kit it was built
122
+ with; a copy older than 0.39.0 does not read `publisherScopes`, so it trusts a partner's key in every context and
123
+ finds no rule broken. A service that applies the estate record updates its copy of `fabric_estate.py` /
124
+ `fabric-estate.mjs` to 0.39.0 or later first.
116
125
 
117
126
  Every rule is total on hostile JSON, as the contract's are: an `instancePath` names an item by its index as written (a
118
127
  non-object before it does not shift it), and a value whose own `toString` member is data is shown as its JSON. Both kits
@@ -174,14 +183,15 @@ sharer's node's and the organization server's, not this kit's.
174
183
  ## Checking a record you produce
175
184
 
176
185
  `estate_record_rules(kind, value)` / `estateRecordRules(kind, value)` are the contract's rules, finding for finding:
177
- - `estate-record` (FAC-SEM-072…076, 110, 111);
186
+ - `estate-record` (FAC-SEM-072…076, 110, 111, 122);
178
187
  - `estate-record-revision` `{parent, next}` (072);
179
- - `estate-placement` `{record, machine, package, context?}` (074, 075, 111): the package placed on that machine in that
188
+ - `estate-placement` `{record, machine, package, context?}` (074, 075, 111, 122): the package placed on that machine in that
180
189
  context, `personal` when `context` is absent.
181
190
 
182
191
  `placement_trust(record, machine, agentRef, context="personal")` / `placementTrust(record, machine, agentRef, context)`
183
192
  names the publisher keys a package verifier trusts for the package placed in that context. A caller that passes no
184
- context gets the personal placement, as before contexts.
193
+ context gets the personal placement, as before contexts. A partner's key placed outside the contexts its scope names
194
+ answers `{publisher: []}`: the placement exists, and no key may verify it (DEC-0090).
185
195
 
186
196
  ## What the kit does not do
187
197
 
@@ -13,7 +13,7 @@ The feed is a view: never copy rows into a second store that can drift from the
13
13
 
14
14
  ## Reading a feed (FAC-SEM-036)
15
15
 
16
- A client of `GET /fabric/v1/events` — or of any token-protected route — sends the token in the
16
+ A client of `GET /fabric/v1/events` (or of any token-protected route) sends the token in the
17
17
  header the descriptor's `auth.header` names, in the form `auth.scheme` names: the raw token for
18
18
  `none`, `Bearer <token>` for `Bearer`. Never assume `Authorization: Bearer`: a client that did
19
19
  was refused on every poll and silently dropped every notify event. `token_header(auth, token)` /
@@ -27,7 +27,7 @@ in the URL.
27
27
  - `level`: `info` (routine), `notice` (the operator may want to look), `warning`
28
28
  (something is degrading), `error` (something failed and needs action).
29
29
  - `text`: one sentence, in the operator's language, with the object named and the
30
- number stated — "The Q3 report draft is ready for your approval (12 sections, 3
30
+ number stated: "The Q3 report draft is ready for your approval (12 sections, 3
31
31
  languages)." Never a machine id, never a stack trace.
32
32
  - `subject`: what it is about (`{type: "report", id: "q3", label: "Q3 report"}`), so a host
33
33
  can group.
@@ -42,11 +42,11 @@ lands on what matters:
42
42
 
43
43
  | The operator… | Event | `notify` |
44
44
  |---|---|---|
45
- | must decide or act — a choice, an approval, a key to enter | kind says it: `job.awaiting_choice`, `plan.awaiting_approval`, `human_step.opened`; level `notice` | `true` |
46
- | lost work — a job failed after its retries, a key expired | level `error`; kind `*.failed`, `*.expired` | `true` |
47
- | will lose work soon — a degraded source that blocks orders | level `warning`, `service.degraded`, **once when it starts** | `true` |
45
+ | must decide or act: a choice, an approval, a key to enter | kind says it: `job.awaiting_choice`, `plan.awaiting_approval`, `human_step.opened`; level `notice` | `true` |
46
+ | lost work: a job failed after its retries, a key expired | level `error`; kind `*.failed`, `*.expired` | `true` |
47
+ | will lose work soon: a degraded source that blocks orders | level `warning`, `service.degraded`, **once when it starts** | `true` |
48
48
  | asked to be told when a long job finishes | `job.delivered`, level `notice` | `true` only if that person asked; a host may still keep it in Activity |
49
- | only needs the record — progress, retries, recoveries, syncs | `job.started`, `service.recovered`, `*.cleared`, level `info` or `notice` | `false` |
49
+ | only needs the record: progress, retries, recoveries, syncs | `job.started`, `service.recovered`, `*.cleared`, level `info` or `notice` | `false` |
50
50
 
51
51
  - **One episode, one request.** Notify on the transition, never on each poll: a source that
52
52
  flaps between degraded and healthy sends `service.degraded` with `notify: true` once, and later
@@ -4,14 +4,14 @@ Normative source: the Fabric Agent Contract's `docs/specification/interop.md` (D
4
4
  at the commit this plugin pins. This page says how the kit implements it; where the two
5
5
  differ, the contract is right.
6
6
 
7
- A service that other agents — and Fabric — call does five things. Each has one helper in
7
+ A service that other agents (and Fabric) call does five things. Each has one helper in
8
8
  `scripts/fabric_interop.py` (Python) and `scripts/fabric-interop.mjs` (Node).
9
9
 
10
10
  | Rule | What the service does | Helper |
11
11
  |---|---|---|
12
- | C3.1 capability = tool | serves each `mcp` capability of its manifest as the MCP tool of the same name, with the manifest's input schema unchanged, its output schema unchanged — or, for a job, wrapped as `oneOf[result envelope, job handle]` — and annotations from the effect | `tool_for_capability` / `toolForCapability`, `job_tool_output_schema`, `expected_annotations` |
12
+ | C3.1 capability = tool | serves each `mcp` capability of its manifest as the MCP tool of the same name, with the manifest's input schema unchanged, its output schema unchanged (or, for a job, wrapped as `oneOf[result envelope, job handle]`) and annotations from the effect | `tool_for_capability` / `toolForCapability`, `job_tool_output_schema`, `expected_annotations` |
13
13
  | C3.2 long work = job | a capability whose work can outlive one request declares `"job": true` in its interop block, returns `{"job": {"id", "status": "working"}}`, and serves `fabric.job.get` and `fabric.job.cancel` | `JobStore`, `McpToolServer` (Python), `unknown_job_result` |
14
- | C3.2 result | a completed job carries the full result envelope — the shape a synchronous call returns: `id`, `contractVersion`, `outcome`, `done`, `proof`, `scope`, `notVerified`, `artifacts`, `createdAt`, `producer`, `output`, `usage`, and its `trace` | `result_envelope` / `resultEnvelope` |
14
+ | C3.2 result | a completed job carries the full result envelope, the shape a synchronous call returns: `id`, `contractVersion`, `outcome`, `done`, `proof`, `scope`, `notVerified`, `artifacts`, `createdAt`, `producer`, `output`, `usage`, and its `trace` | `result_envelope` / `resultEnvelope` |
15
15
  | C3.3 a person decides | the job goes `input_required` with an elicitation: a titled single-select in form mode, and URL mode for anything secret | `choice_request`, `form_request`, `url_request` |
16
16
  | C3.4 trace | every call runs as a child span of the caller's `_meta.traceparent`; outgoing calls carry the same trace; the result envelope records it; events about traced work carry `traceId` and `spanId`, events about untraced work none | `child_traceparent`, `trace_ids`, `make_event(..., trace_id=, span_id=)` |
17
17
 
@@ -61,7 +61,7 @@ response = server.handle(json_rpc_message) # POST /mcp body in, JSON re
61
61
  open with a handshake, and a server that refuses it is marked failed), `server/discover`,
62
62
  `tools/list` and `tools/call` with JSON responses. Every tool's `outputSchema` must be
63
63
  rooted at `type: object` (DEC-0018): `add_tool` refuses any other root, because a client may
64
- drop the whole tool list over one bad schema — an SDK-level test will not show it, so check
64
+ drop the whole tool list over one bad schema; an SDK-level test will not show it, so check
65
65
  with the real client (below). A service already on an MCP SDK keeps
66
66
  the SDK and uses only the helpers. The Node kit has the helpers and the same job file
67
67
  format, without the dispatcher. `scripts/sample_service.py` is the worked example:
@@ -73,7 +73,7 @@ DEC-0017 amended by DEC-0018), so its `structuredContent` always conforms; an ev
73
73
  untraced work carries no trace pair.
74
74
 
75
75
  **Check with a real client**, never only an SDK: `FABRIC_REAL_CLIENT=1 python3 -m unittest
76
- test.test_real_client` in the adapter repository, or by hand — a throwaway directory, a
76
+ test.test_real_client` in the adapter repository, or by hand: a throwaway directory, a
77
77
  temporary `--mcp-config` (mode 600, the token in a header) and `--strict-mcp-config`, so
78
78
  the operator's own configuration is not touched:
79
79
 
@@ -105,7 +105,9 @@ As a provider:
105
105
  `limit_holds=False`, a `limit-reached` job once `approval_granted=True`, and an `outcome-unknown` job never.
106
106
  - **Usage you did not measure is `null`, never 0,** but only toward a caller whose request `_meta.protocols` names
107
107
  `fabric-interop/0.2`: pass `request_meta` to `result_envelope` (`usage_toward`). Toward any other caller the kit
108
- writes the measured lower bound, as before.
108
+ writes the measured lower bound, as before. Either way, record that call's usage receipt with
109
+ `tokens_measured=False` / `tokensMeasured: false` and the tokens you have, so your own usage report counts it in
110
+ `unmeasuredCalls` and does not show the lower bound as complete (DEC-0091, [usage](usage.md)).
109
111
 
110
112
  As a caller: send `request_meta(traceparent)` as your `_meta` (it names `fabric-interop/0.1` and `0.2`), act on every
111
113
  `fabric.job.get` answer as `job_caller_action(answer, now)` says, and resend only under
@@ -1,4 +1,4 @@
1
- # Lifecycle — supervisor, install, release layout, state
1
+ # Lifecycle: supervisor, install, release layout, state
2
2
 
3
3
  **This page is the rule** for anyone who builds, installs or runs a `fabric-service/0.1` service with this
4
4
  skill. Everything you must follow is stated here or in the public Fabric Agent Contract's
@@ -56,17 +56,17 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
56
56
  | Id | Rule | Here | Kit (Python / Node) |
57
57
  |---|---|---|---|
58
58
  | LC-01 | **Quit reaches exit within a deadline.** Every trigger (`SIGTERM`, `SIGINT`, logout, the app's own quit) ends the process: within 5 s idle, within 10 s with work in flight, then a hard exit. Nothing starts after quit begins | [Shutdown](#shutdown) | `Drain` / `Drain` |
59
- | LC-02 | **Every child has an owner, a deadline and a process group.** No subprocess without a timeout; on timeout or the parent's exit the whole group goes; no orphan survives. Locks and shared resources are released in `finally` | your own code | — |
59
+ | LC-02 | **Every child has an owner, a deadline and a process group.** No subprocess without a timeout; on timeout or the parent's exit the whole group goes; no orphan survives. Locks and shared resources are released in `finally` | your own code | none |
60
60
  | LC-03 | **A job is bounded and exclusive.** One copy holds `service.lock`; a scheduled job has a watchdog shorter than its interval, shares its lock with every manual entry point, bounds every network call and records each run's outcome. Long work is journalled before it starts | [One copy under a supervisor](#one-copy-under-a-supervisor) | `hold_single_instance` / `holdSingleInstance` |
61
- | LC-04 | **Credentials are silent after setup.** No background prompt; a credential read that is denied or absent is cached and backs off until a person acts | [Directories](#directories) (secret files 0600) | — |
62
- | LC-05 | **Identity is stable across updates.** The supervisor runs a stable executable inside a release directory, never an interpreter by a versioned package-manager path; signed builds are pinned by team and identifier | [launchd plist](#launchd-plist-generate-it-with-launchd_plist-or-launchdplist-both-kits-write-the-same-bytes) | — |
61
+ | LC-04 | **Credentials are silent after setup.** No background prompt; a credential read that is denied or absent is cached and backs off until a person acts | [Directories](#directories) (secret files 0600) | none |
62
+ | LC-05 | **Identity is stable across updates.** The supervisor runs a stable executable inside a release directory, never an interpreter by a versioned package-manager path; signed builds are pinned by team and identifier | [launchd plist](#launchd-plist-generate-it-with-launchd_plist-or-launchdplist-both-kits-write-the-same-bytes) | none |
63
63
  | LC-09 | **The footprint is declared.** The service's `AGENTS.md` names every resident process, its supervisor label, unit or task and its port, who starts each and who stops it | the descriptor and the supervisor file | `write_descriptor`, `launchd_plist`, `systemd_unit` / `systemdUnit`, `task_xml` / `taskXml` |
64
64
  | LC-11 | **An update is a lifecycle event.** An upgrade writes a new immutable release and restarts through the supervisor; the previous release is kept for rollback, older ones are pruned | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `prune_releases` / `pruneReleases` |
65
65
  | LC-12 | **Every file is bounded.** Logs rotate by size (5 × 5 MB), mode 0600, never in `/tmp`; temporary leftovers are swept at start | [Logs](#logs) | `RotatingLog`, `cap_stdout_log` / `RotatingLog`, `capStdoutLog` |
66
66
  | LC-14 | **The operator's intent wins; uninstall is symmetric.** No install or upgrade re-enables what the operator turned off; uninstall removes what install added (supervisor file, override, descriptor), and data goes only on an explicit purge | [Install, upgrade, uninstall](#install-upgrade-uninstall), [Linux and Windows](#linux-and-windows) | `launchd_install` / `launchdInstall`, `launchd_uninstall` / `launchdUninstall`; `systemd_install` / `systemdInstall`, `task_install` / `taskInstall` and their uninstalls |
67
67
  | LC-15 | **Builds clean up after themselves.** At most the current release and the one before it remain; the prune runs inside the installer, not later | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `prune_releases` / `pruneReleases` |
68
68
  | LC-21 | **A secret never travels in a command line.** Keys reach the process through its environment, a 0600 file or a vault, never argv; a supervisor file carries no secret | [launchd plist](#launchd-plist-generate-it-with-launchd_plist-or-launchdplist-both-kits-write-the-same-bytes), [Linux and Windows](#linux-and-windows) | the supervisor-file writers refuse a secret-shaped value |
69
- | LC-22 | **Work survives a restart.** Work started but not finished is journalled on disk, without secrets, before it starts; the restarted service takes it back or cleans it up | [Shutdown](#shutdown) | — (the service's own journal) |
69
+ | LC-22 | **Work survives a restart.** Work started but not finished is journalled on disk, without secrets, before it starts; the restarted service takes it back or cleans it up | [Shutdown](#shutdown) | none (the service's own journal) |
70
70
 
71
71
  ## launchd plist (generate it with `launchd_plist` or `launchdPlist`; both kits write the same bytes)
72
72
 
@@ -78,8 +78,8 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
78
78
  | `KeepAlive` | `true` | `{SuccessfulExit:false}` leaves a cleanly exited service down |
79
79
  | `ThrottleInterval` | `10` | bounds a crash loop |
80
80
  | `ExitTimeOut` | above the drain and its hard exit (15 s default; `Drain` ends by 10 s) | SIGKILL arrives after it, so it must never come first |
81
- | `ProcessType` | `Standard` — never `Background`, and no `Nice` > 0 or `LowPriorityIO` | a host probes the service and agents call it while the Mac is busy; a background job is starved for tens of seconds under load and every host reports an outage that never happened (the probe's `lifecycle.priority` rule fails it) |
82
- | `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets; `FABRIC_SERVICE_SUPERVISOR=launchd` | never a secret value — the plist is world-readable in backups; the supervisor marker is how the lock knows to back off (the probe's `lifecycle.supervisor-marker` rule fails a plist without it: a copy that finds the lock held would exit 75 at once into a respawn every `ThrottleInterval`) |
81
+ | `ProcessType` | `Standard`, never `Background`, and no `Nice` > 0 or `LowPriorityIO` | a host probes the service and agents call it while the Mac is busy; a background job is starved for tens of seconds under load and every host reports an outage that never happened (the probe's `lifecycle.priority` rule fails it) |
82
+ | `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets; `FABRIC_SERVICE_SUPERVISOR=launchd` | never a secret value: the plist is world-readable in backups; the supervisor marker is how the lock knows to back off (the probe's `lifecycle.supervisor-marker` rule fails a plist without it: a copy that finds the lock held would exit 75 at once into a respawn every `ThrottleInterval`) |
83
83
  | `StandardOutPath`/`StandardErrorPath` | `~/Library/Logs/<id>/service.log` | one place the host tails |
84
84
 
85
85
  ## Install, upgrade, uninstall
@@ -103,13 +103,13 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
103
103
  rewrites the plist, so the new release is what starts once they turn it back on, and
104
104
  answers `{"disabled": true, ...}` instead of the well-known document. `force_enable=True`
105
105
  is for an operator who asked for the service to be switched on. The service never
106
- implements these verbs itself — its CLI calls launchctl the same way.
106
+ implements these verbs itself; its CLI calls launchctl the same way.
107
107
 
108
108
  **Uninstall is symmetric.** `launchd_uninstall` boots the job out and **waits** until
109
109
  launchd reports it gone (it raises, removing nothing, if the job is still loaded at the
110
110
  timeout), removes the plist, and resets the override to `enabled`. launchctl has no verb
111
111
  that deletes an override, and a leftover `disabled` would make a later fresh install stay
112
- off. Tests use a throwaway label or a fake `launchctl` first on `PATH` — never the
112
+ off. Tests use a throwaway label or a fake `launchctl` first on `PATH`, never the
113
113
  operator's real jobs.
114
114
 
115
115
  ## One copy under a supervisor
@@ -118,7 +118,7 @@ Started by hand, a copy that finds `service.lock` held prints one sentence namin
118
118
  holder (or saying the holder is unknown, when its pid cannot be read) and exits 75 at once.
119
119
  Under launchd that exit is a respawn every `ThrottleInterval` (10 s), forever, and under
120
120
  systemd one every `RestartSec`. So a copy its supervisor started, and that says so with the
121
- **supervisor marker**, backs off in-process — 0.5 s doubling to 30 s, 300 s in all — takes over
121
+ **supervisor marker**, backs off in-process (0.5 s doubling to 30 s, 300 s in all), takes over
122
122
  if the holder leaves, and exits 75 only when the wait runs out: a few starts an hour instead of
123
123
  360. It touches nothing while it waits. Two installs sharing one data directory are still the
124
124
  defect to remove; the back-off only stops the loop from costing the machine.
@@ -136,8 +136,8 @@ never infers supervision from its parent process.
136
136
  `supervised` (with the manager), `manual` or `conflict`. Only an empty variable counts as no
137
137
  marker; a bare `--fabric-supervisor` (followed by nothing, by `--` or by another `--…` flag,
138
138
  none of which it consumes) names no manager.
139
- - Markers that disagree — the variable and the argument with different values, the argument
140
- twice, or a bare or empty argument beside the variable — exit 75 at once, before the lock and
139
+ - Markers that disagree (the variable and the argument with different values, the argument
140
+ twice, or a bare or empty argument beside the variable) exit 75 at once, before the lock and
141
141
  whether or not it is held, naming both markers.
142
142
  - A service's own argument parser accepts the flag in both spellings and ignores it: in Python,
143
143
  `add_supervisor_argument(parser)` (argparse otherwise exits 2 on an unknown flag); the sample
@@ -152,8 +152,8 @@ never infers supervision from its parent process.
152
152
  `"lifecycle": {"manager": "person", "app": "<the installed bundle or executable>"}`, `installedBy {by: "person", role,
153
153
  tool}` and no `origin`: it says the app is installed, never that it runs. The installer writes it; where there is none
154
154
  (a macOS app dragged into Applications), the app writes it on a start when `app_descriptor_action` /
155
- `appDescriptorAction` answers `write` — no descriptor yet, another version in `installedBy.tool`, or another path than
156
- `lifecycle.app` — and never from an App Translocation path. Its live origin and process id go in its own liveness file,
155
+ `appDescriptorAction` answers `write` (no descriptor yet, another version in `installedBy.tool`, or another path than
156
+ `lifecycle.app`), and never from an App Translocation path. Its live origin and process id go in its own liveness file,
157
157
  rewritten on every start and removed on quit; a caller that sends anything reads that file and checks the process.
158
158
  `write_descriptor` claims no port for it, and a reader that finds `lifecycle.app` gone treats the descriptor as stale.
159
159
 
@@ -179,8 +179,8 @@ anything the operator would miss belongs in data.
179
179
 
180
180
  ## Logs
181
181
 
182
- One structured log (JSON lines) rotated by size — `RotatingLog(dirs["logs"] / "service.jsonl")`,
183
- 5 × 5 MB by default, files 0600 in a 0700 directory — plus the launchd stdout file, which
182
+ One structured log (JSON lines) rotated by size (`RotatingLog(dirs["logs"] / "service.jsonl")`,
183
+ 5 × 5 MB by default, files 0600 in a 0700 directory), plus the launchd stdout file, which
184
184
  `cap_stdout_log` copies to `.1` and truncates at start once it passes 5 MB (launchd holds it
185
185
  open for append, so it is truncated in place, never renamed). Configure logging once, in the
186
186
  entry point. Never log a token, a cookie, a login code or a request body that may carry one.
@@ -189,7 +189,7 @@ entry point. Never log a token, a cookie, a login code or a request body that ma
189
189
 
190
190
  On `SIGTERM`: stop accepting new work, append a `service.stopping` event, drain
191
191
  in-flight work, persist what was interrupted so the next start resumes it, release the
192
- lock, exit 0 — within 10 s with work in flight (LC-01). `Drain` does the timing:
192
+ lock, exit 0, within 10 s with work in flight (LC-01). `Drain` does the timing:
193
193
 
194
194
  ```python
195
195
  drain = fs.Drain() # drain 8 s, hard exit 2 s later (EXIT_HARD_STOP, 70)
@@ -208,8 +208,8 @@ A host may run a lifecycle broker: one always-on per-user service that agents as
208
208
  stop and restart products, instead of launching or killing them themselves. A product it manages
209
209
  has to survive being driven from outside:
210
210
 
211
- - **Quit reaches exit through both doors.** A broker asks the platform's own quit first — on
212
- macOS `NSRunningApplication.terminate()`, the path Cmd+Q and the Dock take — and `SIGTERM` a few
211
+ - **Quit reaches exit through both doors.** A broker asks the platform's own quit first (on
212
+ macOS `NSRunningApplication.terminate()`, the path Cmd+Q and the Dock take) and `SIGTERM` a few
213
213
  seconds later. Handle both and exit inside the deadline (LC-01). A desktop app
214
214
  that answers `SIGTERM` by closing its window and staying alive is stopped late or not at all.
215
215
  - **A background launch stays in the background.** A broker opens the app non-activating
@@ -219,8 +219,8 @@ has to survive being driven from outside:
219
219
  - **Identity is the designated requirement.** A broker pins the app by team + bundle id, so an
220
220
  update signed by the same team keeps working and anything else is refused (LC-05). Never ship an
221
221
  ad-hoc or differently signed build to a machine that pins you.
222
- - **Starting is not ready.** Expose readiness the broker can probe without a person — the
223
- well-known document for a service, a process-level answer for a plain app — and keep it honest
222
+ - **Starting is not ready.** Expose readiness the broker can probe without a person (the
223
+ well-known document for a service, a process-level answer for a plain app) and keep it honest
224
224
  while the app is still starting.
225
225
 
226
226
  ## Linux and Windows
@@ -230,7 +230,7 @@ Both kits write and install the supervisor file the contract names (DEC-0032, se
230
230
  secret: the writers refuse a credential-shaped argument, environment name or value, and any
231
231
  control character (a line break would end a unit line early).
232
232
 
233
- **Linux — a systemd user unit.** `systemd_unit(unit, argv, working_directory=…)` /
233
+ **Linux: a systemd user unit.** `systemd_unit(unit, argv, working_directory=…)` /
234
234
  `systemdUnit(unit, argv, {workingDirectory})` writes `Restart=always`, `RestartSec=10`,
235
235
  `TimeoutStopSec` above the drain (15 s default), `KillMode=mixed`, `WantedBy=default.target`
236
236
  and `FABRIC_SERVICE_SUPERVISOR=systemd` (always, whatever the caller passed), quoting every argument (`%` → `%%`, `$` → `$$`).
@@ -247,7 +247,7 @@ descriptor says `"lifecycle": {"manager": "systemd", "unit": "<id>.<instance>.se
247
247
  A user unit runs while the person is logged in; one that must run without a session needs
248
248
  `loginctl enable-linger <user>`, which is the operator's decision, not the installer's.
249
249
 
250
- **Windows — a per-user Scheduled Task.** `task_xml(task, argv, working_directory=…)` /
250
+ **Windows: a per-user Scheduled Task.** `task_xml(task, argv, working_directory=…)` /
251
251
  `taskXml(task, argv, {workingDirectory})` writes a task with a logon trigger for the current
252
252
  user, `InteractiveToken` and `LeastPrivilege` (no administrator rights), restart on failure
253
253
  every minute, no execution time limit and `MultipleInstancesPolicy` `IgnoreNew`, its settings in the
@@ -269,16 +269,16 @@ starting a second copy. Measured (FAA-38, [report](https://github.com/passioncod
269
269
  that still exits 75 stays down until the next logon.
270
270
 
271
271
  **One copy on each system.** The Python kit takes `flock` (Linux) or a locked byte
272
- (Windows); the Node kit cannot, so it holds a name instead — an abstract unix socket on Linux,
272
+ (Windows); the Node kit cannot, so it holds a name instead: an abstract unix socket on Linux,
273
273
  a named pipe on Windows (`lock_name` / `lockName`), each gone with the process. The Python kit
274
274
  holds the same name beside its file lock, so the two kits exclude each other everywhere, and
275
275
  `check_service.py` reports the lock held when either is. The probe only looks (`/proc/net/unix`,
276
276
  `WaitNamedPipeW`); it never takes the name, so it cannot make a service that is starting fail.
277
277
 
278
278
  **Token files on Windows.** The privacy of `service.token` is its ACL (service.md *Windows
279
- token files*): the kits write a protected list — the user, SYSTEM and Administrators, set before the
279
+ token files*): the kits write a protected list: the user, SYSTEM and Administrators, set before the
280
280
  token's bytes go in, any entry already there dropped. On read, `windows_token_acl_problem` /
281
- `windowsTokenAclProblem` decide over what was read — the file's flags, its owner SID and each ACE
281
+ `windowsTokenAclProblem` decide over what was read: the file's flags, its owner SID and each ACE
282
282
  as `{type, sid, mask}` (DEC-0057, `FAC-SEM-079`, held to the contract's
283
283
  `fixtures/vectors/windows-token.json`): a reparse point, a directory, a real path outside the
284
284
  profile, another owner, an allow ACE with a non-zero mask for any other SID, an ACE of another type
@@ -8,7 +8,7 @@ Order matters: each step leaves the service answering.
8
8
  2. **Add the well-known route and the build identity.** Read-only, no risk.
9
9
  3. **Add the events view** over the existing log, then the login-code routes. Keep the
10
10
  service's old login path until the operator has switched.
11
- 4. **Move the lock to the top of startup** — before the startup hook that resumes jobs.
11
+ 4. **Move the lock to the top of startup**, before the startup hook that resumes jobs.
12
12
  Test: a second copy exits 75 and the first copy's running jobs are untouched.
13
13
  5. **Move state out of the code tree** if it lives there: copy to the OS data
14
14
  directory, switch the paths, keep the old tree read-only for one release, then
@@ -1,4 +1,4 @@
1
- # Packing, signing and verifying — `fabric-package/1`
1
+ # Packing, signing and verifying: `fabric-package/1`
2
2
 
3
3
  Contract DEC-0050 (format and digest), DEC-0051 (signatures), DEC-0071 and DEC-0074 (a consumer agent), rules
4
4
  `FAC-SEM-064`…`066` and `FAC-SEM-102`, all at this plugin's pin. The kit: `scripts/fabric_package.py` and
@@ -102,8 +102,13 @@ version `range`. The partner packs and signs every package with its own publishe
102
102
  machine pins that key and `verify` refuses a package signed by any other. One signed package serves every company: what
103
103
  differs per company (a display name, a brand, an address) is placement configuration, never a second package.
104
104
 
105
- Still to come, each waiting on its owner: trusting a partner's key for one organization only (contract CT-58), a
106
- partner's own space in a catalog and publishing to it (FAA-44, Vesta PA-03).
105
+ The company's estate record trusts the partner's key for that company only (DEC-0090): `publisherScopes[]` names the
106
+ key, the partner and the organization contexts, and `placement_trust` / `placementTrust` hand the key to a placement in
107
+ one of those contexts and to no other (FAC-SEM-122, [the estate record reference](estate-record.md)). The partner's own
108
+ agents call the company's agents with the partner innermost in the delegation chain ([delegation](delegation.md)).
109
+
110
+ Still to come, each waiting on its owner: a partner's own space in a catalog and publishing to it (FAA-44, Vesta
111
+ PA-03).
107
112
 
108
113
  ## What this version does not do
109
114
 
@@ -112,4 +117,5 @@ partner's own space in a catalog and publishing to it (FAA-44, Vesta PA-03).
112
117
  - **The organization's countersignature as a passkey assertion.** It waits for `org-countersign/1` and
113
118
  `org-authority/1`; this version's countersignature is a detached Ed25519 signature.
114
119
  - **Fabric recording packages by `agentRef`.** It waits for the ADR-0138 D2b amendment.
115
- - **`publish --to org`** (FAA-28).
120
+ - **`publish --to org`** (FAA-28; an organization cloud's import API is FAA-63, under FAA-28's rule that publishing to
121
+ an organization is the person's own act).
@@ -27,7 +27,7 @@ resolution = fp.resolve_model_profile(catalogue=catalogue, profiles=estate_and_a
27
27
  ```
28
28
 
29
29
  The first level that names a profile wins: **request, agent, binding or project, estate default**. The project's
30
- `modelClasses` narrow the profile's models — a model stays only if every class the catalogue gives it is allowed — and
30
+ `modelClasses` narrow the profile's models (a model stays only if every class the catalogue gives it is allowed) and
31
31
  never choose one. `open-vendor` is descriptive (DEC-0083): it is never checked against the policy, and a model whose
32
32
  only class is `open-vendor` does not stay. An empty result is `{"refused": "model-classes-empty", "policy": {id, revision}}`: refuse the launch
33
33
  and name the policy; never fall back to a model outside it. No level at all is `no-profile`; an id nobody offers is
@@ -37,17 +37,17 @@ and name the policy; never fall back to a model outside it. No level at all is `
37
37
 
38
38
  `profile_problems(kind, doc)` / `profileProblems(kind, doc)` returns the contract's findings for a `model-catalogue`
39
39
  (a model catalogued once; a provider never both ordered and ignored, a base slug covering its variants), a
40
- `model-profiles` set (an id once), a `runner-profile` (a subscription only in its vendor's runner — Claude in Claude
40
+ `model-profiles` set (an id once), a `runner-profile` (a subscription only in its vendor's runner: Claude in Claude
41
41
  Code, Kimi in Kimi Code, ChatGPT in Codex; a key route's allow-list is its own model profile; a key named by reference,
42
42
  never by value) and a recorded `profile-resolution` (it equals the order's answer). Every rule compares as the contract
43
- does — `===` (`1` and `true` differ, `null` is not absent) and its `jsonEqual` for a recorded resolution, where NaN
44
- equals nothing — and never raises on hostile JSON: a value whose own `toString` member is data is shown as its JSON.
43
+ does: `===` (`1` and `true` differ, `null` is not absent) and its `jsonEqual` for a recorded resolution, where NaN
44
+ equals nothing. It never raises on hostile JSON: a value whose own `toString` member is data is shown as its JSON.
45
45
 
46
46
  ## A lane that reads user content
47
47
 
48
- A model profile names what the lane it serves reads (DEC-0083, `reads`): `user-content` — what a member, a client or
49
- the public sends (messages, questions, images, reviews, screenshots, voice); `operator` — only what the estate's own
50
- operator or its agents write; `web` — pages fetched from the open web. Absent is "not declared", which stays valid.
48
+ A model profile names what the lane it serves reads (DEC-0083, `reads`): `user-content`: what a member, a client or
49
+ the public sends (messages, questions, images, reviews, screenshots, voice); `operator`: only what the estate's own
50
+ operator or its agents write; `web`: pages fetched from the open web. Absent is "not declared", which stays valid.
51
51
 
52
52
  A profile that reads `user-content` has an **open-vendor route** (FAC-SEM-112): with `fallback: none` (or any value the
53
53
  contract does not name) its first model is `open-vendor` in the catalogue; with `between-turns` at least one of its
@@ -83,7 +83,7 @@ and answers whether the runner is installed, which version answers and whether i
83
83
  catalogued binary found on `PATH`, only with the catalogued version argv (and `auth` argv), without a shell, within
84
84
  the entry's timeout (at most 5 s); an entry outside these rules is refused and nothing runs. `model_usable(model,
85
85
  catalogue, policy)` names why a model cannot be used here: not catalogued, a class the project does not allow, or
86
- only `open-vendor`, which admits nothing — the same admission the resolution applies.
86
+ only `open-vendor`, which admits nothing: the same admission the resolution applies.
87
87
 
88
88
  `probe` is the contract's closed probe result for that runner, the one Fabric's walk records (DEC-0026):
89
89
  `not-installed` (no catalogued binary on `PATH`), `not-responding` (the version probe did not answer in its bound,
@@ -102,12 +102,12 @@ contract's `routeRules` finding for finding:
102
102
  | `kind` | value | rules |
103
103
  |---|---|---|
104
104
  | `runner-route` | the route | one candidate per runner kind (FAC-SEM-028); a candidate can attach or spawn (029) |
105
- | `route-bundle` | `{route, admissions, binding?, catalogue?}` | every candidate's provider revision admitted for the capability by id, revision and hash (030); the binding agrees — project, capability, local-runner, a candidate as provider, this route as `runnerRoute` (032); every kind and named drive is in the runner catalogue (033) |
105
+ | `route-bundle` | `{route, admissions, binding?, catalogue?}` | every candidate's provider revision admitted for the capability by id, revision and hash (030); the binding agrees (project, capability, local-runner, a candidate as provider, this route as `runnerRoute`) (032); every kind and named drive is in the runner catalogue (033) |
106
106
  | `route-event` | `{route, event}` | the event names this route, project and capability; candidates by position; the probes cover every candidate above the selection in order (all for an exhausted walk, none for a sticky relaunch); attach, spawn, `no-held-session` and `spawn-failed` obey the session policy; `held` only under `exhausted: hold`; `preferred-available` only under `recovery: reprobe`, moving up (034) |
107
107
 
108
108
  A probe result in an event is one of `PROBE_RESULTS`; a host never appends a runner the route does not name.
109
109
 
110
110
  ## Usage by route
111
111
 
112
- `make_usage_receipt(..., route="subscription"|"key")` records how the call was paid — the operator's own login in its
112
+ `make_usage_receipt(..., route="subscription"|"key")` records how the call was paid: the operator's own login in its
113
113
  vendor's runner, or an issued key. The usage report keeps the rows of one model on two routes apart.
@@ -1,6 +1,6 @@
1
- # fabric-service/0.1 — wire reference
1
+ # fabric-service/0.1: wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `07042181bfae2a481aa0da1a5651f24c76669c48`
3
+ Pinned to `fabric-agent-contract` commit `c55f2d5a11c9a0197e958e87212fca7ae1154769`
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;
@@ -14,13 +14,13 @@ this page is the working summary. Extension key:
14
14
 
15
15
  ## Contents
16
16
 
17
- - Descriptor — written by the installer
17
+ - Descriptor: written by the installer
18
18
  - Well-known document
19
19
  - Events page
20
20
  - Login code and the operator session
21
21
  - Semantic rules a host applies
22
22
 
23
- ## Descriptor — written by the installer
23
+ ## Descriptor: written by the installer
24
24
 
25
25
  Directory: macOS `~/Library/Application Support/ai.passioncode.fabric/services/`,
26
26
  Linux `${XDG_DATA_HOME:-~/.local/share}/passioncode-fabric/services/`, Windows
@@ -33,13 +33,13 @@ File `<id>.<instance>.json`, mode 0600 (on Windows a protected ACL), atomic.
33
33
  | `id`, `instance` | `^[a-z][a-z0-9-]{1,62}$`, `^[a-z][a-z0-9-]{0,31}$` (default `default`); the pair is unique per machine |
34
34
  | `name`, `summary` | ≤ 80 and ≤ 200 characters |
35
35
  | `origin` | `http://127.0.0.1:<port>`; the port is a machine-wide claim. Optional for an app's descriptor only (`manager: "person"`, DEC-0069): its live origin and process id are in the app's own liveness file, and a descriptor without an origin claims no port (`FAC-SEM-010`) |
36
- | `auth` | `tokenFile` (0600; on Windows a protected ACL — owner and every granting entry the current user, SYSTEM or Administrators: service.md *Windows token files*, DEC-0032, DEC-0033); `header` default `Authorization` with `scheme` `Bearer`; any other header carries the raw token with `scheme: "none"` |
36
+ | `auth` | `tokenFile` (0600; on Windows a protected ACL: owner and every granting entry the current user, SYSTEM or Administrators: service.md *Windows token files*, DEC-0032, DEC-0033); `header` default `Authorization` with `scheme` `Bearer`; any other header carries the raw token with `scheme: "none"` |
37
37
  | `lifecycle` | `manager` `launchd` (then `label` and `plist`), `systemd` (then `unit`, a `systemctl --user` unit), `task-scheduler` (then `task`, a per-user Scheduled Task), `node` (Project Observatory's node supervises it as its child, with no supervisor file, DEC-0061), `person` (an app a person installs and starts, then `app`, the installed app, DEC-0069) or `none`; `unit` and `task` are refused beside any other manager (DEC-0032), `label` and `plist` beside any but `launchd` (DEC-0057), `app` beside any but `person`. Both kits and the probe accept all six; both kits write the unit and the task (`systemd_unit`, `task_xml` / `systemdUnit`, `taskXml`). An app's descriptor says it is installed, never that it runs: the probe checks `app` is still there (`descriptor.app-installed`) and reads no well-known document; a reader that finds the app gone treats the descriptor as stale (`app_descriptor_stale` / `appDescriptorStale`), and an app that writes its own descriptor rewrites it only when `app_descriptor_action` / `appDescriptorAction` says `write` |
38
38
  | `paths` | `data` (required), `logs[]`, optional `config`, `cache` |
39
- | `commands` | only `doctor` and `update`, argument arrays whose first item is an absolute or `~/` path (Windows: also `C:\…`, `C:/…` or `~\…`, never a network share — DEC-0032) |
39
+ | `commands` | only `doctor` and `update`, argument arrays whose first item is an absolute or `~/` path (Windows: also `C:\…`, `C:/…` or `~\…`, never a network share; DEC-0032) |
40
40
  | `source.repository`, `fabricManifest` | optional |
41
- | `installedAt`, `installedBy` | when, and who installed it: the installer and its version as a string, or `{by, role?, tool}` (DEC-0061) — `by` `person` (with `role` `operator` or `administrator`), `node` or `organization`; `role` for a person only; `tool` the installer and its version. It names a role, never a person. An app's descriptor uses the object form with `by: "person"`. `register_remote` / `registerRemote` and the sample write the object form |
42
- | `runner` | optional `{kind?, credentialKind}` (DEC-0061): `credentialKind` `subscription`, `key`, `platform-metered` or `none` — a kind, never a credential (`FAC-SEM-086` refuses a credential-shaped value here or in `installedBy`); a subscription names its vendor's runner (`claude-code`, `kimi-code`, `codex`) |
41
+ | `installedAt`, `installedBy` | when, and who installed it: the installer and its version as a string, or `{by, role?, tool}` (DEC-0061): `by` `person` (with `role` `operator` or `administrator`), `node` or `organization`; `role` for a person only; `tool` the installer and its version. It names a role, never a person. An app's descriptor uses the object form with `by: "person"`. `register_remote` / `registerRemote` and the sample write the object form |
42
+ | `runner` | optional `{kind?, credentialKind}` (DEC-0061): `credentialKind` `subscription`, `key`, `platform-metered` or `none`: a kind, never a credential (`FAC-SEM-086` refuses a credential-shaped value here or in `installedBy`); a subscription names its vendor's runner (`claude-code`, `kimi-code`, `codex`) |
43
43
  | `peer` | optional `{endpoint, custody, residency, maxDataClass}` (DEC-0061): the service is offered to others. `endpoint` `https://<dns-name>[:<port>][/<path>]`, never a reserved name (`FAC-SEM-085`); `custody` `organization-managed` or `personal`; `residency` 1–32 ISO 3166-1 alpha-2 codes, `EU` admitted, user-assigned codes refused; `maxDataClass` `public` < `internal` < `confidential` < `restricted`. An offer needs `runner` and never runs on a subscription. A host sends a request from anyone but the operator only when `peer_refusal` / `peerRefusal` answers null (`FAC-SEM-088`); usage rows may take `route: "platform-metered"` |
44
44
 
45
45
  ## Identifier equality (DEC-0076)
@@ -53,7 +53,7 @@ and a value that is not a string is compared as it is. The kits compare by it in
53
53
  events), FAC-SEM-076 (estate grants and their revocations) and FAC-SEM-014 (a provider entry's `providerId`), and they
54
54
  write the identifiers they scaffold in normal form.
55
55
 
56
- ## Well-known document — `GET /.well-known/fabric-service`
56
+ ## Well-known document: `GET /.well-known/fabric-service`
57
57
 
58
58
  No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
59
59
 
@@ -83,8 +83,8 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
83
83
  - `build` needs `commit` or `digest` (`sha256:<64 hex>`).
84
84
  - `summary`: at most six tiles; `attention: true` counts toward the host's badge.
85
85
  - `surfaces.events` is required; `dashboard` and `mcp` when the service has them; `usage`
86
- (DEC-0021) when the service reports its spend — see [the usage reference](usage.md);
87
- `operatorChannel.path` (DEC-0034) when the service has an operator channel — its status
86
+ (DEC-0021) when the service reports its spend (see [the usage reference](usage.md));
87
+ `operatorChannel.path` (DEC-0034) when the service has an operator channel; its status
88
88
  document follows the contract's `operator-channel-status.schema.json` and is token-protected
89
89
  like the events feed. The kits do not serve it yet.
90
90
  - `extensions` MAY carry the embedded dashboard declaration under
@@ -98,7 +98,7 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
98
98
  token is not an MCP credential: a host and the probe never call `/mcp` with it as a caller.
99
99
  The kits verify them with `fabric_delegation` / `fabric-delegation.mjs` ([delegation](delegation.md)).
100
100
 
101
- ## Events page — `GET /fabric/v1/events?after=<cursor>&limit=<n>`
101
+ ## Events page: `GET /fabric/v1/events?after=<cursor>&limit=<n>`
102
102
 
103
103
  Token required. `limit` default 50, maximum 200. Without `after`: the newest `limit`,
104
104
  ascending. `cursor` is the last id returned, or the `after` you sent when the page is
@@ -134,7 +134,7 @@ reading it further (HTTP 400, or an MCP `isError` result's `structuredContent`);
134
134
  own fault `no-accepted-revisions`. `PROTOCOL_REVISIONS` is the contract's register; `check_service.py` reads
135
135
  `well-known.protocols`.
136
136
 
137
- ## Login code and the operator session — `POST /fabric/v1/login-code`
137
+ ## Login code and the operator session: `POST /fabric/v1/login-code`
138
138
 
139
139
  Token required → `{ "url": "/fabric/v1/login?code=<16–256 url-safe chars>", "expiresAt": "…" }`,
140
140
  single use, at most 120 s, recorded as used before it is honoured. `GET` that URL →
@@ -151,4 +151,4 @@ ended session answers `401` on the dashboard page.
151
151
  | `FAC-SEM-010` | no two descriptors claim one port or one `id.instance` |
152
152
  | `FAC-SEM-011` | `ready` carries no degraded source |
153
153
  | `FAC-SEM-012` | commands start with an absolute or `~/` executable |
154
- | `FAC-SEM-025` | a usage report adds up; an all-unpriced row costs `null`, not `0` (DEC-0021) |
154
+ | `FAC-SEM-025` | a usage report adds up; an all-unpriced row costs `null`, not `0` (DEC-0021); unmeasured calls are within calls and a day's are its models' sum (DEC-0091) |
@@ -1,4 +1,4 @@
1
- # The remote placement — online agents and dashboards
1
+ # The remote placement: online agents and dashboards
2
2
 
3
3
  Normative source: `fabric-agent-contract` `docs/specification/service.md` → *Remote placement*,
4
4
  DEC-0019, at the commit this skill pins. This page is the how-to.
@@ -7,7 +7,7 @@ DEC-0019, at the commit this skill pins. This page is the how-to.
7
7
 
8
8
  It is, when it runs outside the operator's computer and the operator wants it in Fabric beside
9
9
  the local services: a hosted dashboard, an agent on a server, a bot's web console. It keeps the
10
- same objects — well-known document, events feed, login code — at the same paths.
10
+ same objects (well-known document, events feed, login code) at the same paths.
11
11
 
12
12
  ## The descriptor
13
13
 
@@ -47,7 +47,7 @@ watch the gate fail.
47
47
  the same value is written on the operator's computer by `registerRemote`. Compare in constant
48
48
  time (`tokenMatches`).
49
49
  - **Session key**: 32 random bytes, a separate secret; rotating it signs every operator out.
50
- - **Codes and sessions**: `MemoryCodeStore` — a restart forgets every outstanding code, so none
50
+ - **Codes and sessions**: `MemoryCodeStore`: a restart forgets every outstanding code, so none
51
51
  can be replayed, and every session record, so a deploy signs the operator out (DEC-0039: the
52
52
  server keeps each session's last use and logout). The host signs in again on the page's `401`.
53
53
  To keep sessions across a deploy, pass a durable store (any object with `load()` and `save()`).
@@ -68,7 +68,7 @@ reach the service, and still need its own `403`.
68
68
 
69
69
  | Situation | Host |
70
70
  |---|---|
71
- | `401` on the well-known document | `down` — the service refused the token; never `foreign` |
71
+ | `401` on the well-known document | `down`: the service refused the token; never `foreign` |
72
72
  | answer names another `id.instance` | `foreign`; the token is not sent again until the descriptor changes |
73
73
  | certificate invalid or for another name | `down`, the reason names TLS |
74
74
  | redirect | not followed; `down` |
@@ -1,8 +1,8 @@
1
- # Settings backups — what the operator decided survives the data directory (DEC-0025)
1
+ # Settings backups: what the operator decided survives the data directory (DEC-0025)
2
2
 
3
3
  Fabric Agent Contract DEC-0025, `docs/specification/service.md` → *Settings backup*, schema
4
4
  `schemas/settings-backup.schema.json`, rule `FAC-SEM-035`. A service whose store holds what the operator
5
- decided — which projects it serves, how each is bound, which secret slot each binding uses, their overrides —
5
+ decided (which projects it serves, how each is bound, which secret slot each binding uses, their overrides)
6
6
  SHOULD keep settings backups, so a lost, purged or wrongly migrated data directory costs the operator nothing.
7
7
 
8
8
  ## Where
@@ -61,7 +61,7 @@ plan = fs.restore_plan(doc, {"projects": {"columns": [...], "key": ["id"], "requ
61
61
  # plan["tables"]["projects"]: insert, present, droppedColumns, defaultedColumns, missingRequired, refused
62
62
  ```
63
63
 
64
- - **Automatic restore** only on a *fresh* database — no domain records **and** the service has never started —
64
+ - **Automatic restore** only on a *fresh* database (no domain records **and** the service has never started),
65
65
  so a restart never undoes a deletion. Insert `plan[...]["insert"]` and add every row the database refuses to
66
66
  `refused` with its key and reason.
67
67
  - **Manual restore** shows the plan first; applying it is the operator's act, never an agent's.
@@ -16,13 +16,13 @@
16
16
  the estate's delegation tokens; declare `auth.delegation {issuer, jwks}` in the well-known
17
17
  document. The probe treats it like `own` and checks the declaration
18
18
  (`auth.delegation-declared`). The kits verify the tokens: put `delegation_gate` /
19
- `delegationGate` in front of the tools — see [the delegation reference](delegation.md).
19
+ `delegationGate` in front of the tools (see [the delegation reference](delegation.md)).
20
20
  - Register it into a client config with the service's own command
21
21
  (`<tool> mcp-register`), which writes `{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{...}}`
22
22
  atomically, aborts if the file changed meanwhile, and keeps one 0600 backup. The token
23
- goes in `headers`, never in the URL — every log that records a URL prints its query
23
+ goes in `headers`, never in the URL: every log that records a URL prints its query
24
24
  string. An HTTP registration starts nothing per session: every client session talks to the
25
- one resident service (LC-19). A stdio server, by contrast, is a process per session — keep
25
+ one resident service (LC-19). A stdio server, by contrast, is a process per session; keep
26
26
  one only at project scope.
27
27
  - Ship server `instructions` that tell a caller where to start and that every answer
28
28
  carries `degraded`.
@@ -39,7 +39,7 @@ a sign-in link only when stdout is a terminal.
39
39
  ## A2A for remote agents
40
40
 
41
41
  Remote reach goes through A2A `1.0` over HTTPS or an authenticated gateway in front of
42
- MCP — never by binding a LAN address. A tunnel (Tailscale Serve and the like) is an
42
+ MCP, never by binding a LAN address. A tunnel (Tailscale Serve and the like) is an
43
43
  outward surface with its own token lifetime; watch it like a service, because nothing
44
44
  else will notice it expire.
45
45
 
@@ -50,7 +50,7 @@ else will notice it expire.
50
50
  - Compared in constant time (`token_matches`).
51
51
  - Rotation is an operator act: write a new file, restart, re-run `mcp-register`.
52
52
  - Same-user processes can read it. The protocol defends against web pages and
53
- mistakes, not against hostile code running as the operator — say so in SECURITY.md.
53
+ mistakes, not against hostile code running as the operator; say so in SECURITY.md.
54
54
 
55
55
  ## Dashboard session
56
56