@passioncode-ai/passioncode 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/CHANGELOG.md +18 -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 +3 -3
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  9. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +25 -6
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +24 -1
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +5 -5
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +102 -0
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +2 -2
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +26 -0
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +144 -11
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +590 -0
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.schema.json +631 -0
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +182 -0
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +140 -10
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +824 -0
  21. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  22. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  23. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +20 -0
  24. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -1
  25. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2 - 2026-10-14
4
+
5
+ ### Changed
6
+
7
+ - **Fabric Agent Adapter is pinned at `v0.32.0`** (it was `v0.30.0`).
8
+ - v0.31.0 added packing, signing and verifying an agent as a `fabric-package/1` folder, in both service kits and from
9
+ the command line (`npx @passioncode-ai/fabric-agent-adapter pack|sign|verify`). This is the adapter's part of the
10
+ RM-28 pilot.
11
+ - v0.31.1 lets this launcher vendor the kit.
12
+ - v0.32.0 pins contract `1636fa9`: a board responder now waits under an operator's hold, never treating it as an
13
+ outage, and reads the estate owner's settlement.
14
+
15
+ ## 0.2.1 - 2026-10-13
16
+
17
+ ### Changed
18
+
19
+ - **Fabric Agent Adapter is pinned at `v0.30.0`** (it was `v0.29.0`). The Node service kit now installs and uninstalls a launchd job the way the Python kit does: `launchdPlist`, `launchdInstall` and `launchdUninstall`. A service's plist comes out the same from either kit. The service probe (`check_service.py`) gains a rule, `lifecycle.supervisor-marker`: it fails a launchd job whose plist does not tell the service that launchd supervises it. Without that marker, a second copy that finds the lock held exits at once and launchd restarts it every few seconds.
20
+
3
21
  ## 0.2.0 - 2026-10-13
4
22
 
5
23
  ### Added
package/family.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "name": "fabric-agent-adapter",
9
9
  "displayName": "Fabric Agent Adapter",
10
10
  "repo": "passioncode-ai/fabric-agent-adapter",
11
- "ref": "v0.29.0",
11
+ "ref": "v0.32.0",
12
12
  "kind": "plugin",
13
13
  "path": "plugins/fabric-agent-adapter",
14
14
  "legacyPluginIds": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/passioncode",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
