@passioncode-ai/passioncode 0.2.17 → 0.2.19

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 (117) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +13 -13
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +1 -1
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -4
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +56 -45
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -1
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +64 -0
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +264 -0
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +12 -12
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +4 -4
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +1 -1
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -4
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +13 -11
  26. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +11 -5
  27. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +6 -6
  28. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +8 -6
  29. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +128 -0
  30. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +26 -26
  31. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -1
  32. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +3 -2
  33. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +10 -10
  34. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +13 -13
  35. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +4 -4
  36. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +3 -3
  37. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -5
  38. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +7 -4
  39. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -209
  40. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
  41. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +1 -1
  42. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +286 -0
  43. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
  44. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
  45. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
  46. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +840 -0
  47. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
  48. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
  49. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -16
  50. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
  51. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +15 -46
  52. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
  53. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +8 -67
  54. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
  55. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
  56. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -2
  57. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
  58. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +202 -0
  59. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
  60. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
  61. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +3 -13
  62. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
  63. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
  64. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
  65. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
  66. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2521
  67. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
  68. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +400 -0
  69. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
  70. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
  71. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +402 -0
  72. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
  73. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +78 -0
  74. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
  75. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
  76. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +902 -0
  77. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
  78. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -38
  79. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +289 -0
  80. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +16 -181
  81. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
  82. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +39 -400
  83. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +137 -0
  84. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
  85. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +1 -6
  86. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
  87. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
  88. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
  89. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
  90. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
  91. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +22 -44
  92. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
  93. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
  94. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +31 -38
  95. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
  96. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3430
  97. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
  98. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +412 -0
  99. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
  100. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
  101. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1526 -0
  102. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +1 -1
  103. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +33 -33
  104. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +1 -1
  105. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
  106. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +27 -27
  107. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +31 -31
  108. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +9 -9
  109. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +8 -8
  110. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +116 -1392
  111. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +270 -0
  112. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +508 -0
  113. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +265 -0
  114. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +155 -0
  115. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +335 -0
  116. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -0
  117. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
@@ -1,4 +1,4 @@
1
- # fabric-service/0.1 — wire reference
1
+ # fabric-service/0.1: wire reference
2
2
 
3
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
@@ -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
 
@@ -1,4 +1,4 @@
1
- # Usage report — what the agent spent (DEC-0021)
1
+ # Usage report: what the agent spent (DEC-0021)
2
2
 
3
3
  Fabric Agent Contract DEC-0021, `docs/specification/service.md` → *Usage report*, schema
4
4
  `schemas/service-usage.schema.json`. Optional. A service that calls paid models or tools should
@@ -29,6 +29,8 @@ ledger.record(receipt) # fs.JsonlUsageLedger(data / "usage.j
29
29
  ledger.record(k.makeUsageReceipt('openrouter', model, { inputTokens, outputTokens, costUsd: usage.cost ?? null }));
30
30
  ```
31
31
 
32
+ - **OpenRouter through the kit.** `fabric_openrouter.Client` records these receipts itself, one per answered call,
33
+ and checks a spend ceiling before every attempt ([connectors](connectors.md)).
32
34
  - **The provider's own numbers.** Take tokens from the response's usage block, not from a local
33
35
  estimate. The cost is `provider` when the provider reported the charge (OpenRouter `usage.cost`,
34
36
  a billing API) and `price-list` when you computed it from a published price list.
@@ -39,7 +41,8 @@ ledger.record(k.makeUsageReceipt('openrouter', model, { inputTokens, outputToken
39
41
  - **Unknown tokens are named too** (DEC-0091). When the provider gave no token count (a stream cut off, a local
40
42
  model without a counter), pass `tokens_measured=False` / `tokensMeasured: false` with the tokens you have (often 0).
41
43
  The receipt carries `tokensMeasured: false`, and the report counts the call in `unmeasuredCalls` on its day and model
42
- row, never more than `calls`; a host then shows the token totals as a lower bound.
44
+ row, never more than `calls`; a host then shows the token totals as a lower bound. A job whose usage you send as
45
+ `null` or `partial` ([interop](interop.md)) is such a call.
43
46
  - **No content.** A receipt carries no prompt, output, caller or user id. The kits accept only the
44
47
  fields above.
45
48
  - If the service already keeps a ledger (a jobs table with token counts), build the receipts from
@@ -63,7 +66,7 @@ left out.
63
66
  it, and the host only shows it.
64
67
 
65
68
  **Every limit (DEC-0027).** Pass `budgets` to list every spending limit the service applies,
66
- enforced or not, so the spend view shows all of them — the operator asked for that on 2026-10-07:
69
+ enforced or not, so the spend view shows all of them; the operator asked for that on 2026-10-07:
67
70
 
68
71
  ```python
