@passioncode-ai/passioncode 0.2.40 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +4 -1
  10. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +3 -2
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +22 -0
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +44 -6
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +46 -8
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -4
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +7 -2
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +30 -15
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +5 -0
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +17 -9
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +606 -0
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +48 -525
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +43 -6
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +14 -4
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +48 -15
  26. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +4 -3
  27. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +39 -16
  28. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +8 -1
  29. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -11
  30. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +86 -642
  31. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +766 -0
  32. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +70 -12
  33. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +59 -8
  34. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +56 -16
  35. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +4 -2
  36. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +220 -21
  37. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  38. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +4 -1
  39. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -0
  40. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +69 -34
  41. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +27 -15
  42. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +13 -7
  43. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +8 -0
  44. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -9
  45. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +60 -1
  46. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +10 -3
  47. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +39 -4
  48. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.42 - 2026-10-26
4
+
5
+ ### Changed
6
+
7
+ - **Fabric Agent Adapter is pinned at `v0.52.0`** (it was `v0.51.2`): the contract release `v0.1.0+2026-10-26.12`,
8
+ with where the operator said an approval travelling with a call, a hub key per organization context, a job's
9
+ approval step read under the operator's mandate, and Fabric's rules for checking in every three minutes and for being
10
+ seen correctly. Notes are in the adapter's CHANGELOG.
11
+
12
+ ## 0.2.41 - 2026-10-26
13
+
14
+ ### Changed
15
+
16
+ - **Fabric Agent Adapter is pinned at `v0.51.2`** (it was `v0.51.1`): the reminders Fabric gives on a turn (from
17
+ Fabric 0.3.14) reach the agent at its next prompt, once each. Notes are in the adapter's CHANGELOG.
18
+
3
19
  ## 0.2.40 - 2026-10-26
4
20
 
5
21
  ### Changed
package/family.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "name": "fabric-agent-adapter",
9
9
  "displayName": "Fabric Agent Adapter",
10
10
  "repo": "passioncode-ai/fabric-agent-adapter",
11
- "ref": "v0.51.1",
11
+ "ref": "v0.52.0",
12
12
  "kind": "plugin",
13
13
  "path": "plugins/fabric-agent-adapter",
14
14
  "legacyPluginIds": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/passioncode",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
