@passioncode-ai/fabric-agent-adapter 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/README.md +12 -0
- package/bin/fabric-agent-adapter.js +16 -1
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +24 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +5 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +102 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +26 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +590 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.schema.json +631 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +182 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +824 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.31.0 - 2026-10-14
|
|
4
|
+
|
|
5
|
+
An agent is packed, signed and verified as a `fabric-package/1` folder (FAA-27a and FAA-36, `RM-28 pilot`; contract
|
|
6
|
+
DEC-0050, DEC-0051, DEC-0071, DEC-0074, all at the pin `f00a768`).
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`fabric_package.py` and `fabric-package.mjs`** (`building-fabric-services`). The two write the same bytes.
|
|
11
|
+
- `pack <folder>` reads the author's `fabric.package.json` and does the rest:
|
|
12
|
+
- derives `agentRef` and lists every other file with its SHA-256, in UTF-16 order;
|
|
13
|
+
- computes the RFC 8785 digest;
|
|
14
|
+
- checks the result against the contract's schema (bundled at the pin), FAC-SEM-064…066 and FAC-SEM-102, and the
|
|
15
|
+
folder check.
|
|
16
|
+
- It refuses symlinks, `.git`, caches and files that are credentials by name, and names each one.
|
|
17
|
+
- It writes nothing without `--apply`, or while a problem stands.
|
|
18
|
+
- `sign <folder> --key <file>` adds an Ed25519 (`EdDSA`) publisher signature or organization countersignature over
|
|
19
|
+
the typed statement.
|
|
20
|
+
- The key is a JWK or PKCS#8 PEM file at mode 600, which the person names. The kit never makes, keeps or prints a
|
|
21
|
+
key.
|
|
22
|
+
- A folder that no longer matches its package is not signed.
|
|
23
|
+
- `verify <folder> --trust <file>` runs the contract's `verifyPackage` in PK-1…PK-10's order, with the small-order key
|
|
24
|
+
check first, plus the folder check and the rules.
|
|
25
|
+
- Also exported: `consumer_placement` / `consumerPlacement` for a consumer agent's `requires.mcp`.
|
|
26
|
+
- **`npx @passioncode-ai/fabric-agent-adapter pack|sign|verify`**: the same commands from the npm package.
|
|
27
|
+
- **`references/package.md`**: what a package is, the author's file, the three commands, a consumer agent, and what this
|
|
28
|
+
version does not do. That list: ES256 (Fabric D2), hub and passkey signing, the passkey countersignature, Fabric
|
|
29
|
+
recording packages by `agentRef`, and `publish`.
|
|
30
|
+
|
|
31
|
+
### Evidence
|
|
32
|
+
|
|
33
|
+
- `test/fixtures/contract/vendor_vectors.mjs` now writes `package-vectors.json`, with every case re-answered by the
|
|
34
|
+
contract's own functions, and the bundled schema. The other vendored files came out byte-identical.
|
|
35
|
+
- The Python and Node tests each hold 17 cases:
|
|
36
|
+
- every verifier case;
|
|
37
|
+
- every signature reproduced from the vector seeds;
|
|
38
|
+
- every fixture through the schema, and every semantic fixture through the rules;
|
|
39
|
+
- pack rebuilding the vector package byte for byte;
|
|
40
|
+
- both kits packing and signing one folder to the same bytes.
|
|
41
|
+
|
|
42
|
+
`test/test_installer.py` covers the npm commands. 28 planted defects were all caught, three of them after a case was
|
|
43
|
+
added for them.
|
|
44
|
+
- A scenario eval (`package-pack-sign`) and three trigger queries were written before the prose.
|
|
45
|
+
|
|
46
|
+
## 0.30.0 - 2026-10-13
|
|
47
|
+
|
|
48
|
+
The Node service kit installs and uninstalls a launchd job, as the Python kit does (FAA-04). The probe fails a plist
|
|
49
|
+
whose copy would not know launchd supervises it (FAA-05).
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **launchd in the Node kit** (`fabric-service.mjs`, FAA-04): `launchdPlist`, `launchdLoaded`, `launchdOverride`,
|
|
54
|
+
`launchdInstall` and `launchdUninstall`, the twins of the Python `launchd_*` helpers.
|
|
55
|
+
- `launchdPlist` writes the same bytes as Python's `plistlib`.
|
|
56
|
+
- It writes `FABRIC_SERVICE_SUPERVISOR=launchd` itself and refuses a marker argument.
|
|
57
|
+
- It refuses a secret-named variable (a `*_FILE` path is fine), and any variable value or argument shaped like a
|
|
58
|
+
credential, a control character or an empty program.
|
|
59
|
+
- `launchdInstall` follows the LC-14 order: lint, then enable only on a first install. A label the operator
|
|
60
|
+
disabled stays off, with the plist rewritten. The bootout is waited out, the bootstrap retried five times, and
|
|
61
|
+
the install resolves once the well-known document answers with this `id.instance`.
|
|
62
|
+
- `launchdUninstall` waits for the unload, removes the plist and resets the override to `enabled`. `purge` (with
|
|
63
|
+
the service id) removes data, logs and cache; a symlinked folder loses only its link.
|
|
64
|
+
- Off macOS both are refused, with the name of that platform's installer. `run`, `fetchWellKnown` and `sleep` are
|
|
65
|
+
injectable, so tests never reach the real launchctl.
|
|
66
|
+
- **Probe rule `lifecycle.supervisor-marker`** (`check_service.py`, FAA-05). It reads the plist's
|
|
67
|
+
`EnvironmentVariables` and `ProgramArguments` the way the started copy reads them (`supervisor_marker`,
|
|
68
|
+
DEC-0057). It fails these cases:
|
|
69
|
+
- no marker;
|
|
70
|
+
- a marker naming another supervisor, or naming none;
|
|
71
|
+
- markers that disagree.
|
|
72
|
+
|
|
73
|
+
Without the marker, a copy that finds the lock held exits 75 at once, and KeepAlive restarts it every
|
|
74
|
+
`ThrottleInterval`. Plists written by `launchd_plist` since 0.8.0 (FAA-06) carry the marker; an older one fails
|
|
75
|
+
until it is regenerated and reinstalled.
|
|
76
|
+
|
|
77
|
+
### Evidence
|
|
78
|
+
|
|
79
|
+
- `test/fabric-service-launchd.test.mjs` (20 cases) runs against a launchd stand-in with the Python fake's
|
|
80
|
+
semantics. Its byte-identity case compares `launchdPlist` with `plistlib` output.
|
|
81
|
+
- Two Python cases in `test/test_fabric_service.py` check `marker_problem` and the rule run on a written plist.
|
|
82
|
+
- 16 planted defects were all caught, after replacing two equivalent mutants with real ones. Among them: enable on
|
|
83
|
+
every install, a skipped unload wait, a kept override, unsorted keys, an early bootstrap give-up, an ignored
|
|
84
|
+
instance, a dropped marker, a skipped lint, the legacy override format, an endless uninstall wait, and a probe
|
|
85
|
+
that accepts any supervisor or none.
|
|
86
|
+
|
|
3
87
|
## 0.29.0 - 2026-10-13
|
|
4
88
|
|
|
5
89
|
`working-with-fabric` carries the limit-stop rule (fabric-workspace principle 10, item 6, operator 2026-10-13).
|
package/README.md
CHANGED
|
@@ -231,6 +231,18 @@ python3 "$SKILL_DIR/scripts/adapt_project.py" check /path/to/project \
|
|
|
231
231
|
--contract /path/to/fabric-agent-contract --json
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
+
Pack, sign and verify an agent as a `fabric-package/1` folder (contract DEC-0050, DEC-0051; nothing is written without
|
|
235
|
+
`--apply`, and the key file is the person's, never made or kept here):
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npx @passioncode-ai/fabric-agent-adapter pack ./example-agent # the digest, agentRef and every problem
|
|
239
|
+
npx @passioncode-ai/fabric-agent-adapter pack ./example-agent --apply
|
|
240
|
+
npx @passioncode-ai/fabric-agent-adapter sign ./example-agent --key ./publisher.jwk --apply
|
|
241
|
+
npx @passioncode-ai/fabric-agent-adapter verify ./example-agent --trust ./trust.json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
See [the package reference](plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md).
|
|
245
|
+
|
|
234
246
|
Scaffolding is non-destructive by default. It writes only the paths declared in the
|
|
235
247
|
[delivery brief](docs/evidence/specs/2026-08-27-brief.md) and refuses collisions.
|
|
236
248
|
|
|
@@ -31,6 +31,14 @@ Usage:
|
|
|
31
31
|
remove plain ~/.claude/skills copies that shadow the installed plugin
|
|
32
32
|
npx @passioncode-ai/fabric-agent-adapter --help
|
|
33
33
|
|
|
34
|
+
Packages (fabric-package/1):
|
|
35
|
+
npx @passioncode-ai/fabric-agent-adapter pack <folder> [--apply]
|
|
36
|
+
list the folder's files, derive agentRef, compute the digest, check it
|
|
37
|
+
npx @passioncode-ai/fabric-agent-adapter sign <folder> --key <file> [--kid <kid>] [--role publisher|organization] [--apply]
|
|
38
|
+
add an Ed25519 signature from a private key file (never made or kept here)
|
|
39
|
+
npx @passioncode-ai/fabric-agent-adapter verify <folder> --trust <trust.json> [--countersigned]
|
|
40
|
+
check the folder and its signatures against the keys you pin
|
|
41
|
+
|
|
34
42
|
Claude Code (recommended):
|
|
35
43
|
claude plugin marketplace add ${REPO}
|
|
36
44
|
claude plugin install ${PLUGIN_ID}
|
|
@@ -162,4 +170,11 @@ function main(argv) {
|
|
|
162
170
|
return 0;
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
|
|
173
|
+
const PACKAGE_COMMANDS = ['pack', 'sign', 'verify'];
|
|
174
|
+
if (PACKAGE_COMMANDS.includes(process.argv[2])) {
|
|
175
|
+
// The package commands live in the kit (fabric-package.mjs), one implementation for the skill and this CLI.
|
|
176
|
+
const kit = path.join(ROOT, 'plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs');
|
|
177
|
+
import(require('url').pathToFileURL(kit).href).then((m) => { process.exitCode = m.main(process.argv.slice(2)); });
|
|
178
|
+
} else {
|
|
179
|
+
process.exit(main(process.argv));
|
|
180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -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.
|
|
6
|
+
"version": "0.31.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ 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.
|
|
8
|
+
version: "0.31.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "f00a7685e3861c27f7899c033fb850a145a1708f"
|
|
11
11
|
---
|
|
@@ -8,16 +8,17 @@ 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,
|
|
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
|
|
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.
|
|
21
|
+
version: "0.31.0"
|
|
21
22
|
contract-version: "0.1.0"
|
|
22
23
|
extension: "fabric-service/0.1"
|
|
23
24
|
extension-commit: "f00a7685e3861c27f7899c033fb850a145a1708f"
|
|
@@ -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
|
|
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
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -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`
|
|
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
|
|
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).
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py
CHANGED
|
@@ -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"
|