69
72
  ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budgets=[
@@ -78,7 +81,7 @@ ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budget
78
81
  way `budget` is, so `budget` and its twin in `budgets` never disagree. A per-order limit
79
82
  (`per_job`, `approval`) gets `spentUsd: null`. Every other limit states its own spend.
80
83
  - `scope` is `machine`, `project`, `pool` or `job`; a `project` or `pool` limit names its
81
- `subject`. A limit has at most one window — `period`, `windowSeconds` or `since` — and it agrees
84
+ `subject`. A limit has at most one window (`period`, `windowSeconds` or `since`), and it agrees
82
85
  with the kind (`daily`: the day or 86400 s; `monthly`: the month or 28–31 days; `velocity`: a
83
86
  rolling window). `tripped: true` (work stopped by it now) needs `enforced: true`. An unknown
84
87
  kind is shown generically, never refused.
@@ -57,74 +57,16 @@ import fabric_interop as fi # noqa: E402
57
57
  Result = Dict[str, str]
58
58
 
59
59
 
60
-
61
- def plist_problems(plist: dict, label: object, token: object = None) -> list:
62
- """What makes a service's launchd job unfit to stay up: identity, restart, secrets."""
63
- problems = []
64
- if plist.get("Label") != label:
65
- problems.append("Label %r" % plist.get("Label"))
66
- if plist.get("RunAtLoad") is not True:
67
- problems.append("RunAtLoad is not true")
68
- if plist.get("KeepAlive") is not True:
69
- problems.append("KeepAlive is %r, not true" % plist.get("KeepAlive"))
70
- for key, value in (plist.get("EnvironmentVariables") or {}).items():
71
- if re.search(r"(TOKEN|SECRET|PASSWORD|KEY)$", key) and not key.endswith("_FILE"):
72
- problems.append("secret-like variable %s in the plist" % key)
73
- if token and token in str(value):
74
- problems.append("the service token itself is in the plist")
75
- return problems
76
-
77
-
78
- def marker_problem(plist: dict) -> Optional[str]:
79
- """What is wrong with the job's supervisor marker (DEC-0057; FAA-05), read the way the copy it starts reads it.
80
-
81
- Without ``FABRIC_SERVICE_SUPERVISOR=launchd`` (or ``--fabric-supervisor launchd``) a copy that finds the
82
- lock held exits 75 at once, and KeepAlive starts it again every ThrottleInterval."""
83
- env = plist.get("EnvironmentVariables")
84
- env = {str(k): str(v) for k, v in env.items()} if isinstance(env, dict) else {}
85
- argv = plist.get("ProgramArguments")
86
- argv = [str(a) for a in argv] if isinstance(argv, list) else []
87
- marker = fs.supervisor_marker(env, argv)
88
- if marker["kind"] == "supervised" and marker["manager"] == "launchd":
89
- return None
90
- fix = ("a copy that finds the lock held exits 75 at once and KeepAlive respawns it every ThrottleInterval; "
91
- "regenerate the plist with launchd_plist / launchdPlist and reinstall")
92
- if marker["kind"] == "conflict":
93
- return "%s — %s" % (fs.marker_conflict_message(marker), fix)
94
- if marker["kind"] == "supervised":
95
- return "the marker names %s, not launchd — %s" % (marker["manager"], fix)
96
- if marker.get("unreadable") is not None:
97
- return "the marker %r names no supervisor — %s" % (marker["unreadable"], fix)
98
- return "no %s=launchd — %s" % (fs.SUPERVISOR_ENV, fix)
99
-
100
-
101
- def priority_problems(plist: dict) -> list:
102
- """A service that answers hosts and agents must not be scheduled as background work."""
103
- out = []
104
- if plist.get("ProcessType", "Standard") in ("Background", "Adaptive"):
105
- out.append("ProcessType is %s" % plist.get("ProcessType"))
106
- if isinstance(plist.get("Nice"), int) and plist["Nice"] > 0:
107
- out.append("Nice is %d" % plist["Nice"])
108
- if plist.get("LowPriorityIO") is True or plist.get("LowPriorityBackgroundIO") is True:
109
- out.append("low-priority I/O")
110
- return out
111
-
112
-
113
- # A platform router (Heroku, Fly, Render, a CDN) routes by Host, so a request naming a foreign
114
- # Host never reaches an online service: the router itself answers, typically 404 or 421. That is
115
- # still a refusal — the service disclosed nothing — so for a remote placement the host check
116
- # accepts it, as long as the body is not the well-known document. Every other guard request names
117
- # the right Host, reaches the service, and must be the service's own 403.
118
- PLATFORM_HOST_REFUSALS = (400, 404, 421)
119
-
120
-
121
- def guard_verdict(rule: str, remote: bool, status: int, body: bytes) -> Tuple[str, str]:
122
- if status == 403:
123
- return "PASS", "HTTP 403"
124
- disclosed = b"fabric-service/" in (body or b"")
125
- if rule == "network.host-check" and remote and status in PLATFORM_HOST_REFUSALS and not disclosed:
126
- return "PASS", "HTTP %d from the platform router: a foreign Host never reaches the service" % status
127
- return "FAIL", "HTTP %d%s" % (status, ", and the body is the well-known document" if disclosed else "")
60
+ from check_service_rules import PLATFORM_HOST_REFUSALS # noqa: E402,F401 - moved (FAA-65)
61
+ from check_service_rules import plist_problems # noqa: E402,F401 - moved (FAA-65)
62
+ from check_service_rules import marker_problem # noqa: E402,F401 - moved (FAA-65)
63
+ from check_service_rules import priority_problems # noqa: E402,F401 - moved (FAA-65)
64
+ from check_service_rules import guard_verdict # noqa: E402,F401 - moved (FAA-65)
65
+ from check_service_rules import board_responder_verdict # noqa: E402,F401 - moved (FAA-65)
66
+ from check_service_rules import session_lifetime_problem # noqa: E402,F401 - moved (FAA-65)
67
+ from check_service_rules import _semver # noqa: E402,F401 - moved (FAA-65)
68
+ from check_service_rules import usage_problems # noqa: E402,F401 - moved (FAA-65)
69
+ from check_service_rules import same_repository # noqa: E402,F401 - moved (FAA-65)
128
70
 
129
71
 
130
72
  class _PinnedHTTPS(http.client.HTTPSConnection):
@@ -729,7 +671,7 @@ class Probe:
729
671
  marker or "%s: %s=launchd, so a copy that finds the lock held backs off" % (plist_path.name, fs.SUPERVISOR_ENV))