5
5
  "bin": {
6
6
  "passioncode": "bin/passioncode.js"
@@ -11,7 +11,7 @@
11
11
  "name": "fabric-agent-adapter",
12
12
  "displayName": "Fabric Agent Adapter",
13
13
  "source": "./plugins/fabric-agent-adapter",
14
- "version": "0.51.1",
14
+ "version": "0.52.0",
15
15
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
16
16
  "author": {
17
17
  "name": "PassionCode.ai",
@@ -35,7 +35,7 @@
35
35
  "name": "passioncode",
36
36
  "displayName": "PassionCode.ai",
37
37
  "source": "./plugins/passioncode",
38
- "version": "0.2.40",
38
+ "version": "0.2.42",
39
39
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
40
40
  "author": {
41
41
  "name": "PassionCode.ai",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "family": "passioncode",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "release": true,
5
5
  "members": [
6
6
  {
7
7
  "name": "fabric-agent-adapter",
8
8
  "displayName": "Fabric Agent Adapter",
9
9
  "repo": "passioncode-ai/fabric-agent-adapter",
10
- "ref": "v0.51.1",
11
- "commit": "37a3dee395a5cfbcc86b8523bcfcbb50111d82d4",
12
- "version": "0.51.1",
10
+ "ref": "v0.52.0",
11
+ "commit": "25383f4058c70d365fb5903b0ed8cde218cbe385",
12
+ "version": "0.52.0",
13
13
  "via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
14
14
  "skills": [
15
15
  "adapting-projects-to-fabric",
@@ -23,7 +23,7 @@
23
23
  "legacyMarketplaces": [
24
24
  "fabric-agent-adapter"
25
25
  ],
26
- "contentHash": "sha256:73fca6258f066f80776eae0a3abc2cd23c09e2de77ba0d47d24681ac25e7b0e5",
26
+ "contentHash": "sha256:b76014070bb894c2ab421cc774ebc1401e20815908080d932fa36a92c4f16c63",
27
27
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
28
28
  "author": {
29
29
  "name": "PassionCode.ai",
@@ -62,9 +62,9 @@
62
62
  "name": "passioncode",
63
63
  "displayName": "PassionCode.ai",
64
64
  "repo": "passioncode-ai/passioncode",
65
- "ref": "v0.2.40",
65
+ "ref": "v0.2.42",
66
66
  "commit": null,
67
- "version": "0.2.40",
67
+ "version": "0.2.42",
68
68
  "via": "this repository",
69
69
  "skills": [
70
70
  "setting-up-passioncode",
@@ -72,7 +72,7 @@
72
72
  ],
73
73
  "legacyPluginIds": [],
74
74
  "legacyMarketplaces": [],
75
- "contentHash": "sha256:8528f506d1851ce653316227bfa9d25db9cffa54a8e0b4ebccb2eb272338eaf2",
75
+ "contentHash": "sha256:0265b4cf8965eace837779cc775f1a8c594463cf923e1acb58961190267ccc76",
76
76
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
77
77
  "author": {
78
78
  "name": "PassionCode.ai",
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
6
- "version": "0.51.1",
6
+ "version": "0.52.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose. All silent in a session Fabric launched. Every hook reads no token, prints at most one block, always exits 0, and hands any Fabric call to a detached worker with a 10 s deadline.",
2
+ "description": "Two silent-unless-relevant hook sets. The dashboard-kit notice: one line at session start or /clear in a repository that owns a Fabric service whose dashboard has no kit or an older one (fabric-dashboard/0.1, DEC-0036), phrased as an upgrade the agent may make itself. The Fabric session hooks (working-with-fabric, COM-04, FAA-12a): at session start or /clear, the preamble when Claude Code has the MCP server `fabric` and Fabric runs (fabric_whoami, then the project board through com.status and com.list, claim and release); when Fabric is down, one line, only in a project that uses Fabric (a Fabric contract file, a project-bound registration or a service descriptor naming the repository); nothing on compact or resume; each turn's facts (HEAD, files changed, lines added and removed, never content), the board's unread count and the session's end through Fabric, only when the running Fabric advertises session-hooks/1; at each prompt one line only when the unread count rose, then each nudge a turn's answer carried, once (watch-mail prints those of turns it delivered). All silent in a session Fabric launched. Every hook reads no token, prints at most one block, always exits 0, and hands any Fabric call to a detached worker with a 10 s deadline.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.51.1"
8
+ version: "0.52.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "e6c4fc16b415eec3190dd34d924d269405359dc9"
10
+ contract-commit: "1197a90dc4b57152cabcc71ad17db7559f35a27f"
11
11
  ---
12
12
 
13
13
  # Adapting projects to Fabric
@@ -76,7 +76,7 @@ Use exactly:
76
76
 
77
77
  - contract version `0.1.0`;
78
78
  - repository `https://github.com/passioncode-ai/fabric-agent-contract`;
79
- - commit `e6c4fc16b415eec3190dd34d924d269405359dc9`.
79
+ - commit `1197a90dc4b57152cabcc71ad17db7559f35a27f`.
80
80
 
81
81
  This is the default for new bundles. When checking an issued legacy bundle, preserve its
82
82
  explicit supported lock and use that exact clean checkout. Never silently upgrade or
@@ -30,7 +30,10 @@ report, and do not ask again; an agent that serves `surfaces.operatorChannel` al
30
30
  stay the person's (a `secret` step, contract DEC-0121 AP-4): never create the bot, or ask for or
31
31
  store the token's value. Enabling the channel, and removing a webhook found on its token, are the
32
32
  operator's decision, taken by them on the agent's page or relayed to its agent in their own words
33
- with `_meta.decision` (OC-1, OC-2, DEC-0121 AP-2); never enable it on your own account. The default
33
+ with `_meta.decision` (OC-1, OC-2, DEC-0121 AP-2), including a relay from another of their agents
34
+ that carries their words verbatim, when and where they said them and the scope (DEC-0126 OA-2,
35
+ OA-3; its `source` sent only to an agent whose well-known `protocols` list `operator-source/1`,
36
+ OA-7); never enable it on your own account. The default
34
37
  pin carries DEC-0034 and DEC-0035: a served status document is
35
38
  checked against the pinned `schemas/operator-channel-status.schema.json`; the channel's runtime
36
39
  rules have no probe rule here yet, so report them `NOT_RUN`. An agent with neither stops nor notify
@@ -22,10 +22,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
22
22
 
23
23
  CONTRACT_VERSION = "0.1.0"
24
24
  CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
25
- CONTRACT_COMMIT = "e6c4fc16b415eec3190dd34d924d269405359dc9"
25
+ CONTRACT_COMMIT = "1197a90dc4b57152cabcc71ad17db7559f35a27f"
26
26
  # #region supported-contract-revisions — docs: README.md#contract-pin
27
27
  # Only this declaration may name a legacy revision in live files.
28
- SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
28
+ SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "e6c4fc16b415eec3190dd34d924d269405359dc9", "a3a66eb0052043847f6556348e37d34ca873a629", "52c7d798b9f4eb784eb4e29e717038a8912c759d", "2f020a696453ae299b123517f14957f6a1354379", "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
29
29
  # #endregion supported-contract-revisions
30
30
  INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
31
31
  MCP_REVISION = "2026-07-28"
@@ -14,10 +14,10 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
14
14
  compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); 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.
15
15
  metadata:
16
16
  author: PassionCode.ai
17
- version: "0.51.1"
17
+ version: "0.52.0"
18
18
  contract-version: "0.1.0"
19
19
  extension: "fabric-service/0.1"
20
- extension-commit: "e6c4fc16b415eec3190dd34d924d269405359dc9"
20
+ extension-commit: "1197a90dc4b57152cabcc71ad17db7559f35a27f"
21
21
  ---
22
22
 
23
23
  # Building Fabric services
@@ -86,8 +86,8 @@ from `leaseExpiresAt`.
86
86
  why not; a refusal accepts the request and completes it `failed_known` with that sentence, so the requester learns it
87
87
  and the request does not bounce. A redacted body is refused the same way. The kit never hands a body to a model, a
88
88
  shell or a tool; what the handler does with an admitted body (and the checks it needs before a model, a tool or a
89
- spend sees it) is the service's own responsibility. The kit's responder reads no approval or mandate from a message in
90
- 0.51.0: a request from the operator's own Project may carry `mandate/1` (DEC-0125 MD-3), and a handler acts on it only
89
+ spend sees it) is the service's own responsibility. The kit's responder reads no approval or mandate from a message yet
90
+ (0.52.0): a request from the operator's own Project may carry `mandate/1` (DEC-0125 MD-3), and a handler acts on it only
91
91
  once the kit reads it (FAA-135).
92
92
 
93
93
  ## Effects, observation, evidence and the reply
@@ -101,6 +101,7 @@ once the kit reads it (FAA-135).
101
101
  - `evidence` is at most 8 references `{id: uri, contentHash: sha256:<hex>, label?}`; `artifact_ref(uri, content=…|path=…)`
102
102
  hashes the bytes itself. Two ids with one normal form (DEC-0076) name one artifact and are refused.
103
103
  - `reply: {text, format?, artifacts?}` is posted with `com.reply` before the completion. A reply is not a completion.
104
+ It is a fenced reply, so it carries no `deliverTo` and reaches the requester (DEC-0135 DR-3).
104
105
  - A handler that raises before any effect completes the request `failed_known` naming only the exception's class.
105
106
  - The handler checks `ctx.stopped` in long work: after a session loss its answer is not sent.
106
107
 
@@ -55,6 +55,18 @@ hub forwarding a T2 caller). DL-26: a T0 or T1 caller is never required to prese
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.
57
57
 
58
+ **The family and the agent network** (DEC-0130, wording settled by DEC-0137). The operator's family is every agent
59
+ placed on a machine of his estate and his own local sessions; a callee recognises it only by placing the caller in T0
60
+ or T1 (DL-27). It is neither a context (`personal` and `org:<orgId>` stay the only ones) nor a tier: it is open to
61
+ what DL-26 opens, everything open to the operator in the call's context, which for a tokenless call is the callee
62
+ instance's own context. No claim of a caller places it in the family: a call relayed onto loopback stays T2 unless the
63
+ relay proves the origin's tier, and a caller under another OS account stays T2 (DL-28). The agent network is every
64
+ agent a caller can reach, the family included; outside the family a caller is T2, with a token per call (DL-29). Money
65
+ limits, human gates, the contexts, the credential source (never another agent's key or vault slot), the machine
66
+ profiles on calls over the relay, and a relayed action's decision or mandate still bind inside the family (DEC-0137).
67
+ Every T0 process of the owner's account counts as the family, one he did not install included, so a product that must
68
+ hold against a hostile local process adds its own approval (FAA-148). The kit's tiers already do this; nothing changes in code.
69
+
58
70
  The token is read from `Fabric-Delegation` first (a forwarded call; present but empty is refused, never skipped),
59
71
  then from `Authorization: Bearer` when the bearer is a delegation JWT (DL-9, DL-10). A tool with no declared
60
72
  effect is refused (`unknown-state`), never guessed. The checks run in the DL-13 order; nothing about a token is
@@ -131,6 +143,16 @@ current verified revision as `estate=` / `{ estate }`, as a value or as a functi
131
143
  applied mark or its revocations is an unreadable source, `estate-unreadable`, in both kits (FAA-78).
132
144
  - Every verifier refuses a clock that is not a number of seconds `unknown-state` `no-clock` before row 1: a NaN clock
133
145
  would make every time row false and pass an expired token.
146
+ - **Which hub key a token is checked against** (DL-14a, DEC-0128). An organization's tokens are signed by that
147
+ organization context's own hub key (`machines[].contextHubKeys`), under the context issuer its pin names
148
+ (`organizations[].iss`, `urn:fabric:context:<opaque>`, never the estate id). An instance that serves an organization
149
+ passes its context as `estate_context=` / `{ estateContext }` to `delegation_gate` or `verify_delegation`, once the
150
+ record pins its organization (until then its tokens are refused `kid-not-certified`, brief D5), and that pin's `iss`
151
+ as `issuer`: the `kid` is then trusted only when one machine lists it for that context with that key and
152
+ the record pins the organization. Without it, or with `personal`, the personal `hubKey` is read, so a context key
153
+ never verifies a personal token and the personal key never an organization's; any other value (`Org:x`, a trailing
154
+ space, a number) certifies nothing. `verify_approval` takes no context (the contract's `VerifyApprovalOptions`
155
+ declares none at the pin), so it reads the personal keys. Held to `contextCases` and `contextVerifier` in `test/fixtures/contract/hub-key-vectors.json`.
134
156
 
135
157
  ## What the kit does not do
136
158
 
@@ -14,7 +14,10 @@ One revision of one person's estate lists:
14
14
 
15
15
  A revision names its parent's digest and is signed by a passkey assertion (WebAuthn Level 3) over a challenge
16
16
  derived from its own digest. Python: [`scripts/fabric_estate.py`](../scripts/fabric_estate.py). Node:
17
- [`scripts/fabric-estate.mjs`](../scripts/fabric-estate.mjs). Both answer the contract's own vectors.
17
+ [`scripts/fabric-estate.mjs`](../scripts/fabric-estate.mjs). Both answer the contract's own vectors. Since 0.52.0 the
18
+ record's rules live in [`fabric_estate_rules.py`](../scripts/fabric_estate_rules.py) and
19
+ [`fabric-estate-rules.mjs`](../scripts/fabric-estate-rules.mjs); the estate modules re-export them, and
20
+ `fabric_kit.py install` copies both.
18
21
 
19
22
  ## Contents
20
23
 
@@ -85,6 +88,21 @@ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., esta
85
88
  estate_revocations=lambda: delta_entries)
86
89
  ```
87
90
 
91
+ **Which key: the instance's context** (DL-14a, contract DEC-0128). The personal context's tokens are signed by a
92
+ machine's `hubKey`; an organization's by that context's own key in the same machine's `contextHubKeys`
93
+ (`{context: "org:<orgId>", key}`), under the context issuer its pin in `organizations` names (`iss`,
94
+ `urn:fabric:context:<opaque>`). An instance that serves an organization passes its context and that issuer:
95
+
96
+ ```python
97
+ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=pin["iss"], effects=..., estate=lambda: current,
98
+ estate_context="org:acme-rehearsal") # the context fabric_context started it for
99
+ ```
100
+
101
+ The `kid` is then certified only when exactly one machine lists it for that context with the published key and the
102
+ revision pins the organization; omitted or `personal`, the personal `hubKey` is read. A context key never verifies a
103
+ personal token, the personal key never an organization's, and a context that is neither (`Org:x`, a trailing space, a
104
+ number, `null`) certifies nothing: a misconfigured callee fails closed with `kid-not-certified`.
105
+
88
106
  - **The token's fault** (no such certification, another key under that `kid`, a `kid` on two machines, a revoked hub
89
107
  key or machine) is `unauthenticated` (for an approval, `approval-invalid`) with detail `kid-not-certified`.
90
108
  - **The trust source's fault** is `issuer-unavailable`: `estate-unreadable` (a source that throws or answers nothing
@@ -97,10 +115,12 @@ gate = fd.delegation_gate(audience=..., jwks=keys, issuer=..., effects=..., esta
97
115
  source in both kits, and so is `applied: null`; an omitted `applied` means no revision applied yet (FAA-78).
98
116
  - **What you pass in.** Pass only a revision that `verify_estate_record` accepted and whose number is not lower than
99
117
  the highest the machine applied; a higher one only when you verified its chain from the applied revision.
100
- - `hub_key_certified(record, kid, x, at, applied, extra_revocations)` /
101
- `hubKeyCertified(record, kid, x, at, applied, {extraRevocations})` answers the question on its own:
118
+ - `hub_key_certified(record, kid, x, at, applied, extra_revocations, context)` /
119
+ `hubKeyCertified(record, kid, x, at, applied, {extraRevocations, context})` answers the question on its own:
102
120
  `{certified: true, machine, revision}` or `{certified: false, fault, detail}`. Both kits are held to the contract's
103
- `fixtures/vectors/hub-key.json` (`test/fixtures/contract/hub-key-vectors.json`).
121
+ `fixtures/vectors/hub-key.json` (`test/fixtures/contract/hub-key-vectors.json`), and to its `contextCases` and
122
+ `contextVerifier`, generated with the contract's `hubKeyCertified` and `verifyDelegation` over its
123
+ `estate-record-organizations.json`.
104
124
 
105
125
  ## Contexts: one agent for you and for an organization
106
126
 
@@ -129,6 +149,24 @@ of `[A-Za-z0-9][A-Za-z0-9_-]{0,127}`. Absent is `personal`; `null` is not absent
129
149
  finds no rule broken. A service that applies the estate record updates its copy first:
130
150
  `fabric_kit.py install <dir> --module fabric_estate` (or `--lang node --module fabric-estate`). Since 0.40.0 the
131
151
  estate reader imports the kit's core modules, so the one file alone no longer loads ([kit modules](kit-modules.md)).
152
+ - **The organizations the person admitted** (DEC-0127, DEC-0128). `organizations[]` pins each organization once by
153
+ its context (`org`), with its server (`issuer`), its context issuer (`iss`) and the root that signs its key sets
154
+ (`root`). FAC-SEM-147: an organization pinned twice, one `iss` used by two pins, an `iss` whose opaque part contains
155
+ the estate id (eight characters or more, any case), and a root of small order are findings.
156
+ - **A hub key per organization context** (DEC-0128). `machines[].contextHubKeys[]` (`{context, key}`) certifies the
157
+ key that signs that context's tokens. FAC-SEM-073: each context once on a machine, a context the record pins, every
158
+ `kid` and public key unique across all the record's hub keys (personal and per context), no key of small order, and
159
+ only on the machine that holds the personal `hubKey`; FAC-SEM-134: only a `full` machine; FAC-SEM-076: revoking
160
+ `hub-key <kid>` while listing it; FAC-SEM-114: a context key dropped, replaced, moved to another context or left on a
161
+ removed machine is revoked in the same revision.
162
+ - **An organization instance whose context the record does not pin** (decision D5 of 0.52.0). `fabric_context` starts
163
+ it as before: it checks the placement and its share grant, not the pin. Every token presented to it in that context
164
+ is then refused `kid-not-certified`, since `hub_key_certified` reads a context's keys only while the record pins it.
165
+ The start is not refused because the contract lets Fabric write `organizations` only once every reader of the record
166
+ is pinned at or after DEC-0128, so today's records pin none; a refusal now would stop every organization instance.
167
+ - **A tokenless call runs in the instance's context** (DEC-0137). A T0 or T1 caller presents no token (DL-26), so the
168
+ call is in the context of the instance it reached: the personal instance for the person, an organization's instance
169
+ for that organization ([delegation](delegation.md), the family).
132
170
 
133
171
  Every rule is total on hostile JSON, as the contract's are: an `instancePath` names an item by its index as written (a
134
172
  non-object before it does not shift it), and a value whose own `toString` member is data is shown as its JSON. Both kits
@@ -289,8 +327,8 @@ sharer's node's and the organization server's, not this kit's.
289
327
  ## Checking a record you produce
290
328
 
291
329
  `estate_record_rules(kind, value)` / `estateRecordRules(kind, value)` are the contract's rules, finding for finding:
292
- - `estate-record` (FAC-SEM-072…076, 110, 111, 122);
293
- - `estate-record-revision` `{parent, next}` (072);
330
+ - `estate-record` (FAC-SEM-072…076, 092, 110, 111, 122, 134, 147);
331
+ - `estate-record-revision` `{parent, next}` (072, 114, 137);
294
332
  - `estate-placement` `{record, machine, package, context?}` (074, 075, 111, 122): the package placed on that machine in that
295
333
  context, `personal` when `context` is absent.
296
334
 
@@ -237,8 +237,8 @@ Contract DEC-0125 (`mandate/1`, rule `FAC-SEM-144`, `docs/specification/delegati
237
237
  MD-6): the operator's instruction for one task travels with the task, narrowed at each hand-over, so a service the
238
238
  task reaches takes a call the mandate covers as his decision without asking him again. It is the machine form of his
239
239
  rule of 2026-10-26: one approval he gives in the chat of any of his agents binds all his agents (working-with-fabric,
240
- Step 5). Both kits carry the reference in `fabric_mandate` / `fabric-mandate.mjs` and answer the contract's
241
- `mandate.json` vectors case for case.
240
+ Step 5), which the contract states as DEC-0126 (OA-1 to OA-7). Both kits carry the reference in `fabric_mandate` /
241
+ `fabric-mandate.mjs` and answer the contract's `mandate.json` vectors case for case, its `handOn` group included.
242
242
 
243
243
  **As a service.** Name your own agent id and each relayed action's effect class; the gate then reads the call's
244
244
  `_meta.mandate` when it carries no decision:
@@ -272,20 +272,58 @@ server.add_tool(approve_tool, approve_reply, action=reply_action, target="reply_
272
272
  (and `estimateUsd`, `perCallMaxUsd` for spend); an invalid one is `decision-invalid` with `mandate-malformed`,
273
273
  `mandate-widened`, `mandate-expired`, `mandate-early` or `mandate-not-for-this-call`. A `change-policy` effect is
274
274
  never covered (AP-5); human steps never travel (MD-6).
275
+ - **Where the operator spoke (DEC-0126).** A mandate or a decision may carry `source` (`{project, session or
276
+ thread}`), set by the agent that heard the operator's words relayed from another of his agents (OA-2). The kits read
277
+ it exactly as `mandate.schema.json#/$defs/source` does (an unplaced one is `mandate-malformed` or `decision-invalid`)
278
+ and return it in the record: `ctx.record`, `input_records` and `job_step`'s record carry `source` (OA-6). Keep it,
279
+ as `instruction`, in the service's own audit record, never in the events feed, a usage report, a log line or a page
280
+ another party reads (OA-7).
281
+ - **Listing `operator-source/1`.** A service that reads `source` lists `operator-source/1` in its well-known
282
+ `protocols` (OA-7): `McpToolServer.well_known_protocols(protocols)` returns your list with it added when the server
283
+ has `agent_id` or serves a relayed action, for `build_well_known(protocols=server.well_known_protocols([...]))`. A
284
+ service that builds its own list (a Node server, or one without `McpToolServer`) adds `operator-source/1` itself
285
+ when it reads `source`; a caller that sees no such entry sends the call without `source`, and it is taken as before.
275
286
  - `action_gate(..., self_id=, effect=, caller_agent=, spend_usd=, project=)` / `actionGate({..., self, effect,
276
287
  callerAgent, spendUsd, project})` is the same gate for a server of your own; `action_decision` / `actionDecision`
277
288
  take `mandate`, `self`, `callerAgent`, `effect`, `spendUsd` and `project` as the contract's function does
278
- (`effect-missing`, `self-missing` are the service's own faults). A job's input request (`input_actions=True`) does
279
- not read a mandate yet.
289
+ (`effect-missing`, `self-missing` are the service's own faults), and the kit's own `mandateCapability` (a job's kept
290
+ step: the capability the mandate is read under), which the contract's function does not take; like `effect`, it is
291
+ the service's own input, never the caller's.
292
+ - **A job's approval step (interop C3.3).** A job reads the mandate its start call carried at its own approval step:
293
+ `ctx.approval_step(job_id, key, request, effect=, spend_usd=, project=, message=)` on the start call's context (a
294
+ server with `agent_id`) answers the record when the mandate covers the step `{capability: the job's, action: key,
295
+ effect}` and asks nothing, the job going on as the operator's decision; otherwise the job waits `input_required` on
296
+ `request`, its message naming the part not covered (`outside-mandate: <part>`) or saying the mandate expired, and the
297
+ step is kept in the job's private part (`request_input(..., steps=)`). An answer's `_meta.mandate` that covers that
298
+ step is then read when `input_actions=True`, beside a decision for the key. The rule alone is `job_step(mandate, ...)`
299
+ / `jobStep({mandate, tier, now, self, capability, key, effect, callerAgent, spendUsd, project})`; Node's
300
+ `requestInput(jobId, inputRequests, statusMessage, {steps})` keeps the same private part. Each `request_input`
301
+ replaces the kept steps, and an answer clears them: pass `steps=` again on every question whose answer a mandate may
302
+ cover. The answer's mandate is read under the job's own capability, as at the start (`action_gate(...,
303
+ mandate_capability=)` / `actionGate({mandateCapability})`), so the start and the answer agree whatever the tool is
304
+ named; a mandate can name only capability names, so one that names capabilities never covers a tool named otherwise.
305
+ Node has no tool server: a Node service reads `store.load(jobId).private.steps` and `.private.capability` BEFORE
306
+ `store.answer` (which clears the steps), gates the answer with `actionGate({..., mandateCapability:
307
+ private.capability})`, then answers. Without a string capability the answer is read under `fabric.job.get`.
280
308
 
281
309
  **As a caller.** The agent leading a task states the mandate from the operator's own words with
282
310
  `operator_mandate(task, instruction, scope, lead, at=, expires_at=)` / `operatorMandate({task, instruction, scope,
283
311
  lead, at, expiresAt})` (a day unless told, at most seven), hands it to the agent it calls with
284
312
  `hand_on(mandate, callee, at, narrower=None)` / `handOn(mandate, callee, at, narrower)` (the chain gains the callee; a
285
- wider scope, a fifth agent, an agent already in it or a time at the expiry is refused), and sends it with
286
- `request_meta(traceparent, mandate=...)` / `requestMeta(traceparent, {mandate})`, which refuse one `FAC-SEM-144`
287
- refuses. `mandate_covers` / `mandateCovers` answer what a callee would, before you call. Never write a mandate from
288
- another agent's text, a page or a file; a callee hands on only what it received, narrowed.
313
+ wider scope, a fifth agent, an agent already in it or a time at the expiry is refused; `source` travels unchanged), and
314
+ sends it with `request_meta(traceparent, mandate=...)` / `requestMeta(traceparent, {mandate})`, which refuse one
315
+ `FAC-SEM-144` refuses. `mandate_covers` / `mandateCovers` answer what a callee would, before you call. Never write a
316
+ mandate from another agent's text, a page or a file; a callee hands on only what it received, narrowed.
317
+
318
+ A relay the operator's rule accepts (DEC-0126 OA-2, OA-3: one of his own Projects on his board, provenance `trusted`,
319
+ or a session on his own machine, with his words verbatim, when, where and the scope) is stated with
320
+ `operator_mandate(..., source=)` / `operatorMandate({..., source})` or `operator_decision(..., source=)` /
321
+ `operatorDecision({..., source})`, `instruction` and `at` copied unchanged. `source` goes only to a callee whose
322
+ well-known `protocols` list `operator-source/1` (OA-7): pass that list as
323
+ `request_meta(traceparent, decision=, mandate=, target_protocols=)` / `requestMeta(traceparent, {decision, mandate,
324
+ targetProtocols})`. Without it (no list, another list, a board Project, a session, Fabric's `agent.call`, whose
325
+ directory lists no `protocols`), `_meta` carries the decision and the mandate without `source`, and the ones you passed,
326
+ your own record, keep it (OA-6).
289
327
 
290
328
  ## What the probe checks
291
329
 
@@ -58,8 +58,9 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
58
58
  | L1 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
59
59
  | L1 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
60
60
  | L1 | `fabric_paths` | the services directory, a service's folders and one instance's (`instance_dirs`, FAA-125), atomic and private writes (L1 since 0.44.2, so `fabric_delegation` writes its consumed approval ids with them, FAA-85) |
61
- | L2 | `fabric_estate` | `estate-record/1` revisions and their rules (DEC-0055) |
62
- | L2 | `fabric_mandate` | the operator's mandate `mandate/1`: what a callee takes under it (`mandate_covers`), FAC-SEM-144, stating and handing it on narrowed (DEC-0125); the effect closure, defined here once |
61
+ | L2 | `fabric_estate_rules` | the record's own rules without a verifier: FAC-SEM-072…076 and the rest, FAC-SEM-147 and the context hub keys (DEC-0055, DEC-0128), contexts, the digest and challenge, a passkey's key; split from `fabric_estate` in 0.52.0 (D8) |
62
+ | L2 | `fabric_estate` | `estate-record/1` revisions: the passkey assertion, the verifier, placement trust, `hub_key_certified` with its context (DL-14a, DEC-0128); re-exports `fabric_estate_rules` (DEC-0055) |
63
+ | L2 | `fabric_mandate` | the operator's mandate `mandate/1`: what a callee takes under it (`mandate_covers`), FAC-SEM-144, stating and handing it on narrowed (DEC-0125); where the operator spoke, `source`, and its withholding from a target that does not list `operator-source/1` (DEC-0126); the effect closure, defined here once |
63
64
  | L2 | `fabric_delegation` | the callee's side of `fabric-delegation/0.1` (DEC-0039) |
64
65
  | L2 | `fabric_share` | share grants and registrations (DEC-0086); whether an organization's instance may run under its grant (`instance_share_grant`, FAA-125) |
65
66
  | L2 | `fabric_ceremonies` | the headless join token (DEC-0084) |
@@ -81,7 +82,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
81
82
  | L3 | `fabric_supervisors` | launchd, systemd user units, Task Scheduler (DEC-0032, LC-14) |
82
83
  | L3 | `fabric_service` | the facade: every name 0.39.1 exported, re-exported from the modules above |
83
84
  | L3 | `fabric_profiles` | runner and model profiles ([profiles](profiles.md)) |
84
- | L3 | `fabric_interop_rules` | `fabric-interop/0.1` rules without a service: constants, `InteropError`, trace context, job limits (DEC-0087; FAA-92) |
85
+ | L3 | `fabric_interop_rules` | `fabric-interop/0.1` rules without a service: constants, `InteropError`, trace context, job limits (DEC-0087; FAA-92), a job's approval step under the mandate (`job_step`, C3.3) |
85
86
  | L3 | `fabric_interop` | `fabric-interop/0.1`: jobs, envelopes, MCP tools; re-exports the rule layer ([interop](interop.md)) |
86
87
  | L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
87
88
  | L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
@@ -103,7 +104,8 @@ public name 0.39.1 had, in both languages (the snapshot is `test/fixtures/kit-ap
103
104
  name fails `test_kit_api`), and the Python facade the private names other code reached through it (`_number`,
104
105
  `_DAY_MS`, `_utc_date_ms` from the core). `fabric_board` and `fabric-board.mjs` are
105
106
  facades the same way, and since 0.44.1 so are `fabric_interop` (over `fabric_interop_rules`) and `fabric_package` (over
106
- `fabric_package_schema`), with their Node twins: patch the rule or schema module, not the facade. What changed for a service that copies the kit: `fabric_service.py` alone is no longer enough;
107
+ `fabric_package_schema`), and since 0.52.0 `fabric_estate` (over `fabric_estate_rules`, listed just before it, since it
108
+ imports the rules), with their Node twins: patch the rule or schema module, not the facade. What changed for a service that copies the kit: `fabric_service.py` alone is no longer enough;
107
109
  install with `fabric_kit.py install` and `status` names what is missing. A test or service that replaced a function
108
110
  by assigning it on the facade (`fabric_service.X = ...`) no longer changes what the kit does: the code reads its own
109
111
  module's name, so patch the module that defines it (`fabric_lock.X`, `fabric_session_env.X`). A service that imported a module-private
@@ -1,6 +1,6 @@
1
1
  # fabric-service/0.1: wire reference
2
2
 
3
- Pinned to `fabric-agent-contract` commit `e6c4fc16b415eec3190dd34d924d269405359dc9`
3
+ Pinned to `fabric-agent-contract` commit `1197a90dc4b57152cabcc71ad17db7559f35a27f`
4
4
  (`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
5
5
  DEC-0032 and DEC-0033; the operator channel DEC-0034 and DEC-0035; the embedded dashboard DEC-0036;
6
6
  delegation and the operator session's lifetime DEC-0039; the meter's call window DEC-0041; session handoff DEC-0042;
@@ -19,7 +19,12 @@ DEC-0113, a node summary against the certificate DEC-0114, key windows read stri
19
19
  gates each credential member DEC-0116 to DEC-0118, a pin of four members and `profile` DEC-0120; every action a person
20
20
  has as an agent tool, `fabric-actions/0.1` and the operator's relayed decision DEC-0121; `surfaces.a2a` and the public A2A
21
21
  Agent Card DEC-0122, a service's pages as Fabric Dashboards' `open_link` DEC-0123, Fabric's button in the page's language
22
- DEC-0124; the operator's mandate `mandate/1`, handed on narrowed with a task DEC-0125). The contract's schemas are normative;
22
+ DEC-0124; the operator's mandate `mandate/1`, handed on narrowed with a task DEC-0125; one approval binds all the
23
+ operator's agents, its `source` on a mandate and a decision DEC-0126; context hub keys and the organizations an estate
24
+ record pins DEC-0127 and DEC-0128; the family and the agent network named DEC-0130 and DEC-0137; a reply delivered to
25
+ the sender it answers, or to whom it names DEC-0135; a hand-over adds no naming list the scope in force leaves out
26
+ DEC-0136. The organization's member list, draft exchange, relay and the machines' report, DEC-0129, DEC-0131 to DEC-0134,
27
+ reach the kit only as protocol register entries). The contract's schemas are normative;
23
28
  this page is the working summary. Extension key:
24
29
  `https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
25
30
 
@@ -9,19 +9,22 @@
9
9
  // - actionsExtensionRules(wellKnown), actionRules(kind, value): FAC-SEM-142.
10
10
  // - actionsExtension(actions, { capabilities }): the `extensions` entry a service declares; it refuses a declaration
11
11
  // FAC-SEM-142 would find, so a service never publishes one.
12
- // - operatorDecision({action, instruction, at, target}): the decision an agent relays in _meta.decision (AP-2, AP-3),
13
- // refused when decision.schema.json would refuse it.
12
+ // - operatorDecision({action, instruction, at, target, source}): the decision an agent relays in _meta.decision (AP-2,
13
+ // AP-3), refused when decision.schema.json would refuse it.
14
14
  // - actionCaller(tier, authenticated): the caller a service records (AP-2): the identity it authenticated, else
15
15
  // local:T0 / local:T1 for a caller its local floor authenticated, never a name the call carries.
16
16
  // - DEC-0125: actionDecision reads a call's mandate (_meta.mandate) through fabric-mandate.mjs's mandateCovers when the
17
17
  // call carries no decision.
18
+ // - DEC-0126: a decision may carry `source` (where the operator said the words, OA-2); actionDecision returns it in
19
+ // recordAs (OA-6). A service that reads it lists operator-source/1 and keeps it, as `instruction`, in its own audit
20
+ // record only: never an event, a usage report, a log line or a page another party reads (OA-7).
18
21
  //
19
22
  // The kit has no JSON Schema validator: the two shapes are written here from the schemas, lengths in code points and
20
23
  // patterns over the whole string, as the contract's validator reads them. Members are an object's own members (a JSON
21
24
  // text has no other kind; an inherited member is not read). Never throws on what it reads. Node 20+, no dependency.
22
25
  import { ServiceError } from './fabric-base.mjs';
23
26
  import { isObject, jsonEqual } from './fabric-jsvalue.mjs';
24
- import { mandateCovers } from './fabric-mandate.mjs'; // DEC-0125
27
+ import { mandateCovers, sourceShape } from './fabric-mandate.mjs'; // DEC-0125, DEC-0126
25
28
  import { formatRfc3339, parseRfc3339Ms } from './fabric-time.mjs';
26
29
 
27
30
  export const ACTIONS_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/actions/0.1';
@@ -43,7 +46,7 @@ const CAPABILITY = /^[a-z][a-z0-9._-]{1,127}$/u; // common.schema.json#/$defs/ca
43
46
  const SHA256 = /^sha256:[0-9a-f]{64}$/;
44
47
  const STEP_URL = /^(https:\/\/|\/)/;
45
48
  const ACTION_MEMBERS = new Set(['id', 'title', 'tool', 'gate', 'humanStep']);
46
- const DECISION_MEMBERS = new Set(['by', 'via', 'action', 'target', 'instruction', 'at']);
49
+ const DECISION_MEMBERS = new Set(['by', 'via', 'action', 'target', 'instruction', 'at', 'source']);
47
50
  const KEY_PATH = `/extensions/${ACTIONS_EXTENSION_KEY.replace(/~/g, '~0').replace(/\//g, '~1')}`;
48
51
 
49
52
  /** An own member that is not undefined, as the validator reads a member as present. */
@@ -91,15 +94,17 @@ export const readableAction = (action) => actionErrors(action, '').length === 0
91
94
 
92
95
  /**
93
96
  * decision.schema.json (DEC-0121 AP-2): by `operator`, via `relayed-instruction`, the action's id, an optional target
94
- * of 1..200, the operator's words of 1..500, and `at` read strictly (the contract's strictTime; ajv-formats' date-time
95
- * also admits a space for T and a leap second, which this refuses, decision D5).
97
+ * of 1..200, the operator's words of 1..500, `at` read strictly (the contract's strictTime; ajv-formats' date-time also
98
+ * admits a space for T and a leap second, which this refuses, decision D5), and an optional `source` as
99
+ * mandate.schema.json#/$defs/source reads it (DEC-0126).
96
100
  */
97
101
  export function decisionShape(value) {
98
102
  if (!isObject(value) || Object.keys(value).some((key) => !DECISION_MEMBERS.has(key))) return false;
99
103
  if (['by', 'via', 'action', 'instruction', 'at'].some((key) => !has(value, key))) return false;
100
104
  return value.by === 'operator' && value.via === 'relayed-instruction' && matches(REQUEST_KEY, value.action, 64)
101
105
  && (!has(value, 'target') || text(value.target, 1, 200)) && text(value.instruction, 1, 500)
102
- && typeof value.at === 'string' && !Number.isNaN(parseRfc3339Ms(value.at));
106
+ && typeof value.at === 'string' && !Number.isNaN(parseRfc3339Ms(value.at))
107
+ && (!has(value, 'source') || sourceShape(value.source));
103
108
  }
104
109
 
105
110
  const acceptedApproval = (approval) => isObject(approval) && member(approval, 'ok') === true && nonEmpty(member(approval, 'approver'));
@@ -116,12 +121,16 @@ const refuse = (code) => ({ ok: false, code });
116
121
  * accepted approval is the approver's; a T2 caller without one gets approval-required with the digest; an accepted
117
122
  * elicitation is the operator's; no decision is operator-decision-required; a decision that is not the schema's, is for
118
123
  * another action or object, or whose `at` is more than 60 s ahead of `now` or more than 24 h behind it is
119
- * decision-invalid; otherwise the operator's act, recorded with `caller`. DEC-0125: a call with no decision that carries
124
+ * decision-invalid; otherwise the operator's act, recorded with `caller` and, when the decision carries one, its `source`
125
+ * (DEC-0126 OA-6). DEC-0125: a call with no decision that carries
120
126
  * `mandate` (its `_meta.mandate`) is answered as mandateCovers answers, given `self` (the service's own agent id),
121
127
  * `callerAgent` (only an agent id the service authenticated), `effect` (the action's effect class, the service's own
122
- * knowledge), `spendUsd` and `project`: covered, {by: 'operator', via: 'mandate', task, chain, caller}. `{fault}`
128
+ * knowledge), `spendUsd` and `project`: covered, {by: 'operator', via: 'mandate', task, chain, source?, caller}. `{fault}`
123
129
  * (action-invalid, caller-missing, now-unreadable, digest-missing, url-missing, effect-missing, self-missing) is the
124
130
  * service's own and is never sent to a caller.
131
+ *
132
+ * Kit input beside the contract's: mandateCapability, a job's kept step's capability, read by the mandate in place of
133
+ * the action's tool (the contract's actionDecision does not take it; the service's own input, never a caller's).
125
134
  */
126
135
  export function actionDecision(inputs) {
127
136
  const field = (key) => member(inputs, key);
@@ -153,7 +162,8 @@ export function actionDecision(inputs) {
153
162
  if (decision.action !== action.id || (has(decision, 'target') && decision.target !== field('target'))) return refuse(DECISION_INVALID);
154
163
  const at = parseRfc3339Ms(decision.at);
155
164
  if (at > clock + CLOCK_SKEW_SECONDS * 1000 || at < clock - DECISION_LIFETIME_SECONDS * 1000) return refuse(DECISION_INVALID);
156
- return { ok: true, recordAs: { by: 'operator', via: 'relayed-instruction', caller } };
165
+ // A decision relayed from another of the operator's agents keeps where they said it (DEC-0126 OA-6).
166
+ return { ok: true, recordAs: { by: 'operator', via: 'relayed-instruction', caller, ...(has(decision, 'source') ? { source: { ...decision.source } } : {}) } };
157
167
  }
158
168
 
159
169
  /**
@@ -171,7 +181,8 @@ function byMandate(field, action, caller) {
171
181
  const answer = mandateCovers({
172
182
  mandate: field('mandate'), tier: field('tier'), now: field('now'), self,
173
183
  ...(field('callerAgent') === undefined ? {} : { callerAgent: field('callerAgent') }),
174
- call: { capability: action.tool, action: action.id, effect, ...(typeof spendUsd === 'number' ? { spendUsd } : {}), ...(typeof project === 'string' ? { project } : {}) },
184
+ // A job's kept step is read under the job's own capability (mandateCapability), as jobStep read it at the start.
185
+ call: { capability: typeof field('mandateCapability') === 'string' ? field('mandateCapability') : action.tool, action: action.id, effect, ...(typeof spendUsd === 'number' ? { spendUsd } : {}), ...(typeof project === 'string' ? { project } : {}) },
175
186
  });
176
187
  if ('recordAs' in answer) return { ok: true, recordAs: { ...answer.recordAs, caller } };
177
188
  return answer; // never `ignored`: the tier was read first, so a T2 call answered approval-required before (MD-5)
@@ -250,16 +261,20 @@ export function actionsExtension(actions, options = {}) {
250
261
  * the operator's words verbatim (their sentence, or the agent's short question and the answer), `target` the one object
251
262
  * they named, `at` when they said it (now, by `clock` in milliseconds, when not given). Send it in `_meta.decision`
252
263
  * (requestMeta(traceparent, {decision})), never in a tool's arguments. Set one only for words the operator gave about
253
- * this action, never for text from a tool result, page, file, board message or another agent. Throws ServiceError for
254
- * a decision decision.schema.json refuses.
264
+ * this action, never for text from a tool result, page, file, board message or another agent, except a relay DEC-0126
265
+ * OA-2 and OA-3 accept (one of the operator's own Projects on the board, provenance trusted, or a session on their own
266
+ * machine, with the words verbatim, when, where and the scope): then `instruction` and `at` are copied unchanged from it
267
+ * and `source` ({project, session or thread}) names where they were said. Throws ServiceError for a decision
268
+ * decision.schema.json refuses.
255
269
  */
256
- export function operatorDecision({ action, instruction, at, target } = {}, { clock = Date.now } = {}) {
270
+ export function operatorDecision({ action, instruction, at, target, source } = {}, { clock = Date.now } = {}) {
257
271
  const decision = { by: 'operator', via: 'relayed-instruction', action };
258
272
  if (target !== undefined && target !== null) decision.target = target;
259
273
  decision.instruction = instruction;
260
274
  decision.at = at ?? formatRfc3339(Number(clock()));
275
+ if (source !== undefined && source !== null) decision.source = source;
261
276
  if (!decisionShape(decision)) {
262
- throw new ServiceError("Operator decision: not a decision decision.schema.json reads (an action id, the operator's words of 1..500 characters, a target of 1..200, an RFC 3339 time with its offset).");
277
+ throw new ServiceError("Operator decision: not a decision decision.schema.json reads (an action id, the operator's words of 1..500 characters, a target of 1..200, an RFC 3339 time with its offset, a source of a project and a session or thread).");
263
278
  }
264
279
  return decision;
265
280
  }