5
5
  "bin": {
6
6
  "passioncode": "bin/passioncode.js"
@@ -11,7 +11,7 @@
11
11
  "name": "fabric-agent-adapter",
12
12
  "displayName": "Fabric Agent Adapter",
13
13
  "source": "./plugins/fabric-agent-adapter",
14
- "version": "0.29.0",
14
+ "version": "0.32.0",
15
15
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
16
16
  "author": {
17
17
  "name": "PassionCode.ai",
@@ -35,7 +35,7 @@
35
35
  "name": "passioncode",
36
36
  "displayName": "PassionCode.ai",
37
37
  "source": "./plugins/passioncode",
38
- "version": "0.2.0",
38
+ "version": "0.2.2",
39
39
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
40
40
  "author": {
41
41
  "name": "PassionCode.ai",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "family": "passioncode",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "release": true,
5
5
  "members": [
6
6
  {
7
7
  "name": "fabric-agent-adapter",
8
8
  "displayName": "Fabric Agent Adapter",
9
9
  "repo": "passioncode-ai/fabric-agent-adapter",
10
- "ref": "v0.29.0",
11
- "commit": "73af866a11e5f5df16e062d860806e3af331d014",
12
- "version": "0.29.0",
10
+ "ref": "v0.32.0",
11
+ "commit": "9078be6478029e8d9fe37855e8034c81f45ef3b3",
12
+ "version": "0.32.0",
13
13
  "via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
14
14
  "skills": [
15
15
  "adapting-projects-to-fabric",
@@ -23,7 +23,7 @@
23
23
  "legacyMarketplaces": [
24
24
  "fabric-agent-adapter"
25
25
  ],
26
- "contentHash": "sha256:ab96afe1f3face2a185938cc5a8f4f2893079a30e2ea279bd686177f4a16efc4",
26
+ "contentHash": "sha256:86290c04a4dcdefb5ee2fca424a363195947ded890e0ad97335d79fe4051b181",
27
27
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
28
28
  "author": {
29
29
  "name": "PassionCode.ai",
@@ -62,9 +62,9 @@
62
62
  "name": "passioncode",
63
63
  "displayName": "PassionCode.ai",
64
64
  "repo": "passioncode-ai/passioncode",
65
- "ref": "v0.2.0",
65
+ "ref": "v0.2.2",
66
66
  "commit": null,
67
- "version": "0.2.0",
67
+ "version": "0.2.2",
68
68
  "via": "this repository",
69
69
  "skills": [
70
70
  "setting-up-passioncode",
@@ -72,7 +72,7 @@
72
72
  ],
73
73
  "legacyPluginIds": [],
74
74
  "legacyMarketplaces": [],
75
- "contentHash": "sha256:ff4556c8304a081746b125922aac23bc3ab4ca9b25c3ab48d4cbedd1b68c23ee",
75
+ "contentHash": "sha256:f2bb2ec0982bd940e6d24746295692c7e84a1488a0298d3f9977c312ad8bdd7a",
76
76
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
77
77
  "author": {
78
78
  "name": "PassionCode.ai",
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
6
- "version": "0.29.0",
6
+ "version": "0.32.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.29.0"
8
+ version: "0.32.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "f00a7685e3861c27f7899c033fb850a145a1708f"
10
+ contract-commit: "1636fa97262f4a26e2086572d540c2ee701e0010"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -70,7 +70,7 @@ Use exactly:
70
70
 
71
71
  - contract version `0.1.0`;
72
72
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
73
- - commit `f00a7685e3861c27f7899c033fb850a145a1708f`.
73
+ - commit `1636fa97262f4a26e2086572d540c2ee701e0010`.
74
74
 
75
75
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
76
76
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -21,10 +21,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
21
21
 
22
22
  CONTRACT_VERSION = "0.1.0"
23
23
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
24
- CONTRACT_COMMIT = "f00a7685e3861c27f7899c033fb850a145a1708f"
24
+ CONTRACT_COMMIT = "1636fa97262f4a26e2086572d540c2ee701e0010"
25
25
  # #region supported-contract-revisions — docs: README.md#contract-pin
26
26
  # Only this declaration may name a legacy revision in live files.
27
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
27
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
28
  # #endregion supported-contract-revisions
29
29
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
30
30
  MCP_REVISION = "2026-07-28"
@@ -8,19 +8,20 @@ description: >-
8
8
  "make this agent a local service", "make an online dashboard a Fabric service", "add the
9
9
  well-known endpoint", «расходы агента на модели». Covers fabric-service/0.1:
10
10
  surfaces, token and one-time login, state, one copy, launchd, descriptor, well-known document,
11
- events, the usage report; online: the https guard, the token-gated well-known document, the
12
- __Host- cookie; the board responder (fenced claims); Python, Node and dashboard kits, probes.
11
+ events, the usage report; online: the https guard, token-gated well-known, the
12
+ __Host- cookie; the board responder (fenced claims); Python, Node and dashboard kits, probes;
13
+ packing and signing (fabric-package/1).
13
14
  NOT for a one-off script or cron job, a hosted product
14
- with no agent behind it, the provider manifest (adapting-projects-to-fabric), or building Fabric
15
+ with no agent, the provider manifest (adapting-projects-to-fabric), or building Fabric
15
16
  Dashboards.
16
17
  license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
17
18
  compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
18
19
  metadata:
19
20
  author: PassionCode.ai
20
- version: "0.29.0"
21
+ version: "0.32.0"
21
22
  contract-version: "0.1.0"
22
23
  extension: "fabric-service/0.1"
23
- extension-commit: "f00a7685e3861c27f7899c033fb850a145a1708f"
24
+ extension-commit: "1636fa97262f4a26e2086572d540c2ee701e0010"
24
25
  ---
25
26
 
26
27
  # Building Fabric services
@@ -203,7 +204,8 @@ The installer, not the service, owns the plist and the descriptor. Sequence:
203
204
 
204
205
  On Linux the supervisor is a systemd user unit (`systemd_unit` + `systemd_install`), on
205
206
  Windows a per-user Scheduled Task (`task_xml` + `task_install`); the Node kit has the same
206
- writers (`systemdUnit`, `taskXml`, …) and both kits write identical files.
207
+ writers and installers (`launchdPlist` + `launchdInstall`, `systemdUnit`, `taskXml`, …) and
208
+ both kits write identical files.
207
209
 
208
210
  Code runs from an immutable release directory; an upgrade writes a new release,
209
211
  rewrites the plist and restarts. Read [the lifecycle reference](references/lifecycle.md)
@@ -270,6 +272,23 @@ Then prove the lock: start a second copy by hand against the same data directory
270
272
  must exit 75 while the first keeps serving. Prove recovery: `kill -9` the launchd pid;
271
273
  the well-known document must answer again with a new pid within about 15 seconds.
272
274
 
275
+ ## Packing and signing it — `fabric-package/1`
276
+
277
+ To hand an agent to another machine or an organization, pack it as one folder (contract DEC-0050, DEC-0051). Write the
278
+ author's `fabric.package.json` (the descriptive fields; for an agent `resultSignal` and `evals` with the receipt of the
279
+ suite as packed), then:
280
+
281
+ ```bash
282
+ python3 <skill-dir>/scripts/fabric_package.py pack <folder> # or: node fabric-package.mjs, npx @passioncode-ai/fabric-agent-adapter pack
283
+ python3 <skill-dir>/scripts/fabric_package.py pack <folder> --apply
284
+ python3 <skill-dir>/scripts/fabric_package.py sign <folder> --key <key file> --kid <kid> --apply
285
+ python3 <skill-dir>/scripts/fabric_package.py verify <folder> --trust <trust.json>
286
+ ```
287
+
288
+ `pack` derives `agentRef`, lists every file with its SHA-256, computes the digest and checks the schema, the rules and
289
+ the folder; it refuses `.env`, keys, `.git`, caches and symlinks. `sign` uses a key file the person names (Ed25519,
290
+ `EdDSA`); it never makes, keeps or prints a key. Read [the package reference](references/package.md) before packing.
291
+
273
292
  ## The repository around the service
274
293
 
275
294
  **For a PassionCode.ai repository**, the service's repository follows the organization's
@@ -1,7 +1,7 @@
1
1
  # Answering a Project's requests on Fabric's project board
2
2
 
3
3
  Read this when a service should answer requests other Projects post to its Project on Fabric's project board
4
- (`fabric-project-comms/0.1`, contract DEC-0022, DEC-0073, DEC-0077, DEC-0078; Fabric ADR-0149). The kits do it with
4
+ (`fabric-project-comms/0.1`, contract DEC-0022, DEC-0073, DEC-0077, DEC-0078, DEC-0079, DEC-0081; Fabric ADR-0149). The kits do it with
5
5
  two modules each: the hub door client [`fabric_hub.py`](../scripts/fabric_hub.py) /
6
6
  [`fabric-hub.mjs`](../scripts/fabric-hub.mjs) and the responder [`fabric_board.py`](../scripts/fabric_board.py) /
7
7
  [`fabric-board.mjs`](../scripts/fabric-board.mjs). A coding agent that only reads and writes the board is the other
@@ -15,6 +15,7 @@ skill's business (working-with-fabric, `references/project-board.md`).
15
15
  - The request is data: the input gate
16
16
  - Effects, observation, evidence and the reply
17
17
  - When the board says stop, and restarts
18
+ - When the operator holds work (DEC-0079)
18
19
  - What a board session may call
19
20
  - Looking a Project up in the directory (FAC-SEM-107)
20
21
  - Events, the probe and the live smoke
@@ -115,6 +116,28 @@ accepted ones go back to this responder with `com.responder_replace {expectedGen
115
116
  contract has no `com.claim` read-back and `com.claim` never moves an accepted request. A known outcome is sent after the
116
117
  transfer without running the handler again. `shutdown()` drains, then leaves the session (its DELETE releases the slot).
117
118
 
119
+ ## When the operator holds work (DEC-0079)
120
+
121
+ The operator can hold a thread, a whole Project or one (Project, capability) slot from Fabric's own controls. While the
122
+ hold lasts, the board refuses **new** work in its scope with `held`, which is always `retryable: true`. It never
123
+ refuses a call that settles, reports on or keeps alive work already started (`com.renew`, `com.progress`,
124
+ `com.complete`, `com.reconcile`), a cancel or a read. `hold_refuses(tool, target, holds)` / `holdRefuses` is the
125
+ contract's matrix, held to its vectors (`test/fixtures/contract/comms-hold-vectors.json`).
126
+
127
+ | Refused `held` | What the kit does |
128
+ |---|---|
129
+ | `com.status` names the slot `held: true` at connect | the slot is not enrolled; it pauses |
130
+ | `com.enroll`, `com.claim`, `com.responder_replace` | the slot pauses: asked again after 15 s, then 30 s, doubling to 15 minutes; one `board.slot.held` event per capability, and `board.slot.released` when an enroll succeeds again |
131
+ | `com.accept` (the hold came after the claim) | nothing was done: the entry is dropped, the claim lapses and the request waits on the board (`board.request.waiting`); the slot pauses |
132
+ | `com.effect_begin` | the effect has not begun: the handler waits (same idempotency key, the pause doubling) while the tick keeps renewing the lease, then begins once the hold is lifted; `ctx.stopped` ends the wait |
133
+ | accepted work at restart under a hold | kept as `lapsed`, then taken back with `com.responder_replace` and a transfer once the hold is lifted — never claimed back |
134
+
135
+ A hold is not an outage: no `board.down`, no notify. Work already accepted keeps running and completes.
136
+
137
+ A receipt's request may carry `settledBy: "owner"` (DEC-0081): the estate owner settled an `outcome_unknown` request
138
+ whose responder was gone. Read it as the owner's attestation, not the responder's; it appears only on `completed` or
139
+ `failed_known`, and its absence means the responder settled it.
140
+
118
141
  ## What a board session may call
119
142
 
120
143
  A board session (`fabric.board.join`) is served the respond scope only: `com.status`, `com.list`, `com.get`,
@@ -12,7 +12,7 @@ wins and this page is the defect. The rule each section serves:
12
12
  | LC-09 the footprint is declared | the descriptor and the plist, unit or task; the service's own `AGENTS.md` names its label (unit, task) and port | `write_descriptor`, `launchd_plist`, `systemd_unit` / `systemdUnit`, `task_xml` / `taskXml` |
13
13
  | LC-11 previous release kept, older pruned | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `prune_releases` / `pruneReleases` |
14
14
  | LC-12 every file is bounded | [Logs](#logs) | `RotatingLog`, `cap_stdout_log` / `RotatingLog`, `capStdoutLog` |
15
- | LC-14 operator intent wins; uninstall is symmetric | [Install, upgrade, uninstall](#install-upgrade-uninstall), [Linux and Windows](#linux-and-windows) | `launchd_install`, `launchd_uninstall` (Python); `systemd_install` / `systemdInstall`, `task_install` / `taskInstall` and their uninstalls |
15
+ | LC-14 operator intent wins; uninstall is symmetric | [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 |
16
16
  | LC-15 builds clean up after themselves | release pruning runs inside the installer, not later | `prune_releases` / `pruneReleases` |
17
17
 
18
18
  ## Contents
@@ -26,7 +26,7 @@ wins and this page is the defect. The rule each section serves:
26
26
  - [Being managed by a host lifecycle broker](#being-managed-by-a-host-lifecycle-broker)
27
27
  - [Linux and Windows](#linux-and-windows)
28
28
 
29
- ## launchd plist (generate it with `launchd_plist`)
29
+ ## launchd plist (generate it with `launchd_plist` or `launchdPlist`; both kits write the same bytes)
30
30
 
31
31
  | Key | Value | Why |
32
32
  |---|---|---|
@@ -37,7 +37,7 @@ wins and this page is the defect. The rule each section serves:
37
37
  | `ThrottleInterval` | `10` | bounds a crash loop |
38
38
  | `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 |
39
39
  | `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) |
40
- | `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 |
40
+ | `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`) |
41
41
  | `StandardOutPath`/`StandardErrorPath` | `~/Library/Logs/<id>/service.log` | one place the host tails |
42
42
 
43
43
  ## Install, upgrade, uninstall
@@ -45,14 +45,14 @@ wins and this page is the defect. The rule each section serves:
45
45
  1. Build or unpack the release into `~/.local/share/<id>/releases/<version>-<sha12>/`
46
46
  (a venv or `node_modules` inside it). Never edit a release after it is written.
47
47
  2. `write_descriptor` (port and id claims are checked here).
48
- 3. `launchd_install`: write + `plutil -lint`, read `launchctl print-disabled gui/<uid>`,
48
+ 3. `launchd_install` (Node: `await launchdInstall(...)`, the same steps): write + `plutil -lint`, read `launchctl print-disabled gui/<uid>`,
49
49
  `enable` **only when no override is recorded** (a first install), `bootout` and wait
50
50
  until `launchctl print gui/<uid>/<label>` fails, `bootstrap` with up to five retries,
51
51
  then poll the well-known document until it answers with this `id.instance`.
52
52
  4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
53
53
  again, then `prune_releases(releases_dir, current=<new release>)`: the running release
54
54
  and the one before it stay for rollback, older ones go (LC-11, LC-15).
55
- 5. Uninstall = `launchd_uninstall` + `remove_descriptor`. Data stays; `purge=True`
55
+ 5. Uninstall = `launchd_uninstall` (Node: `await launchdUninstall(...)`) + `remove_descriptor`. Data stays; `purge=True`
56
56
  (with `service_id`) removes data, logs and cache when the operator asks.
57
57
 
58
58
  **The operator's intent wins (LC-14).** A host stops a service with `bootout` **and**
@@ -0,0 +1,102 @@
1
+ # Packing, signing and verifying — `fabric-package/1`
2
+
3
+ Contract DEC-0050 (format and digest), DEC-0051 (signatures), DEC-0071 and DEC-0074 (a consumer agent), rules
4
+ `FAC-SEM-064`…`066` and `FAC-SEM-102`, all at this plugin's pin. The kit: `scripts/fabric_package.py` and
5
+ `scripts/fabric-package.mjs`, which write the same bytes, and `npx @passioncode-ai/fabric-agent-adapter pack|sign|verify`.
6
+
7
+ ## Contents
8
+
9
+ - [What a package is](#what-a-package-is)
10
+ - [The author's file](#the-authors-file)
11
+ - [pack](#pack)
12
+ - [sign](#sign)
13
+ - [verify](#verify)
14
+ - [A consumer agent](#a-consumer-agent)
15
+ - [What this version does not do](#what-this-version-does-not-do)
16
+
17
+ ## What a package is
18
+
19
+ One version of an agent or a skill set, as one folder: `fabric.package.json` at its root, every other file listed in it
20
+ with its SHA-256. The digest is SHA-256 over the RFC 8785 form of the document without `digest` and `signatures`, so it
21
+ covers every file through `files[]`, and a countersignature needs no repacking. A folder that is a Claude Code plugin
22
+ has `.claude-plugin/plugin.json` whose `name` and `version` are the package's `id` and `version`.
23
+
24
+ ## The author's file
25
+
26
+ The person writes the descriptive part; `pack` fills in the rest.
27
+
28
+ | You write | `pack` writes |
29
+ |---|---|
30
+ | `schema` (`fabric-package/1`), `kind` (`agent` or `skill-set`), `id` (kebab-case, the plugin name), `version` (semver), `publisher` (a name, never a key), `licence` (SPDX) | `agentRef` = `urn:fabric:agent:<publisher>:<id>`, after `publisher` (an agent only; a hand-written one that differs is refused) |
31
+ | an agent: `resultSignal {name, says}`, `evals {suite, receipt {ranAt, suite, passed, failed, skipped?}}`, and as needed `provider`, `service`, `secrets[]` (slots: name and purpose, never a value), `requires`, `costPresets`, `state[]`, `dependencies[]` | `files[]`: every file but `fabric.package.json`, in UTF-16 order, each with `sha256:` and its hex digest |
32
+ | a skill set: `layer`, `skills` | `digest` |
33
+
34
+ The evals receipt's `suite` is the SHA-256 of the suite **as packed**. When the suite changes, run it again and write that
35
+ receipt; `pack` names the digest it expects. Never edit the receipt to match.
36
+
37
+ ## pack
38
+
39
+ ```bash
40
+ python3 scripts/fabric_package.py pack <folder> # a dry run: the package, the digest, every problem
41
+ python3 scripts/fabric_package.py pack <folder> --apply # writes fabric.package.json when nothing is wrong
42
+ ```
43
+
44
+ It refuses to list what must not ship, and names each: a symlink, a special file, `.git`, `__pycache__`, `.DS_Store`,
45
+ `*.pyc`, and files that are credentials by name (`.env` and `.env.*` but `.env.example|sample|template`, `*.pem`, `*.key`,
46
+ `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `id_rsa`-style keys, `.npmrc`, `.netrc`, `.pypirc`). Remove them or pack a clean
47
+ copy. A member named twice in `fabric.package.json` is refused too.
48
+
49
+ Then every problem is listed, each with its `rule`: `schema` (the contract's schema), `FAC-SEM-064` (whole: its own
50
+ digest, each path once, every named file listed with the same SHA-256), `FAC-SEM-065` (no secret), `FAC-SEM-066`
51
+ (`agentRef` derived), `FAC-SEM-102` (one role, one server) and `folder` (`file-missing`, `file-digest`, `file-unlisted`,
52
+ `plugin-manifest`, `provider-manifest`). With a problem, `--apply` writes nothing. Repacking drops old signatures and
53
+ says so: a changed package is signed again.
54
+
55
+ ## sign
56
+
57
+ ```bash
58
+ python3 scripts/fabric_package.py sign <folder> --key <key file> [--kid <kid>] [--role publisher|organization] --apply
59
+ ```
60
+
61
+ - The key is a file the person names: an Ed25519 private JWK (`kty` OKP, `crv` Ed25519, `d`, its `kid`) or a PKCS#8 PEM
62
+ from `openssl genpkey -algorithm ed25519` (then `--kid`). It must be mode 600. The kit never makes, keeps or prints a
63
+ key; where the key lives is the person's decision (contract v4 D3).
64
+ - The signature covers `{"type": "fabric-package-signature/1", "role", "digest"}` in RFC 8785 form, so a publisher's
65
+ signature never stands for a countersignature. `alg` is `EdDSA`, the one algorithm DEC-0051 admits.
66
+ - Only a package that is whole and matches its folder is signed. A publisher signature is always one (re-signing replaces
67
+ it); an organization countersignature under the same `kid` is replaced, another is added.
68
+ - The answer carries `publicKey`, the public JWK the installing machine pins (the estate record's `publisherKeys`,
69
+ DEC-0055). The package names a publisher, never a key.
70
+
71
+ ## verify
72
+
73
+ ```bash
74
+ python3 scripts/fabric_package.py verify <folder> --trust trust.json [--countersigned]
75
+ ```
76
+
77
+ `trust.json` is `{"publisher": [jwk…], "organization": [jwk…]}`, the keys this machine pins. The verdict is the
78
+ contract's `verifyPackage`, in PK-1…PK-10's order: `malformed`, `digest-mismatch`, `unsigned`, `no-publisher-signature`,
79
+ then per signature `unknown-algorithm`, `unknown-key` / `duplicate-kid`, `key-algorithm-mismatch` / `unusable-key`,
80
+ `small-order-key` (checked before any verification, DEC-0045), `bad-signature`, and with `--countersigned`
81
+ `no-countersignature`. A countersignature under a key the machine does not pin is listed in `unverified` unless
82
+ `--countersigned` is asked; a bad one under a pinned key refuses the package. `verify` also runs the folder check and the
83
+ rules, so a file changed after signing is caught (`file-digest`, `file-unlisted`).
84
+
85
+ Exit codes for all three: 0 when it holds, 1 when it does not, 2 for a usage error. The answer is JSON on stdout.
86
+
87
+ ## A consumer agent
88
+
89
+ An agent that serves nothing and only calls MCP servers is `kind: agent` without `provider` (DEC-0071). It lists its
90
+ `skills` and its plugin manifest, has no `service`, `state` or `layer`, and names the servers it calls in `requires.mcp[]`
91
+ by role and tools. A role names the secret slots that configure its server (`secrets`, DEC-0074), each declared in
92
+ `secrets[]`, one slot to one role. Before installing, `consumer_placement(pkg, configured)` / `consumerPlacement(pkg,
93
+ Map)` names each role the placement does not register and each tool its server lacks.
94
+
95
+ ## What this version does not do
96
+
97
+ - **ES256.** It waits for Fabric's decision D2; until then a verifier refuses it as `unknown-algorithm` (FAA-29).
98
+ - **Hub signing for the person's own machines, and the passkey.** Stage 3 of target architecture v4.
99
+ - **The organization's countersignature as a passkey assertion.** It waits for `org-countersign/1` and
100
+ `org-authority/1`; this version's countersignature is a detached Ed25519 signature.
101
+ - **Fabric recording packages by `agentRef`.** It waits for the ADR-0138 D2b amendment.
102
+ - **`publish --to org`** (FAA-28).
@@ -1,6 +1,6 @@
1
1
  # fabric-service/0.1 — wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `f00a7685e3861c27f7899c033fb850a145a1708f`
3
+ Pinned to `fabric-agent-contract` commit `1636fa97262f4a26e2086572d540c2ee701e0010`
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;
@@ -8,7 +8,7 @@ canonical forms DEC-0043; `degraded[].setup` and `run.mcp.stdio.scope` DEC-0044;
8
8
  DEC-0045; runner and model profiles DEC-0046; the estate record DEC-0055; one copy under any supervisor and its marker,
9
9
  the Windows token-file reader DEC-0057; the hub key the estate certifies DEC-0060; signing credentials DEC-0063; the
10
10
  estate record's 60 s skew DEC-0066; an app's own descriptor DEC-0069; the descriptor for listings DEC-0061; identifier equality
11
- by normal form DEC-0076; the project board's directory page DEC-0077 and a replacement's transferred attempts DEC-0078). The contract's schemas are normative;
11
+ by normal form DEC-0076; the project board's directory page DEC-0077 and a replacement's transferred attempts DEC-0078; an operator's hold DEC-0079, a structured finding DEC-0080 and the owner's settlement DEC-0081). The contract's schemas are normative;
12
12
  this page is the working summary. Extension key:
13
13
  `https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
14
14
 
@@ -75,6 +75,29 @@ def plist_problems(plist: dict, label: object, token: object = None) -> list:
75
75
  return problems
76
76
 
77
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
+
78
101
  def priority_problems(plist: dict) -> list:
79
102
  """A service that answers hosts and agents must not be scheduled as background work."""
80
103
  out = []
@@ -701,6 +724,9 @@ class Probe:
701
724
  return
702
725
  problems = plist_problems(plist, life.get("label"), self.token)
703
726
  self.add("lifecycle.plist", "FAIL" if problems else "PASS", "; ".join(problems) or "%s: RunAtLoad, KeepAlive, no secrets" % plist_path.name)
727
+ marker = marker_problem(plist)
728
+ self.add("lifecycle.supervisor-marker", "FAIL" if marker else "PASS",
729
+ marker or "%s: %s=launchd, so a copy that finds the lock held backs off" % (plist_path.name, fs.SUPERVISOR_ENV))
704
730
  slow = priority_problems(plist)
705
731
  self.add("lifecycle.priority", "FAIL" if slow else "PASS",
706
732
  "; ".join(slow) + " — macOS may starve the service under load and hosts then see an outage; use ProcessType Standard"