@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.
- package/CHANGELOG.md +17 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +13 -13
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +56 -45
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +64 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +264 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +12 -12
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +13 -11
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +11 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +6 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +8 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +128 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +26 -26
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +3 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +10 -10
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +13 -13
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +3 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +7 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -209
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +286 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +840 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -16
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +15 -46
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +8 -67
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +202 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +3 -13
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2521
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +400 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +402 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +78 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +902 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -38
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +289 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +16 -181
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +39 -400
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +137 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +1 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +22 -44
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +31 -38
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3430
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +412 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1526 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +33 -33
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +27 -27
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +31 -31
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +9 -9
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +8 -8
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +116 -1392
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +270 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +508 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +265 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +155 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +335 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -0
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Delegation
|
|
1
|
+
# Delegation: checking who acts for whom (DEC-0039)
|
|
2
2
|
|
|
3
3
|
A service that other agents of the estate call checks each call offline with the kit's verifier: the estate hub
|
|
4
4
|
signs a delegation token naming the person (`sub`), the actor chain (`act`), this service (`aud`), the
|
|
@@ -22,7 +22,7 @@ contract's own verifier does.
|
|
|
22
22
|
## Declaring it
|
|
23
23
|
|
|
24
24
|
In the well-known document: `surfaces.mcp.auth: "delegation"` (only delegation tokens) and
|
|
25
|
-
`auth.delegation: {issuer, jwks}
|
|
25
|
+
`auth.delegation: {issuer, jwks}`: the `iss` you trust (`urn:fabric:estate:<id>`) and where the key set is read.
|
|
26
26
|
The descriptor's token stays the host's: it is never a caller's credential on `/mcp` (DL-12). The probe checks
|
|
27
27
|
the declaration (`auth.delegation-declared`) and that `/mcp` refuses the descriptor token.
|
|
28
28
|
|
|
@@ -34,7 +34,7 @@ keys = fd.JwksCache(well_known["auth"]["delegation"]["jwks"]) # https,
|
|
|
34
34
|
gate = fd.delegation_gate(audience="example-agent", jwks=keys, issuer=well_known["auth"]["delegation"]["issuer"], # required
|
|
35
35
|
effects={"example.write": "draft", "example.publish": "publish"}) # each tool's manifest effect
|
|
36
36
|
server = fi.McpToolServer("example-agent", VERSION, authorize=gate) # pass the request headers to handle()
|
|
37
|
-
# in a handler: ctx.delegation["principal"], ["caller"], ["chain"]
|
|
37
|
+
# in a handler: ctx.delegation["principal"], ["caller"], ["chain"]; record the chain with the work
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
```js
|
|
@@ -50,7 +50,7 @@ if (!verdict.ok) return refusalToolResult(verdict);
|
|
|
50
50
|
DL-25 places each caller in a tier by its origin: **T0** a loopback caller under the owner's account that the
|
|
51
51
|
service's own local floor (the descriptor token, the Host guard) has authenticated; **T1** an enrolled machine of
|
|
52
52
|
the same owner; **T2** everyone else, and any call relayed onto loopback from off the machine (a tunnel, a proxy, the
|
|
53
|
-
hub forwarding a T2 caller). DL-26: a T0 or T1 caller is never required to present a token
|
|
53
|
+
hub forwarding a T2 caller). DL-26: a T0 or T1 caller is never required to present a token; pass the tier,
|
|
54
54
|
`gate(tool, headers, tier="T0")` or `server.handle(message, headers, tier="T0")`, and the call passes with no
|
|
55
55
|
delegation. A token such a caller presents is still verified and refused when invalid. A T2 caller takes the whole
|
|
56
56
|
profile. Money limits and human gates bind at every tier.
|
|
@@ -66,8 +66,10 @@ job records who started it.
|
|
|
66
66
|
**A partner in the chain** (DEC-0090). A token a partner's agent presents for a customer organization carries the
|
|
67
67
|
partner as the innermost actor: `{partner: "org:<id>"}`, exactly that member, under at least one agent. It counts
|
|
68
68
|
toward the depth limit and ends the chain; outermost, with an `act` beneath, or as `personal` it is
|
|
69
|
-
`malformed-chain`. A node with a `sub` beside `partner` is an agent,
|
|
70
|
-
|
|
69
|
+
`malformed-chain`. A node with a `sub` beside `partner` is checked as an agent, yet `ctx.delegation["chain"]` lists it
|
|
70
|
+
by its `org:` id, as the contract's `chainOf` does; the schema refuses such a node, so a correct issuer never mints one
|
|
71
|
+
(raised with the contract, FAA-64). Job ownership and idempotency keys follow `(principal, caller)` and do not include
|
|
72
|
+
the partner (FAA-64). `chain_depth(act)` /
|
|
71
73
|
`chainDepth(act)` answer the contract's depth, infinity for a chain that is not one. Minting a partner's token (its
|
|
72
74
|
grant, FAC-SEM-124) is the issuer's, not the kit's.
|
|
73
75
|
|
|
@@ -76,19 +78,19 @@ grant, FAC-SEM-124) is the issuer's, not the kit's.
|
|
|
76
78
|
An action with an external effect may need the operator's approval (DL-17, DL-18). The order is fixed: the token,
|
|
77
79
|
then the grant check where there is one, then `verify_approval` / `verifyApproval`, which consumes the approval's
|
|
78
80
|
`jti` last, then the action. Without an approval, answer `approval-required` with
|
|
79
|
-
`action_digest(action)` / `actionDigest(action)`
|
|
81
|
+
`action_digest(action)` / `actionDigest(action)` (the SHA-256 of the action's RFC 8785 form, `jcs`) so the caller
|
|
80
82
|
can ask the hub. When the meter denies a grant check, answer `refusal_from_denial(denial)` /
|
|
81
83
|
`refusalFromDenial(denial)`: the meter's code unchanged, its window and level in `error.data` (DL-21 step 6). Keep consumed ids in `FileJtiStore` (0600, survives a restart); `MemoryJtiStore` is for tests
|
|
82
84
|
and `UNSAFE_NO_JTI_STORE` exists only by name. No store at all, or a store file that cannot be read, is
|
|
83
|
-
`unknown-state
|
|
85
|
+
`unknown-state`, never a pass.
|
|
84
86
|
|
|
85
87
|
## What a refusal looks like
|
|
86
88
|
|
|
87
|
-
DL-19: an MCP tool answers `refusal_tool_result(r)` / `refusalToolResult(r)
|
|
89
|
+
DL-19: an MCP tool answers `refusal_tool_result(r)` / `refusalToolResult(r)`: `isError: true` and
|
|
88
90
|
`structuredContent: {error: {code, message, data: {detail}}}`; an HTTP route answers `refusal_http(r)` /
|
|
89
91
|
`refusalHttp(r)` with the code's status (401 for `unauthenticated`, `token-expired`, `principal-unproven`, with
|
|
90
92
|
`WWW-Authenticate: Bearer error="invalid_token"`; 503 for `issuer-unavailable` and `unknown-state`; 403 for the
|
|
91
|
-
rest). A published key the service cannot use
|
|
93
|
+
rest). A published key the service cannot use (of small order, for another use, or published twice) is the
|
|
92
94
|
issuer's fault, `issuer-unavailable` (DEC-0045); a gate without a trusted issuer refuses everything as
|
|
93
95
|
`no-trusted-issuer`. `detail` is a short machine word; the token, the approval and the keys never appear in a result or a log.
|
|
94
96
|
|
|
@@ -120,4 +122,4 @@ current verified revision as `estate=` / `{ estate }`, as a value or as a functi
|
|
|
120
122
|
## What the kit does not do
|
|
121
123
|
|
|
122
124
|
It does not mint tokens, check attenuation against a parent (the issuer's rule, DL-5), check a grant or a spend
|
|
123
|
-
window (the meter's, DL-21), or decide which actions need an approval
|
|
125
|
+
window (the meter's, DL-21), or decide which actions need an approval; the service decides that.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# The estate record
|
|
1
|
+
# The estate record: what the operator's machines are, signed by their passkey (DEC-0055)
|
|
2
2
|
|
|
3
3
|
Fabric Agent Contract DEC-0055, `docs/specification/estate-record.md`, schema `schemas/estate-record.schema.json`,
|
|
4
|
-
rules `FAC-SEM-072…076`, with contexts (DEC-0082, `docs/specification/contexts.md`, `FAC-SEM-110`, `FAC-SEM-111`)
|
|
4
|
+
rules `FAC-SEM-072…076`, with contexts (DEC-0082, `docs/specification/contexts.md`, `FAC-SEM-110`, `FAC-SEM-111`) and a
|
|
5
|
+
partner's scoped publisher key (DEC-0090, `docs/specification/partner-channel.md`, `FAC-SEM-122`).
|
|
5
6
|
One revision of one person's estate lists:
|
|
6
7
|
- the passkeys that sign it;
|
|
7
8
|
- the machines, with their device keys and a certificate of each machine's hub key;
|
|
@@ -78,13 +79,13 @@ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., esta
|
|
|
78
79
|
estate_revocations=lambda: delta_entries)
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
- **The token's fault**
|
|
82
|
-
key or machine
|
|
82
|
+
- **The token's fault** (no such certification, another key under that `kid`, a `kid` on two machines, a revoked hub
|
|
83
|
+
key or machine) is `unauthenticated` (for an approval, `approval-invalid`) with detail `kid-not-certified`.
|
|
83
84
|
- **The trust source's fault** is `issuer-unavailable`: `estate-unreadable` (a source that throws or answers nothing
|
|
84
85
|
readable, a revision without a number or digest, a window of zero or over 30 days, dates read strictly, `machines` or
|
|
85
86
|
`revocations` not arrays), `estate-outside-window`, `estate-rolled-back` (lower than the applied revision) and
|
|
86
87
|
`estate-forked` (the applied number with another digest). A caller cannot cure these by minting again.
|
|
87
|
-
- An unreadable clock
|
|
88
|
+
- An unreadable clock (not a finite number of seconds a `Date` can hold) is refused `unknown-state` `no-clock`
|
|
88
89
|
before row 1, in every verifier.
|
|
89
90
|
- Only an absent `estate` skips the check. In the Node kit `null` is an unreadable source; in Python, where `None` is
|
|
90
91
|
the absent value, pass a function returning `None` for a source that has nothing.
|
|
@@ -117,6 +118,11 @@ of `[A-Za-z0-9][A-Za-z0-9_-]{0,127}`. Absent is `personal`; `null` is not absent
|
|
|
117
118
|
names the keys that are a partner's (`kid`, `partner: org:<id>`, `contexts`); a placement under such a key is in one
|
|
118
119
|
of the organization contexts listed, never `personal` or another organization. A key is scoped once, and only a key
|
|
119
120
|
`publisherKeys` pins. A key no scope names keeps DEC-0051's meaning, trusted in any context.
|
|
121
|
+
- **Update a copied kit before a revision carries `publisherScopes`.** A service runs the copy of the kit it was built
|
|
122
|
+
with; a copy older than 0.39.0 does not read `publisherScopes`, so it trusts a partner's key in every context and
|
|
123
|
+
finds no rule broken. A service that applies the estate record updates its copy first:
|
|
124
|
+
`fabric_kit.py install <dir> --module fabric_estate` (or `--lang node --module fabric-estate`). Since 0.40.0 the
|
|
125
|
+
estate reader imports the kit's core modules, so the one file alone no longer loads ([kit modules](kit-modules.md)).
|
|
120
126
|
|
|
121
127
|
Every rule is total on hostile JSON, as the contract's are: an `instancePath` names an item by its index as written (a
|
|
122
128
|
non-object before it does not shift it), and a value whose own `toString` member is data is shown as its JSON. Both kits
|
|
@@ -13,7 +13,7 @@ The feed is a view: never copy rows into a second store that can drift from the
|
|
|
13
13
|
|
|
14
14
|
## Reading a feed (FAC-SEM-036)
|
|
15
15
|
|
|
16
|
-
A client of `GET /fabric/v1/events`
|
|
16
|
+
A client of `GET /fabric/v1/events` (or of any token-protected route) sends the token in the
|
|
17
17
|
header the descriptor's `auth.header` names, in the form `auth.scheme` names: the raw token for
|
|
18
18
|
`none`, `Bearer <token>` for `Bearer`. Never assume `Authorization: Bearer`: a client that did
|
|
19
19
|
was refused on every poll and silently dropped every notify event. `token_header(auth, token)` /
|
|
@@ -27,7 +27,7 @@ in the URL.
|
|
|
27
27
|
- `level`: `info` (routine), `notice` (the operator may want to look), `warning`
|
|
28
28
|
(something is degrading), `error` (something failed and needs action).
|
|
29
29
|
- `text`: one sentence, in the operator's language, with the object named and the
|
|
30
|
-
number stated
|
|
30
|
+
number stated: "The Q3 report draft is ready for your approval (12 sections, 3
|
|
31
31
|
languages)." Never a machine id, never a stack trace.
|
|
32
32
|
- `subject`: what it is about (`{type: "report", id: "q3", label: "Q3 report"}`), so a host
|
|
33
33
|
can group.
|
|
@@ -42,11 +42,11 @@ lands on what matters:
|
|
|
42
42
|
|
|
43
43
|
| The operator… | Event | `notify` |
|
|
44
44
|
|---|---|---|
|
|
45
|
-
| must decide or act
|
|
46
|
-
| lost work
|
|
47
|
-
| will lose work soon
|
|
45
|
+
| must decide or act: a choice, an approval, a key to enter | kind says it: `job.awaiting_choice`, `plan.awaiting_approval`, `human_step.opened`; level `notice` | `true` |
|
|
46
|
+
| lost work: a job failed after its retries, a key expired | level `error`; kind `*.failed`, `*.expired` | `true` |
|
|
47
|
+
| will lose work soon: a degraded source that blocks orders | level `warning`, `service.degraded`, **once when it starts** | `true` |
|
|
48
48
|
| asked to be told when a long job finishes | `job.delivered`, level `notice` | `true` only if that person asked; a host may still keep it in Activity |
|
|
49
|
-
| only needs the record
|
|
49
|
+
| only needs the record: progress, retries, recoveries, syncs | `job.started`, `service.recovered`, `*.cleared`, level `info` or `notice` | `false` |
|
|
50
50
|
|
|
51
51
|
- **One episode, one request.** Notify on the transition, never on each poll: a source that
|
|
52
52
|
flaps between degraded and healthy sends `service.degraded` with `notify: true` once, and later
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md
CHANGED
|
@@ -4,14 +4,14 @@ Normative source: the Fabric Agent Contract's `docs/specification/interop.md` (D
|
|
|
4
4
|
at the commit this plugin pins. This page says how the kit implements it; where the two
|
|
5
5
|
differ, the contract is right.
|
|
6
6
|
|
|
7
|
-
A service that other agents
|
|
7
|
+
A service that other agents (and Fabric) call does five things. Each has one helper in
|
|
8
8
|
`scripts/fabric_interop.py` (Python) and `scripts/fabric-interop.mjs` (Node).
|
|
9
9
|
|
|
10
10
|
| Rule | What the service does | Helper |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| C3.1 capability = tool | serves each `mcp` capability of its manifest as the MCP tool of the same name, with the manifest's input schema unchanged, its output schema unchanged
|
|
12
|
+
| C3.1 capability = tool | serves each `mcp` capability of its manifest as the MCP tool of the same name, with the manifest's input schema unchanged, its output schema unchanged (or, for a job, wrapped as `oneOf[result envelope, job handle]`) and annotations from the effect | `tool_for_capability` / `toolForCapability`, `job_tool_output_schema`, `expected_annotations` |
|
|
13
13
|
| C3.2 long work = job | a capability whose work can outlive one request declares `"job": true` in its interop block, returns `{"job": {"id", "status": "working"}}`, and serves `fabric.job.get` and `fabric.job.cancel` | `JobStore`, `McpToolServer` (Python), `unknown_job_result` |
|
|
14
|
-
| C3.2 result | a completed job carries the full result envelope
|
|
14
|
+
| C3.2 result | a completed job carries the full result envelope, the shape a synchronous call returns: `id`, `contractVersion`, `outcome`, `done`, `proof`, `scope`, `notVerified`, `artifacts`, `createdAt`, `producer`, `output`, `usage`, and its `trace` | `result_envelope` / `resultEnvelope` |
|
|
15
15
|
| C3.3 a person decides | the job goes `input_required` with an elicitation: a titled single-select in form mode, and URL mode for anything secret | `choice_request`, `form_request`, `url_request` |
|
|
16
16
|
| C3.4 trace | every call runs as a child span of the caller's `_meta.traceparent`; outgoing calls carry the same trace; the result envelope records it; events about traced work carry `traceId` and `spanId`, events about untraced work none | `child_traceparent`, `trace_ids`, `make_event(..., trace_id=, span_id=)` |
|
|
17
17
|
|
|
@@ -61,7 +61,7 @@ response = server.handle(json_rpc_message) # POST /mcp body in, JSON re
|
|
|
61
61
|
open with a handshake, and a server that refuses it is marked failed), `server/discover`,
|
|
62
62
|
`tools/list` and `tools/call` with JSON responses. Every tool's `outputSchema` must be
|
|
63
63
|
rooted at `type: object` (DEC-0018): `add_tool` refuses any other root, because a client may
|
|
64
|
-
drop the whole tool list over one bad schema
|
|
64
|
+
drop the whole tool list over one bad schema; an SDK-level test will not show it, so check
|
|
65
65
|
with the real client (below). A service already on an MCP SDK keeps
|
|
66
66
|
the SDK and uses only the helpers. The Node kit has the helpers and the same job file
|
|
67
67
|
format, without the dispatcher. `scripts/sample_service.py` is the worked example:
|
|
@@ -73,7 +73,7 @@ DEC-0017 amended by DEC-0018), so its `structuredContent` always conforms; an ev
|
|
|
73
73
|
untraced work carries no trace pair.
|
|
74
74
|
|
|
75
75
|
**Check with a real client**, never only an SDK: `FABRIC_REAL_CLIENT=1 python3 -m unittest
|
|
76
|
-
test.test_real_client` in the adapter repository, or by hand
|
|
76
|
+
test.test_real_client` in the adapter repository, or by hand: a throwaway directory, a
|
|
77
77
|
temporary `--mcp-config` (mode 600, the token in a header) and `--strict-mcp-config`, so
|
|
78
78
|
the operator's own configuration is not touched:
|
|
79
79
|
|
|
@@ -105,7 +105,9 @@ As a provider:
|
|
|
105
105
|
`limit_holds=False`, a `limit-reached` job once `approval_granted=True`, and an `outcome-unknown` job never.
|
|
106
106
|
- **Usage you did not measure is `null`, never 0,** but only toward a caller whose request `_meta.protocols` names
|
|
107
107
|
`fabric-interop/0.2`: pass `request_meta` to `result_envelope` (`usage_toward`). Toward any other caller the kit
|
|
108
|
-
writes the measured lower bound, as before.
|
|
108
|
+
writes the measured lower bound, as before. Either way, record that call's usage receipt with
|
|
109
|
+
`tokens_measured=False` / `tokensMeasured: false` and the tokens you have, so your own usage report counts it in
|
|
110
|
+
`unmeasuredCalls` and does not show the lower bound as complete (DEC-0091, [usage](usage.md)).
|
|
109
111
|
|
|
110
112
|
As a caller: send `request_meta(traceparent)` as your `_meta` (it names `fabric-interop/0.1` and `0.2`), act on every
|
|
111
113
|
`fabric.job.get` answer as `job_caller_action(answer, now)` says, and resend only under
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# The service kit: modules, layers and installing them
|
|
2
|
+
|
|
3
|
+
Since 0.40.0 (FAA-65) the kit is a set of small modules in two languages, not two large files. `scripts/kit.json`
|
|
4
|
+
lists every module in dependency order, per language, with the kit modules it imports and its sha256; `history` keeps
|
|
5
|
+
the digests of every release since 0.8.0, so a copy in a service can be told apart. `scripts/fabric_kit.py` installs a
|
|
6
|
+
module with exactly what it needs and reports a copy's state. Tests: `test/test_fabric_kit.py`,
|
|
7
|
+
`test/test_kit_api.py`, `test/kit-api.test.mjs`.
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Install, status, build](#install-status-build)
|
|
12
|
+
- [The layers](#the-layers)
|
|
13
|
+
- [What 0.39 code keeps](#what-039-code-keeps)
|
|
14
|
+
- [Dates and times](#dates-and-times)
|
|
15
|
+
- [Adding or changing a module](#adding-or-changing-a-module)
|
|
16
|
+
|
|
17
|
+
## Install, status, build
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
K=<adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts
|
|
21
|
+
python3 $K/fabric_kit.py install <service-dir> # fabric_service and its closure (Python)
|
|
22
|
+
python3 $K/fabric_kit.py install <service-dir> --module fabric_service --module fabric_interop --module fabric_board
|
|
23
|
+
# what sample_service.py imports
|
|
24
|
+
python3 $K/fabric_kit.py install <service-dir> --lang node # fabric-service.mjs and its closure
|
|
25
|
+
python3 $K/fabric_kit.py install <service-dir> --module fabric_openrouter --module fabric_board
|
|
26
|
+
python3 $K/fabric_kit.py install <service-dir> --all --dry-run # every module, nothing written
|
|
27
|
+
python3 $K/fabric_kit.py status <service-dir> [--json] # current, older (the release), modified, missing
|
|
28
|
+
python3 $K/fabric_kit.py build [--check] # maintainers: refresh kit.json from the sources
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `install` copies the closure of the named modules and nothing else. It refuses to overwrite a file it does not
|
|
32
|
+
recognise (`modified`: someone edited the copy) unless `--force`, and says which.
|
|
33
|
+
- `status` names each kit file in the folder as `current`, `older` with the release it came from, or `modified`, and
|
|
34
|
+
prints `missing <file>, which <file> imports` for every module the folder lacks, read from what each copy itself
|
|
35
|
+
imports and followed through the modules it would install: that copy would fail when that code path runs. A folder
|
|
36
|
+
holding only the 0.40.0 `fabric_service.py` gets one line for each of the 16 modules it needs; a 0.39.1 copy is
|
|
37
|
+
asked only for what it imports (`test_an_older_copy_is_judged_by_its_own_imports`).
|
|
38
|
+
- Exit codes: 0 done or every copy current, 1 a problem (a copy missing, `older` or `modified`, no kit module in the
|
|
39
|
+
folder at all, an install refused), 2 usage (an unknown `--module` included).
|
|
40
|
+
- Every kit module puts its own folder on `sys.path` before it imports a sibling, so a service that loads a kit file
|
|
41
|
+
by its path from another working directory gets its siblings too (`test_every_module_loads_by_path_from_any_folder`).
|
|
42
|
+
- `build --check` is what CI runs: `kit.json` must be what the sources say.
|
|
43
|
+
|
|
44
|
+
## The layers
|
|
45
|
+
|
|
46
|
+
A module imports only modules listed before it in `kit.json`, so the kit has no import cycle and no upward import
|
|
47
|
+
(`test_the_order_is_a_layering`; a planted cycle is caught). Python name first, the Node twin is the same name with
|
|
48
|
+
`-` for `_` and `.mjs`. No module is over 1000 lines (`test_no_module_over_the_line_ceiling`).
|
|
49
|
+
|
|
50
|
+
| Layer | Module | What it holds |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| L0 | `fabric_jsvalue` | JavaScript's view of JSON values: numbers, strings, equality, `jcs`, `show`; defined once for the kit |
|
|
53
|
+
| L0 | `fabric_time` | strict RFC 3339, UTC days, formatting, day windows, the clock ([dates](#dates-and-times)) |
|
|
54
|
+
| L0 | `fabric_identifiers` | identifier equality by normal form (DEC-0076) |
|
|
55
|
+
| L0 | `fabric_secrets` | the contract's one credential detector (DEC-0044) |
|
|
56
|
+
| L1 | `fabric_crypto` | Ed25519 and P-256 verification, base64url, sha256: what the verifiers need and nothing more |
|
|
57
|
+
| L2 | `fabric_estate` | `estate-record/1` revisions and their rules (DEC-0055) |
|
|
58
|
+
| L2 | `fabric_delegation` | the callee's side of `fabric-delegation/0.1` (DEC-0039) |
|
|
59
|
+
| L2 | `fabric_share` | share grants and registrations (DEC-0086) |
|
|
60
|
+
| L2 | `fabric_ceremonies` | the headless join token (DEC-0084) |
|
|
61
|
+
| L2 | `fabric_package` | `fabric-package/1`: pack, sign, verify (DEC-0050, DEC-0051) |
|
|
62
|
+
| L3 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
63
|
+
| L3 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
|
|
64
|
+
| L3 | `fabric_paths` | the services directory, a service's folders, atomic and private writes |
|
|
65
|
+
| L3 | `fabric_protocols` | protocol revisions (DEC-0067) and feed data (DEC-0070) |
|
|
66
|
+
| L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
|
|
67
|
+
| L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
|
|
68
|
+
| L3 | `fabric_backups` | settings backups (DEC-0025) |
|
|
69
|
+
| L3 | `fabric_descriptor` | the descriptor, peers, app descriptors, the well-known document |
|
|
70
|
+
| L3 | `fabric_auth` | the token, the network guard, login codes |
|
|
71
|
+
| L3 | `fabric_events` | events, paging, the JSONL event log (DEC-0029) |
|
|
72
|
+
| L3 | `fabric_logs` | rotating logs, the stop signal and the drain (LC-12) |
|
|
73
|
+
| L3 | `fabric_supervisors` | launchd, systemd user units, Task Scheduler (DEC-0032, LC-14) |
|
|
74
|
+
| L3 | `fabric_service` | the facade: every name 0.39.1 exported, re-exported from the modules above |
|
|
75
|
+
| L3 | `fabric_profiles` | runner and model profiles ([profiles](profiles.md)) |
|
|
76
|
+
| L3 | `fabric_interop` | `fabric-interop/0.1`: jobs, envelopes, MCP tools ([interop](interop.md)) |
|
|
77
|
+
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
78
|
+
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
79
|
+
| L4 | `fabric_board_session` | the board responder: a session, the checkpoint, the fenced request |
|
|
80
|
+
| L4 | `fabric_board_fixture` | the scripted board the probe runs a responder against |
|
|
81
|
+
| L4 | `fabric_board` | the facade of the board connector ([board](board.md)) |
|
|
82
|
+
| L4 | `fabric_http` | outbound HTTP: deadline, bounded retries, `Retry-After`, size cap ([connectors](connectors.md)) |
|
|
83
|
+
| L4 | `fabric_openrouter` | OpenRouter chat: own key, spend ceiling, receipts, fallback ([connectors](connectors.md)) |
|
|
84
|
+
|
|
85
|
+
Beside the kit, not in `kit.json`: the probe (`check_service.py`, `check_service_rules.py`), the sample
|
|
86
|
+
(`sample_service.py`, `sample-remote-service.mjs`), `fabric_kit.py` itself and the dashboard tools. Recipes built on
|
|
87
|
+
the kit live in `../recipes/` ([the support agent](../recipes/support-agent/README.md)).
|
|
88
|
+
|
|
89
|
+
## What 0.39 code keeps
|
|
90
|
+
|
|
91
|
+
`import fabric_service as fs` and `import ... from './fabric-service.mjs'` keep working: the facades re-export every
|
|
92
|
+
public name 0.39.1 had, in both languages (the snapshot is `test/fixtures/kit-api/kit-api-0.39.1.json`; a missing
|
|
93
|
+
name fails `test_kit_api`), and the Python facade the private names other code reached through it (`_number`,
|
|
94
|
+
`_DAY_MS`, `_utc_date_ms` from the core). `fabric_board` and `fabric-board.mjs` are
|
|
95
|
+
facades the same way. What changed for a service that copies the kit: `fabric_service.py` alone is no longer enough;
|
|
96
|
+
install with `fabric_kit.py install` and `status` names what is missing. A test or service that replaced a function
|
|
97
|
+
by assigning it on the facade (`fabric_service.X = ...`) no longer changes what the kit does: the code reads its own
|
|
98
|
+
module's name, so patch the module that defines it (`fabric_lock.X`, `fabric_session_env.X`). A service that imported a module-private
|
|
99
|
+
helper (`_x`) of the Node twin should import it from its module now.
|
|
100
|
+
|
|
101
|
+
## Dates and times
|
|
102
|
+
|
|
103
|
+
`fabric_time` / `fabric-time.mjs` is the kit's one reading of dates, on the cases both twins test
|
|
104
|
+
(`test/fixtures/kit-time-cases.json`). Anything unreadable is NaN, never an exception, so a check compares it and
|
|
105
|
+
reports a finding.
|
|
106
|
+
|
|
107
|
+
| Python | Node | What |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `parse_rfc3339_ms(text)` | `parseRfc3339Ms(text)` | strict RFC 3339: a `T`, an offset and a day that exists; no space, no leap second; milliseconds or NaN |
|
|
110
|
+
| `utc_date_ms(text)` | `utcDateMs(text)` | a `YYYY-MM-DD` that exists, as midnight UTC in milliseconds, or NaN |
|
|
111
|
+
| `utc_day(ms)` | `utcDay(ms)` | the UTC calendar day of an instant (NaN or Infinity raise) |
|
|
112
|
+
| `format_rfc3339(ms)` | `formatRfc3339(ms)` | `2026-10-21T09:00:00.000Z`, which parses back to the same value |
|
|
113
|
+
| `day_window(today_ms, days)` | `dayWindow(todayMs, days)` | the last N UTC days ending with today's, oldest first (a usage report's 31) |
|
|
114
|
+
| `clock_ms(at)` | `clockMs(at)` | a verifier's clock (an aware `datetime` / a `Date`, or epoch seconds) in milliseconds |
|
|
115
|
+
| `now_ms(clock)` | `nowMs(clock)` | now in milliseconds; `clock` returns seconds in Python (`time.time`), milliseconds in Node (`Date.now`) |
|
|
116
|
+
| `DAY_MS` | `DAY_MS` | 86 400 000 |
|
|
117
|
+
|
|
118
|
+
`fabric_estate.estate_time` is `parse_rfc3339_ms` under its older name.
|
|
119
|
+
|
|
120
|
+
## Adding or changing a module
|
|
121
|
+
|
|
122
|
+
1. Put it in the lowest layer whose modules it needs; import only modules listed before it.
|
|
123
|
+
2. Write the Node twin with the same public names in camelCase, and shared cases under `test/fixtures/` that both
|
|
124
|
+
test files read.
|
|
125
|
+
3. Add it to `kit.json` (module, file, layer) and run `fabric_kit.py build`; add it to the table above
|
|
126
|
+
(`test_every_kit_module_is_documented`).
|
|
127
|
+
4. A value or helper the core already has is imported, never redefined (`test_the_core_is_defined_once`).
|
|
128
|
+
5. Stay under 1000 lines; split by responsibility, keeping a facade when callers already import the old name.
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Lifecycle
|
|
1
|
+
# Lifecycle: supervisor, install, release layout, state
|
|
2
2
|
|
|
3
3
|
**This page is the rule** for anyone who builds, installs or runs a `fabric-service/0.1` service with this
|
|
4
4
|
skill. Everything you must follow is stated here or in the public Fabric Agent Contract's
|
|
@@ -56,17 +56,17 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
|
|
|
56
56
|
| Id | Rule | Here | Kit (Python / Node) |
|
|
57
57
|
|---|---|---|---|
|
|
58
58
|
| LC-01 | **Quit reaches exit within a deadline.** Every trigger (`SIGTERM`, `SIGINT`, logout, the app's own quit) ends the process: within 5 s idle, within 10 s with work in flight, then a hard exit. Nothing starts after quit begins | [Shutdown](#shutdown) | `Drain` / `Drain` |
|
|
59
|
-
| LC-02 | **Every child has an owner, a deadline and a process group.** No subprocess without a timeout; on timeout or the parent's exit the whole group goes; no orphan survives. Locks and shared resources are released in `finally` | your own code |
|
|
59
|
+
| LC-02 | **Every child has an owner, a deadline and a process group.** No subprocess without a timeout; on timeout or the parent's exit the whole group goes; no orphan survives. Locks and shared resources are released in `finally` | your own code | none |
|
|
60
60
|
| LC-03 | **A job is bounded and exclusive.** One copy holds `service.lock`; a scheduled job has a watchdog shorter than its interval, shares its lock with every manual entry point, bounds every network call and records each run's outcome. Long work is journalled before it starts | [One copy under a supervisor](#one-copy-under-a-supervisor) | `hold_single_instance` / `holdSingleInstance` |
|
|
61
|
-
| LC-04 | **Credentials are silent after setup.** No background prompt; a credential read that is denied or absent is cached and backs off until a person acts | [Directories](#directories) (secret files 0600) |
|
|
62
|
-
| LC-05 | **Identity is stable across updates.** The supervisor runs a stable executable inside a release directory, never an interpreter by a versioned package-manager path; signed builds are pinned by team and identifier | [launchd plist](#launchd-plist-generate-it-with-launchd_plist-or-launchdplist-both-kits-write-the-same-bytes) |
|
|
61
|
+
| LC-04 | **Credentials are silent after setup.** No background prompt; a credential read that is denied or absent is cached and backs off until a person acts | [Directories](#directories) (secret files 0600) | none |
|
|
62
|
+
| LC-05 | **Identity is stable across updates.** The supervisor runs a stable executable inside a release directory, never an interpreter by a versioned package-manager path; signed builds are pinned by team and identifier | [launchd plist](#launchd-plist-generate-it-with-launchd_plist-or-launchdplist-both-kits-write-the-same-bytes) | none |
|
|
63
63
|
| LC-09 | **The footprint is declared.** The service's `AGENTS.md` names every resident process, its supervisor label, unit or task and its port, who starts each and who stops it | the descriptor and the supervisor file | `write_descriptor`, `launchd_plist`, `systemd_unit` / `systemdUnit`, `task_xml` / `taskXml` |
|
|
64
64
|
| LC-11 | **An update is a lifecycle event.** An upgrade writes a new immutable release and restarts through the supervisor; the previous release is kept for rollback, older ones are pruned | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `prune_releases` / `pruneReleases` |
|
|
65
65
|
| LC-12 | **Every file is bounded.** Logs rotate by size (5 × 5 MB), mode 0600, never in `/tmp`; temporary leftovers are swept at start | [Logs](#logs) | `RotatingLog`, `cap_stdout_log` / `RotatingLog`, `capStdoutLog` |
|
|
66
66
|
| LC-14 | **The operator's intent wins; uninstall is symmetric.** No install or upgrade re-enables what the operator turned off; uninstall removes what install added (supervisor file, override, descriptor), and data goes only on an explicit purge | [Install, upgrade, uninstall](#install-upgrade-uninstall), [Linux and Windows](#linux-and-windows) | `launchd_install` / `launchdInstall`, `launchd_uninstall` / `launchdUninstall`; `systemd_install` / `systemdInstall`, `task_install` / `taskInstall` and their uninstalls |
|
|
67
67
|
| LC-15 | **Builds clean up after themselves.** At most the current release and the one before it remain; the prune runs inside the installer, not later | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `prune_releases` / `pruneReleases` |
|
|
68
68
|
| LC-21 | **A secret never travels in a command line.** Keys reach the process through its environment, a 0600 file or a vault, never argv; a supervisor file carries no secret | [launchd plist](#launchd-plist-generate-it-with-launchd_plist-or-launchdplist-both-kits-write-the-same-bytes), [Linux and Windows](#linux-and-windows) | the supervisor-file writers refuse a secret-shaped value |
|
|
69
|
-
| LC-22 | **Work survives a restart.** Work started but not finished is journalled on disk, without secrets, before it starts; the restarted service takes it back or cleans it up | [Shutdown](#shutdown) |
|
|
69
|
+
| LC-22 | **Work survives a restart.** Work started but not finished is journalled on disk, without secrets, before it starts; the restarted service takes it back or cleans it up | [Shutdown](#shutdown) | none (the service's own journal) |
|
|
70
70
|
|
|
71
71
|
## launchd plist (generate it with `launchd_plist` or `launchdPlist`; both kits write the same bytes)
|
|
72
72
|
|
|
@@ -78,8 +78,8 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
|
|
|
78
78
|
| `KeepAlive` | `true` | `{SuccessfulExit:false}` leaves a cleanly exited service down |
|
|
79
79
|
| `ThrottleInterval` | `10` | bounds a crash loop |
|
|
80
80
|
| `ExitTimeOut` | above the drain and its hard exit (15 s default; `Drain` ends by 10 s) | SIGKILL arrives after it, so it must never come first |
|
|
81
|
-
| `ProcessType` | `Standard
|
|
82
|
-
| `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets; `FABRIC_SERVICE_SUPERVISOR=launchd` | never a secret value
|
|
81
|
+
| `ProcessType` | `Standard`, never `Background`, and no `Nice` > 0 or `LowPriorityIO` | a host probes the service and agents call it while the Mac is busy; a background job is starved for tens of seconds under load and every host reports an outage that never happened (the probe's `lifecycle.priority` rule fails it) |
|
|
82
|
+
| `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets; `FABRIC_SERVICE_SUPERVISOR=launchd` | never a secret value: the plist is world-readable in backups; the supervisor marker is how the lock knows to back off (the probe's `lifecycle.supervisor-marker` rule fails a plist without it: a copy that finds the lock held would exit 75 at once into a respawn every `ThrottleInterval`) |
|
|
83
83
|
| `StandardOutPath`/`StandardErrorPath` | `~/Library/Logs/<id>/service.log` | one place the host tails |
|
|
84
84
|
|
|
85
85
|
## Install, upgrade, uninstall
|
|
@@ -103,13 +103,13 @@ that document until `service.id` matches (at most 40 s) before it calls the serv
|
|
|
103
103
|
rewrites the plist, so the new release is what starts once they turn it back on, and
|
|
104
104
|
answers `{"disabled": true, ...}` instead of the well-known document. `force_enable=True`
|
|
105
105
|
is for an operator who asked for the service to be switched on. The service never
|
|
106
|
-
implements these verbs itself
|
|
106
|
+
implements these verbs itself; its CLI calls launchctl the same way.
|
|
107
107
|
|
|
108
108
|
**Uninstall is symmetric.** `launchd_uninstall` boots the job out and **waits** until
|
|
109
109
|
launchd reports it gone (it raises, removing nothing, if the job is still loaded at the
|
|
110
110
|
timeout), removes the plist, and resets the override to `enabled`. launchctl has no verb
|
|
111
111
|
that deletes an override, and a leftover `disabled` would make a later fresh install stay
|
|
112
|
-
off. Tests use a throwaway label or a fake `launchctl` first on `PATH
|
|
112
|
+
off. Tests use a throwaway label or a fake `launchctl` first on `PATH`, never the
|
|
113
113
|
operator's real jobs.
|
|
114
114
|
|
|
115
115
|
## One copy under a supervisor
|
|
@@ -118,7 +118,7 @@ Started by hand, a copy that finds `service.lock` held prints one sentence namin
|
|
|
118
118
|
holder (or saying the holder is unknown, when its pid cannot be read) and exits 75 at once.
|
|
119
119
|
Under launchd that exit is a respawn every `ThrottleInterval` (10 s), forever, and under
|
|
120
120
|
systemd one every `RestartSec`. So a copy its supervisor started, and that says so with the
|
|
121
|
-
**supervisor marker**, backs off in-process
|
|
121
|
+
**supervisor marker**, backs off in-process (0.5 s doubling to 30 s, 300 s in all), takes over
|
|
122
122
|
if the holder leaves, and exits 75 only when the wait runs out: a few starts an hour instead of
|
|
123
123
|
360. It touches nothing while it waits. Two installs sharing one data directory are still the
|
|
124
124
|
defect to remove; the back-off only stops the loop from costing the machine.
|
|
@@ -136,8 +136,8 @@ never infers supervision from its parent process.
|
|
|
136
136
|
`supervised` (with the manager), `manual` or `conflict`. Only an empty variable counts as no
|
|
137
137
|
marker; a bare `--fabric-supervisor` (followed by nothing, by `--` or by another `--…` flag,
|
|
138
138
|
none of which it consumes) names no manager.
|
|
139
|
-
- Markers that disagree
|
|
140
|
-
twice, or a bare or empty argument beside the variable
|
|
139
|
+
- Markers that disagree (the variable and the argument with different values, the argument
|
|
140
|
+
twice, or a bare or empty argument beside the variable) exit 75 at once, before the lock and
|
|
141
141
|
whether or not it is held, naming both markers.
|
|
142
142
|
- A service's own argument parser accepts the flag in both spellings and ignores it: in Python,
|
|
143
143
|
`add_supervisor_argument(parser)` (argparse otherwise exits 2 on an unknown flag); the sample
|
|
@@ -152,8 +152,8 @@ never infers supervision from its parent process.
|
|
|
152
152
|
`"lifecycle": {"manager": "person", "app": "<the installed bundle or executable>"}`, `installedBy {by: "person", role,
|
|
153
153
|
tool}` and no `origin`: it says the app is installed, never that it runs. The installer writes it; where there is none
|
|
154
154
|
(a macOS app dragged into Applications), the app writes it on a start when `app_descriptor_action` /
|
|
155
|
-
`appDescriptorAction` answers `write`
|
|
156
|
-
`lifecycle.app`
|
|
155
|
+
`appDescriptorAction` answers `write` (no descriptor yet, another version in `installedBy.tool`, or another path than
|
|
156
|
+
`lifecycle.app`), and never from an App Translocation path. Its live origin and process id go in its own liveness file,
|
|
157
157
|
rewritten on every start and removed on quit; a caller that sends anything reads that file and checks the process.
|
|
158
158
|
`write_descriptor` claims no port for it, and a reader that finds `lifecycle.app` gone treats the descriptor as stale.
|
|
159
159
|
|
|
@@ -179,8 +179,8 @@ anything the operator would miss belongs in data.
|
|
|
179
179
|
|
|
180
180
|
## Logs
|
|
181
181
|
|
|
182
|
-
One structured log (JSON lines) rotated by size
|
|
183
|
-
5 × 5 MB by default, files 0600 in a 0700 directory
|
|
182
|
+
One structured log (JSON lines) rotated by size (`RotatingLog(dirs["logs"] / "service.jsonl")`,
|
|
183
|
+
5 × 5 MB by default, files 0600 in a 0700 directory), plus the launchd stdout file, which
|
|
184
184
|
`cap_stdout_log` copies to `.1` and truncates at start once it passes 5 MB (launchd holds it
|
|
185
185
|
open for append, so it is truncated in place, never renamed). Configure logging once, in the
|
|
186
186
|
entry point. Never log a token, a cookie, a login code or a request body that may carry one.
|
|
@@ -189,7 +189,7 @@ entry point. Never log a token, a cookie, a login code or a request body that ma
|
|
|
189
189
|
|
|
190
190
|
On `SIGTERM`: stop accepting new work, append a `service.stopping` event, drain
|
|
191
191
|
in-flight work, persist what was interrupted so the next start resumes it, release the
|
|
192
|
-
lock, exit 0
|
|
192
|
+
lock, exit 0, within 10 s with work in flight (LC-01). `Drain` does the timing:
|
|
193
193
|
|
|
194
194
|
```python
|
|
195
195
|
drain = fs.Drain() # drain 8 s, hard exit 2 s later (EXIT_HARD_STOP, 70)
|
|
@@ -208,8 +208,8 @@ A host may run a lifecycle broker: one always-on per-user service that agents as
|
|
|
208
208
|
stop and restart products, instead of launching or killing them themselves. A product it manages
|
|
209
209
|
has to survive being driven from outside:
|
|
210
210
|
|
|
211
|
-
- **Quit reaches exit through both doors.** A broker asks the platform's own quit first
|
|
212
|
-
macOS `NSRunningApplication.terminate()`, the path Cmd+Q and the Dock take
|
|
211
|
+
- **Quit reaches exit through both doors.** A broker asks the platform's own quit first (on
|
|
212
|
+
macOS `NSRunningApplication.terminate()`, the path Cmd+Q and the Dock take) and `SIGTERM` a few
|
|
213
213
|
seconds later. Handle both and exit inside the deadline (LC-01). A desktop app
|
|
214
214
|
that answers `SIGTERM` by closing its window and staying alive is stopped late or not at all.
|
|
215
215
|
- **A background launch stays in the background.** A broker opens the app non-activating
|
|
@@ -219,8 +219,8 @@ has to survive being driven from outside:
|
|
|
219
219
|
- **Identity is the designated requirement.** A broker pins the app by team + bundle id, so an
|
|
220
220
|
update signed by the same team keeps working and anything else is refused (LC-05). Never ship an
|
|
221
221
|
ad-hoc or differently signed build to a machine that pins you.
|
|
222
|
-
- **Starting is not ready.** Expose readiness the broker can probe without a person
|
|
223
|
-
well-known document for a service, a process-level answer for a plain app
|
|
222
|
+
- **Starting is not ready.** Expose readiness the broker can probe without a person (the
|
|
223
|
+
well-known document for a service, a process-level answer for a plain app) and keep it honest
|
|
224
224
|
while the app is still starting.
|
|
225
225
|
|
|
226
226
|
## Linux and Windows
|
|
@@ -230,7 +230,7 @@ Both kits write and install the supervisor file the contract names (DEC-0032, se
|
|
|
230
230
|
secret: the writers refuse a credential-shaped argument, environment name or value, and any
|
|
231
231
|
control character (a line break would end a unit line early).
|
|
232
232
|
|
|
233
|
-
**Linux
|
|
233
|
+
**Linux: a systemd user unit.** `systemd_unit(unit, argv, working_directory=…)` /
|
|
234
234
|
`systemdUnit(unit, argv, {workingDirectory})` writes `Restart=always`, `RestartSec=10`,
|
|
235
235
|
`TimeoutStopSec` above the drain (15 s default), `KillMode=mixed`, `WantedBy=default.target`
|
|
236
236
|
and `FABRIC_SERVICE_SUPERVISOR=systemd` (always, whatever the caller passed), quoting every argument (`%` → `%%`, `$` → `$$`).
|
|
@@ -247,7 +247,7 @@ descriptor says `"lifecycle": {"manager": "systemd", "unit": "<id>.<instance>.se
|
|
|
247
247
|
A user unit runs while the person is logged in; one that must run without a session needs
|
|
248
248
|
`loginctl enable-linger <user>`, which is the operator's decision, not the installer's.
|
|
249
249
|
|
|
250
|
-
**Windows
|
|
250
|
+
**Windows: a per-user Scheduled Task.** `task_xml(task, argv, working_directory=…)` /
|
|
251
251
|
`taskXml(task, argv, {workingDirectory})` writes a task with a logon trigger for the current
|
|
252
252
|
user, `InteractiveToken` and `LeastPrivilege` (no administrator rights), restart on failure
|
|
253
253
|
every minute, no execution time limit and `MultipleInstancesPolicy` `IgnoreNew`, its settings in the
|
|
@@ -269,16 +269,16 @@ starting a second copy. Measured (FAA-38, [report](https://github.com/passioncod
|
|
|
269
269
|
that still exits 75 stays down until the next logon.
|
|
270
270
|
|
|
271
271
|
**One copy on each system.** The Python kit takes `flock` (Linux) or a locked byte
|
|
272
|
-
(Windows); the Node kit cannot, so it holds a name instead
|
|
272
|
+
(Windows); the Node kit cannot, so it holds a name instead: an abstract unix socket on Linux,
|
|
273
273
|
a named pipe on Windows (`lock_name` / `lockName`), each gone with the process. The Python kit
|
|
274
274
|
holds the same name beside its file lock, so the two kits exclude each other everywhere, and
|
|
275
275
|
`check_service.py` reports the lock held when either is. The probe only looks (`/proc/net/unix`,
|
|
276
276
|
`WaitNamedPipeW`); it never takes the name, so it cannot make a service that is starting fail.
|
|
277
277
|
|
|
278
278
|
**Token files on Windows.** The privacy of `service.token` is its ACL (service.md *Windows
|
|
279
|
-
token files*): the kits write a protected list
|
|
279
|
+
token files*): the kits write a protected list: the user, SYSTEM and Administrators, set before the
|
|
280
280
|
token's bytes go in, any entry already there dropped. On read, `windows_token_acl_problem` /
|
|
281
|
-
`windowsTokenAclProblem` decide over what was read
|
|
281
|
+
`windowsTokenAclProblem` decide over what was read: the file's flags, its owner SID and each ACE
|
|
282
282
|
as `{type, sid, mask}` (DEC-0057, `FAC-SEM-079`, held to the contract's
|
|
283
283
|
`fixtures/vectors/windows-token.json`): a reparse point, a directory, a real path outside the
|
|
284
284
|
profile, another owner, an allow ACE with a non-zero mask for any other SID, an ACE of another type
|
|
@@ -8,7 +8,7 @@ Order matters: each step leaves the service answering.
|
|
|
8
8
|
2. **Add the well-known route and the build identity.** Read-only, no risk.
|
|
9
9
|
3. **Add the events view** over the existing log, then the login-code routes. Keep the
|
|
10
10
|
service's old login path until the operator has switched.
|
|
11
|
-
4. **Move the lock to the top of startup
|
|
11
|
+
4. **Move the lock to the top of startup**, before the startup hook that resumes jobs.
|
|
12
12
|
Test: a second copy exits 75 and the first copy's running jobs are untouched.
|
|
13
13
|
5. **Move state out of the code tree** if it lives there: copy to the OS data
|
|
14
14
|
directory, switch the paths, keep the old tree read-only for one release, then
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Packing, signing and verifying
|
|
1
|
+
# Packing, signing and verifying: `fabric-package/1`
|
|
2
2
|
|
|
3
3
|
Contract DEC-0050 (format and digest), DEC-0051 (signatures), DEC-0071 and DEC-0074 (a consumer agent), rules
|
|
4
4
|
`FAC-SEM-064`…`066` and `FAC-SEM-102`, all at this plugin's pin. The kit: `scripts/fabric_package.py` and
|
|
@@ -117,4 +117,5 @@ PA-03).
|
|
|
117
117
|
- **The organization's countersignature as a passkey assertion.** It waits for `org-countersign/1` and
|
|
118
118
|
`org-authority/1`; this version's countersignature is a detached Ed25519 signature.
|
|
119
119
|
- **Fabric recording packages by `agentRef`.** It waits for the ADR-0138 D2b amendment.
|
|
120
|
-
- **`publish --to org`** (FAA-28
|
|
120
|
+
- **`publish --to org`** (FAA-28; an organization cloud's import API is FAA-63, under FAA-28's rule that publishing to
|
|
121
|
+
an organization is the person's own act).
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md
CHANGED
|
@@ -27,7 +27,7 @@ resolution = fp.resolve_model_profile(catalogue=catalogue, profiles=estate_and_a
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
The first level that names a profile wins: **request, agent, binding or project, estate default**. The project's
|
|
30
|
-
`modelClasses` narrow the profile's models
|
|
30
|
+
`modelClasses` narrow the profile's models (a model stays only if every class the catalogue gives it is allowed) and
|
|
31
31
|
never choose one. `open-vendor` is descriptive (DEC-0083): it is never checked against the policy, and a model whose
|
|
32
32
|
only class is `open-vendor` does not stay. An empty result is `{"refused": "model-classes-empty", "policy": {id, revision}}`: refuse the launch
|
|
33
33
|
and name the policy; never fall back to a model outside it. No level at all is `no-profile`; an id nobody offers is
|
|
@@ -37,17 +37,17 @@ and name the policy; never fall back to a model outside it. No level at all is `
|
|
|
37
37
|
|
|
38
38
|
`profile_problems(kind, doc)` / `profileProblems(kind, doc)` returns the contract's findings for a `model-catalogue`
|
|
39
39
|
(a model catalogued once; a provider never both ordered and ignored, a base slug covering its variants), a
|
|
40
|
-
`model-profiles` set (an id once), a `runner-profile` (a subscription only in its vendor's runner
|
|
40
|
+
`model-profiles` set (an id once), a `runner-profile` (a subscription only in its vendor's runner: Claude in Claude
|
|
41
41
|
Code, Kimi in Kimi Code, ChatGPT in Codex; a key route's allow-list is its own model profile; a key named by reference,
|
|
42
42
|
never by value) and a recorded `profile-resolution` (it equals the order's answer). Every rule compares as the contract
|
|
43
|
-
does
|
|
44
|
-
equals nothing
|
|
43
|
+
does: `===` (`1` and `true` differ, `null` is not absent) and its `jsonEqual` for a recorded resolution, where NaN
|
|
44
|
+
equals nothing. It never raises on hostile JSON: a value whose own `toString` member is data is shown as its JSON.
|
|
45
45
|
|
|
46
46
|
## A lane that reads user content
|
|
47
47
|
|
|
48
|
-
A model profile names what the lane it serves reads (DEC-0083, `reads`): `user-content
|
|
49
|
-
the public sends (messages, questions, images, reviews, screenshots, voice); `operator
|
|
50
|
-
operator or its agents write; `web
|
|
48
|
+
A model profile names what the lane it serves reads (DEC-0083, `reads`): `user-content`: what a member, a client or
|
|
49
|
+
the public sends (messages, questions, images, reviews, screenshots, voice); `operator`: only what the estate's own
|
|
50
|
+
operator or its agents write; `web`: pages fetched from the open web. Absent is "not declared", which stays valid.
|
|
51
51
|
|
|
52
52
|
A profile that reads `user-content` has an **open-vendor route** (FAC-SEM-112): with `fallback: none` (or any value the
|
|
53
53
|
contract does not name) its first model is `open-vendor` in the catalogue; with `between-turns` at least one of its
|
|
@@ -83,7 +83,7 @@ and answers whether the runner is installed, which version answers and whether i
|
|
|
83
83
|
catalogued binary found on `PATH`, only with the catalogued version argv (and `auth` argv), without a shell, within
|
|
84
84
|
the entry's timeout (at most 5 s); an entry outside these rules is refused and nothing runs. `model_usable(model,
|
|
85
85
|
catalogue, policy)` names why a model cannot be used here: not catalogued, a class the project does not allow, or
|
|
86
|
-
only `open-vendor`, which admits nothing
|
|
86
|
+
only `open-vendor`, which admits nothing: the same admission the resolution applies.
|
|
87
87
|
|
|
88
88
|
`probe` is the contract's closed probe result for that runner, the one Fabric's walk records (DEC-0026):
|
|
89
89
|
`not-installed` (no catalogued binary on `PATH`), `not-responding` (the version probe did not answer in its bound,
|
|
@@ -102,12 +102,12 @@ contract's `routeRules` finding for finding:
|
|
|
102
102
|
| `kind` | value | rules |
|
|
103
103
|
|---|---|---|
|
|
104
104
|
| `runner-route` | the route | one candidate per runner kind (FAC-SEM-028); a candidate can attach or spawn (029) |
|
|
105
|
-
| `route-bundle` | `{route, admissions, binding?, catalogue?}` | every candidate's provider revision admitted for the capability by id, revision and hash (030); the binding agrees
|
|
105
|
+
| `route-bundle` | `{route, admissions, binding?, catalogue?}` | every candidate's provider revision admitted for the capability by id, revision and hash (030); the binding agrees (project, capability, local-runner, a candidate as provider, this route as `runnerRoute`) (032); every kind and named drive is in the runner catalogue (033) |
|
|
106
106
|
| `route-event` | `{route, event}` | the event names this route, project and capability; candidates by position; the probes cover every candidate above the selection in order (all for an exhausted walk, none for a sticky relaunch); attach, spawn, `no-held-session` and `spawn-failed` obey the session policy; `held` only under `exhausted: hold`; `preferred-available` only under `recovery: reprobe`, moving up (034) |
|
|
107
107
|
|
|
108
108
|
A probe result in an event is one of `PROBE_RESULTS`; a host never appends a runner the route does not name.
|
|
109
109
|
|
|
110
110
|
## Usage by route
|
|
111
111
|
|
|
112
|
-
`make_usage_receipt(..., route="subscription"|"key")` records how the call was paid
|
|
112
|
+
`make_usage_receipt(..., route="subscription"|"key")` records how the call was paid: the operator's own login in its
|
|
113
113
|
vendor's runner, or an issued key. The usage report keeps the rows of one model on two routes apart.
|