730
672
  slow = priority_problems(plist)
731
673
  self.add("lifecycle.priority", "FAIL" if slow else "PASS",
732
- "; ".join(slow) + " — macOS may starve the service under load and hosts then see an outage; use ProcessType Standard"
674
+ "; ".join(slow) + ". macOS may starve the service under load and hosts then see an outage; use ProcessType Standard"
733
675
  if slow else "%s: scheduled as a standard process" % plist_path.name)
734
676
  if sys.platform != "darwin" or not shutil.which("launchctl"):
735
677
  self.add("lifecycle.one-copy", "NOT_RUN", "launchctl is not available")
@@ -839,146 +781,6 @@ class Probe:
839
781
  return self.results
840
782
 
841
783
 
842
- def board_responder_verdict(wk: Any, kit: Optional[str] = None) -> Tuple[str, str]:
843
- """board.responder: NOT_RUN when the well-known document declares no fabric-project-comms revision; FAIL when it
844
- declares one this kit does not read or the kit's fixture conversation finds a problem; PASS otherwise. `kit` is the
845
- service's own copy (a .py or .mjs path); by default the kit beside this probe."""
846
- protocols = wk.get("protocols") if isinstance(wk, dict) else None
847
- declared = [p for p in protocols if isinstance(p, str) and p.startswith("fabric-project-comms/")] if isinstance(protocols, list) else []
848
- if not declared:
849
- return "NOT_RUN", "the service declares no board participation (no fabric-project-comms revision in protocols)"
850
- accepted = fs.accepted_revisions("fabric-project-comms")
851
- unread = [p for p in declared if "error" in fs.negotiate_revision(p, accepted)]
852
- if unread:
853
- return "FAIL", "declares %s, which this kit does not read (it reads %s)" % (", ".join(unread), ", ".join(accepted))
854
- try:
855
- if kit and kit.endswith(".mjs"):
856
- if not shutil.which("node"):
857
- return "NOT_RUN", "node is not installed to run %s's fixture conversation" % kit
858
- url = Path(kit).resolve().as_uri()
859
- out = subprocess.run(["node", "--input-type=module", "-e",
860
- "import(process.env.FABRIC_BOARD_KIT).then(async (m) => console.log(JSON.stringify(await m.selfCheck())))"],
861
- env=dict(os.environ, FABRIC_BOARD_KIT=url), capture_output=True, text=True, timeout=60)
862
- problems = json.loads(out.stdout.strip().splitlines()[-1]) if out.returncode == 0 and out.stdout.strip() else ["the Node kit did not run: %s" % out.stderr.strip()[:200]]
863
- else:
864
- import importlib.util
865
- path = Path(kit) if kit else Path(__file__).resolve().parent / "fabric_board.py"
866
- spec = importlib.util.spec_from_file_location("fabric_board_under_test", str(path))
867
- module = importlib.util.module_from_spec(spec)
868
- sys.path.insert(0, str(path.resolve().parent))
869
- spec.loader.exec_module(module) # type: ignore[union-attr]
870
- problems = module.self_check()
871
- except Exception as exc: # noqa: BLE001 - a kit that cannot run its conversation fails the rule
872
- return "FAIL", "the kit's fixture conversation did not run: %s" % exc.__class__.__name__
873
- if problems:
874
- return "FAIL", "; ".join(problems)
875
- return "PASS", "declares %s; the kit%s answers the fixture conversation (whole, fenced, no epoch)" % (
876
- ", ".join(declared), " at %s" % kit if kit else " beside this probe")
877
-
878
-
879
- def session_lifetime_problem(set_cookie: str, now: Optional[float] = None) -> Optional[str]:
880
- """DEC-0039: a session cookie set at sign-in lives at most 7 days. Max-Age wins over Expires (RFC 6265
881
- §5.3); a cookie with neither ends with the browser session. None when it is within the bound."""
882
- attrs = {}
883
- for part in set_cookie.split(";")[1:]:
884
- name, _, value = part.strip().partition("=")
885
- attrs[name.strip().lower()] = value.strip()
886
- bound = fs.OPERATOR_SESSION_MAX_SECONDS
887
- if "max-age" in attrs:
888
- if not re.fullmatch(r"-?\d+", attrs["max-age"]):
889
- return "the session cookie's Max-Age %r is not a number" % attrs["max-age"]
890
- if int(attrs["max-age"]) > bound:
891
- return "the session cookie's Max-Age %s outlives 7 days (%d s, DEC-0039)" % (attrs["max-age"], bound)
892
- return None
893
- if "expires" in attrs:
894
- try:
895
- at = email.utils.parsedate_to_datetime(attrs["expires"]).timestamp()
896
- except (TypeError, ValueError, IndexError, OverflowError):
897
- return "the session cookie's Expires %r is not a date" % attrs["expires"]
898
- if at - (time.time() if now is None else now) > bound:
899
- return "the session cookie's Expires %s outlives 7 days (DEC-0039)" % attrs["expires"]
900
- return None
901
-
902
-
903
- def _semver(text: str) -> Tuple[int, int, int]:
904
- m = re.match(r"^(\d+)\.(\d+)\.(\d+)", text or "")
905
- return (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else (0, 0, 0)
906
-
907
-
908
- def usage_problems(report: Any, service_id: str, instance: str) -> List[str]:
909
- """What makes this not a DEC-0021 usage report of the service: shape, identity and the sums of
910
- FAC-SEM-025. An unknown cost must be null; a day's totals are the sums of its models."""
911
- if not isinstance(report, dict):
912
- return ["not an object"]
913
- out: List[str] = []
914
- if report.get("protocol") != fs.PROTOCOL:
915
- out.append("protocol is %r" % report.get("protocol"))
916
- if report.get("service") != {"id": service_id, "instance": instance}:
917
- out.append("reports for %r, not %s.%s" % (report.get("service"), service_id, instance))
918
- if report.get("currency") != "USD":
919
- out.append("currency is not USD")
920
- days = report.get("days")
921
- if not isinstance(days, list) or len(days) > fs.USAGE_DAYS:
922
- return out + ["days is not a list of at most 31"]
923
- fields = ("calls", "unpricedCalls", "unmeasuredCalls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens")
924
-
925
- def priced(row: Dict[str, Any], at: str) -> None:
926
- calls, unpriced, cost = row.get("calls", 0), row.get("unpricedCalls", 0), row.get("costUsd")
927
- if unpriced > calls:
928
- out.append("%s: more unpriced calls than calls" % at)
929
- if row.get("unmeasuredCalls", 0) > calls:
930
- out.append("%s: more calls with unknown tokens than calls (DEC-0091)" % at)
931
- if calls and unpriced == calls and cost is not None:
932
- out.append("%s: every call is unpriced, so the cost must be null, not %r" % (at, cost))
933
- if unpriced < calls and cost is None:
934
- out.append("%s: priced calls carry a cost" % at)
935
- if cost is not None and (not isinstance(cost, (int, float)) or cost < 0):
936
- out.append("%s: costUsd is not a non-negative number" % at)
937
-
938
- previous = ""
939
- for i, day in enumerate(days):
940
- at = "days[%d]" % i
941
- date = str(day.get("date", "")) if isinstance(day, dict) else ""
942
- if not re.match(r"^\d{4}-\d{2}-\d{2}$", date) or date <= previous:
943
- out.append("%s: dates must run forward" % at)
944
- previous = date
945
- priced(day, at)
946
- models = day.get("byModel") if isinstance(day, dict) else None
947
- if not isinstance(models, list):
948
- out.append("%s: byModel missing" % at)
949
- continue
950
- for j, m in enumerate(models):
951
- priced(m, "%s.byModel[%d]" % (at, j))
952
- for f in fields:
953
- if day.get(f, 0) != sum(m.get(f, 0) for m in models):
954
- out.append("%s: %s is not the sum of its models" % (at, f))
955
- known = sum(m["costUsd"] for m in models if isinstance(m.get("costUsd"), (int, float)))
956
- if isinstance(day.get("costUsd"), (int, float)) and abs(day["costUsd"] - known) > 1e-6:
957
- out.append("%s: costUsd is not the sum of its models' known costs" % at)
958
- for j, m in enumerate(models):
959
- # DEC-0046, DEC-0061: a row's route is a login in its vendor's runner, an issued key, or the platform's
960
- # own metered models (a listing in the cloud).
961
- if "route" in m and m["route"] not in fs.USAGE_ROUTES:
962
- out.append("%s.byModel[%d]: route %r is not subscription, key or platform-metered" % (at, j, m["route"]))
963
- # DEC-0065: the window and the project attribution, as the contract's usageRules reads them (FAC-SEM-025).
964
- for finding in fs.usage_findings(report):
965
- path, message = finding["instancePath"], finding["message"]
966
- if path.startswith(("/byProject", "/generatedAt")) or "generatedAt" in message or "exists in its month" in message:
967
- out.append("%s: %s" % (path or "/", message))
968
- return out
969
-
970
-
971
- def same_repository(a: str, b: str) -> bool:
972
- """git@github.com:o/r.git, https://github.com/o/r and github.com/o/r are one repository."""
973
- def norm(u: str) -> str:
974
- u = u.strip().lower()
975
- u = re.sub(r"^git@([^:]+):", r"\1/", u)
976
- u = re.sub(r"^[a-z+]+://", "", u)
977
- u = re.sub(r"^[^@/]+@", "", u)
978
- return re.sub(r"\.git$", "", u).rstrip("/")
979
- return bool(a and b) and norm(a) == norm(b)
980
-
981
-
982
784
  def locate(target: Optional[str], descriptor: Optional[str], services_dir: Path) -> Path:
983
785
  if descriptor:
984
786
  return Path(descriptor).expanduser()