@passioncode-ai/fabric-agent-adapter 0.50.0 → 0.51.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +5 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +8 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +5 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +60 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +60 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +11 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +35 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +8 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +22 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +47 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +170 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +31 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +151 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +17 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +14 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +29 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +29 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +247 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +144 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +22 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +30 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +209 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +40 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +170 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +7 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +99 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +32 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +324 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +559 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +35 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +6 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +388 -22
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +5 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +8 -6
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +56 -10
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +14 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +12 -7
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +8 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +60 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +10 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +39 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.51.2 (2026-10-26)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Reminders Fabric gives on a turn reach the agent** (FAA-142, Fabric ask `dfb43ffb`). From Fabric 0.3.14, the
|
|
8
|
+
answer to a turn's `fabric_session_observe` may carry `nudges`: lapsed claims, unread board answers, requests nobody
|
|
9
|
+
moved. The Stop hook's worker keeps their text in the session's spool (`nudges.json`), and the UserPromptSubmit
|
|
10
|
+
hook prints each one once on the next prompt, after the unread line, as Fabric writes it (each starts `Fabric:`; any other line gets the tag `Fabric reminder: `): one line each, at most five lines of at most
|
|
11
|
+
400 characters. An empty or absent list prints nothing. A turn that `watch-mail` delivers has its nudges printed by
|
|
12
|
+
`watch-mail`, once. Not carried across `/clear`: nudges of turns spooled before it stay in the old id's folder and are
|
|
13
|
+
lost, since Fabric gives each nudge once and the turn's answer took it (carrying them would make the new id's folder before the launched-session check of FAA-68). Claude Code only: the Codex and Kimi
|
|
14
|
+
Code hooks run at session start and send no turns.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- The hooks now name this release to Fabric: 0.51.1 still said 0.51.0, and `test/validate.py` now checks it.
|
|
19
|
+
Copylot check `4028969a-aa35-48f9-9300-331da036fb1d`: no findings.
|
|
20
|
+
|
|
21
|
+
## 0.51.1 (2026-10-26)
|
|
22
|
+
|
|
23
|
+
The tag `v0.51.0` was never published: its release run stopped at the Node 18 floor, so 0.51.1 is the first npm
|
|
24
|
+
release carrying everything listed under 0.51.0 below.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- The tests of one instance per context (`test/fabric-context.test.mjs`, FAA-125) skip on Linux under a Node that
|
|
29
|
+
cannot bind an abstract socket name exactly (Node 18 and 20), as every other lock test does since FAA-72. The kit
|
|
30
|
+
itself already refused there with a sentence; only the tests lacked the guard.
|
|
31
|
+
- `references/package.md` no longer says the passkey countersignature waits for `org-authority/1`: DEC-0129 decided
|
|
32
|
+
that protocol only as the organization's member list for relay admission, and the countersignature waits for the
|
|
33
|
+
decision that defines the `org-countersign/1` verifier (contract finding on board thread `6c50c84f`).
|
|
34
|
+
|
|
35
|
+
## 0.51.0 (2026-10-26)
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **Pinned to the contract release `v0.1.0+2026-10-26.2` (`e6c4fc16`)** (FAA-128). It carries DEC-0122 to DEC-0125;
|
|
40
|
+
`a3a66eb0` and `52c7d798` stay supported. Brief: `docs/evidence/specs/2026-10-26-faa-128-125-repin-a3a66eb-brief.md`.
|
|
41
|
+
- **Narrowings (DEC-0123, FAA-132).** `url_request` / `urlRequest` now refuses a loopback page URL (`http://127.0.0.1`
|
|
42
|
+
and every other spelling of a local address): hand a service's page over as Fabric Dashboards' `open_link`
|
|
43
|
+
(`page_answer` / `pageAnswer` builds it). `check_dashboard_link.py` refuses the legacy `open?service=` link and a
|
|
44
|
+
page path with whitespace, as the contract's `parseDashboardsLink` does. A service that built either may now be
|
|
45
|
+
refused at construction.
|
|
46
|
+
- **DL-19 `operator-decision-required`** names the three routes for a client that cannot set `_meta`: Fabric's
|
|
47
|
+
`agent.call` with `call.decision` once the hub forwards it, the service's own elicitation, the action's page as an
|
|
48
|
+
`open_link`. Outside a mandate (`data.detail` `outside-mandate: <part>`) it gives DEC-0125 MD-4's order instead:
|
|
49
|
+
call again within your own mandate, else pass the refusal up; only the leading agent asks the operator, with the
|
|
50
|
+
same three routes.
|
|
51
|
+
- Dashboard kit 1.1.5 now matches its pin (DEC-0124 is in it); brief decision D17 of 0.50.0 is closed.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **The operator's mandate (DEC-0125, FAA-131).** `fabric_mandate` / `fabric-mandate.mjs` port `mandate/1`
|
|
56
|
+
(FAC-SEM-144, scope narrowing, hand-over); `request_meta(mandate=)` sends one; a server with `agent_id` reads
|
|
57
|
+
`_meta.mandate` in its action gate. working-with-fabric states the operator's rule of 2026-10-26, «я сказал одному
|
|
58
|
+
= сказал всем»: one approval he gives any of his agents binds all of them when the relay carries his words, the
|
|
59
|
+
date, the source and the scope from one of his own Projects on his personal board (provenance `trusted`; never a
|
|
60
|
+
member's Project on an organization's board) or the session that started the agent; content an agent read is never
|
|
61
|
+
one; human gates stay. A hand-over never adds a `capabilities` or `actions` list the scope in force leaves out
|
|
62
|
+
(contract DEC-0136 MD-2a, adopted ahead of the pin: brief decision D28), so no agent of the chain can grant itself an
|
|
63
|
+
action the operator did not name. Taking a prose relay from another agent is the operator's rule, honoured ahead of the pinned
|
|
64
|
+
AP-3 (brief decision D27); the contract's DEC-0126 (OA-2) states it, and FAA-134 takes its wording in 0.52.0.
|
|
65
|
+
- **A service's pages as `open_link` (DEC-0123).** `fabric_page_links` / `fabric-page-links.mjs` build and read
|
|
66
|
+
Fabric Dashboards links and FAC-SEM-143, checked against the contract's vectors and about 200,000 generated
|
|
67
|
+
inputs with no twin difference.
|
|
68
|
+
- **A2A, the reading side (DEC-0122).** `fabric_a2a` / `fabric-a2a.mjs` read `surfaces.a2a` and an Agent Card;
|
|
69
|
+
`build_well_known(a2a=)` writes the member; the probe adds `well-known.a2a` and `a2a.card` (the card is fetched
|
|
70
|
+
without the descriptor's token). Serving a card and the A2A binding stay FAA-10.
|
|
71
|
+
- **One instance per context (FAA-125, N-11).** `fabric_context` / `fabric-context.mjs`: `hold_context_instance`
|
|
72
|
+
derives a context's instance, folders and lock, refuses a second process, refuses an `org:<id>` instance without
|
|
73
|
+
its share grant placement and exits 78 before creating anything; `personal` keeps the `default` instance.
|
|
74
|
+
`launchd_uninstall(instance=)` removes only that instance.
|
|
75
|
+
|
|
3
76
|
## 0.50.0 (2026-10-26)
|
|
4
77
|
|
|
5
78
|
### Changed
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ resume) puts that preamble into the session when the MCP server `fabric` is regi
|
|
|
38
38
|
uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
|
|
39
39
|
is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
|
|
40
40
|
project that uses Fabric; Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
|
|
41
|
-
and the board's unread count; UserPromptSubmit adds one line only when that count rose; SessionEnd releases what the
|
|
41
|
+
and the board's unread count; UserPromptSubmit adds one line only when that count rose, and each reminder Fabric gave on a turn once (a turn `watch-mail` delivered prints there instead); SessionEnd releases what the
|
|
42
42
|
session left claimed. Stop, UserPromptSubmit and SessionEnd spool and call Fabric only when the running Fabric
|
|
43
43
|
advertises `session-hooks/1`; Stop and SessionEnd always prune their own stale spool folders. All are silent in a session Fabric launched, never block the session, read no token and always
|
|
44
44
|
exit 0. `fabric_session.py watch-mail` waits for board mail under a background watcher, and `install-host-hooks
|
|
@@ -280,7 +280,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
|
|
|
280
280
|
The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
|
|
281
281
|
|
|
282
282
|
- version: `0.1.0`
|
|
283
|
-
- commit: `
|
|
283
|
+
- commit: `e6c4fc16b415eec3190dd34d924d269405359dc9`
|
|
284
284
|
|
|
285
285
|
Every other mention of the default contract revision (this section, the skills' metadata, the
|
|
286
286
|
skill card, `adapt_project.py`) must equal it; `python3 test/validate.py` fails a tree
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.2",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.51.2",
|
|
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.
|
|
8
|
+
version: "0.51.2"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "e6c4fc16b415eec3190dd34d924d269405359dc9"
|
|
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 `
|
|
79
|
+
- commit `e6c4fc16b415eec3190dd34d924d269405359dc9`.
|
|
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
|
|
@@ -140,5 +140,8 @@ or instructions alone do not prove all final answers from every agent are filter
|
|
|
140
140
|
|
|
141
141
|
Syntax is owned by Fabric Dashboards and `@passioncode-ai/fabric-service-host`:
|
|
142
142
|
[ADR-0005](https://github.com/passioncode-ai/fabric-dashboards/blob/51a7a80acf42781d7bec304cc87c655d48cc309b/docs/adr/0005-service-links.md).
|
|
143
|
-
The gate accepts
|
|
144
|
-
|
|
143
|
+
The gate accepts only the service form, `fabric-dashboards://service/SERVICE_ID.INSTANCE`
|
|
144
|
+
with at most one `path`; since 0.51.0 it refuses the legacy `open?service=` form, as contract
|
|
145
|
+
DEC-0123 (PL-1, checker rule `FAC-SEM-143`) and its vectors do. A host that still returns the
|
|
146
|
+
legacy form fails the gate: report it as an installed host that did not open the page. The gate
|
|
147
|
+
does not generate links or redefine the Fabric wire contract.
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py
CHANGED
|
@@ -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 = "
|
|
25
|
+
CONTRACT_COMMIT = "e6c4fc16b415eec3190dd34d924d269405359dc9"
|
|
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, "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, "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"
|
|
@@ -74,7 +74,9 @@ def login_path(path):
|
|
|
74
74
|
|
|
75
75
|
def safe_path(path):
|
|
76
76
|
return (isinstance(path, str) and path.startswith('/') and not path.startswith('//')
|
|
77
|
-
and len(path) <= 2048 and not re.search(r'[\\\x00-\x1f\x7f]', path)
|
|
77
|
+
and len(path) <= 2048 and not re.search(r'[\\\x00-\x1f\x7f]', path)
|
|
78
|
+
# JavaScript's whitespace (\s), which the contract's parseDashboardsLink refuses in a page (DEC-0123)
|
|
79
|
+
and not re.search('[ \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]', path))
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
def check(data):
|
|
@@ -87,11 +89,11 @@ def check(data):
|
|
|
87
89
|
require(not re.search(r'[\\\x00-\x20\x7f]', native + http), 'unsafe_url')
|
|
88
90
|
deep, plain = urlsplit(native), urlsplit(http)
|
|
89
91
|
require(deep.scheme == 'fabric-dashboards' and not deep.username and not deep.password and not deep.port and not deep.fragment, 'invalid_deep_link')
|
|
90
|
-
query
|
|
91
|
-
if deep.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
# An empty query is no fields: Python 3.9's strict parse_qs raises on '' where 3.11 and later answer {}.
|
|
93
|
+
query = parse_qs(deep.query, keep_blank_values=True, strict_parsing=True) if deep.query else {}
|
|
94
|
+
# DEC-0123 PL-1: only the service form; the legacy open?service= link is refused (FAC-SEM-143, since 0.51.0).
|
|
95
|
+
require(deep.netloc == 'service', 'not_service_link')
|
|
96
|
+
require(deep.path in ('/' + service, '/' + service + '/') and set(query) <= {'path'}, 'wrong_service_or_parameter')
|
|
95
97
|
require('path' not in query or len(query['path']) == 1, 'duplicate_path')
|
|
96
98
|
page = query.get('path', [None])[0]
|
|
97
99
|
require(page is None or safe_path(page), 'unsafe_path')
|
|
@@ -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.
|
|
17
|
+
version: "0.51.2"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
|
-
extension-commit: "
|
|
20
|
+
extension-commit: "e6c4fc16b415eec3190dd34d924d269405359dc9"
|
|
21
21
|
---
|
|
22
22
|
|
|
23
23
|
# Building Fabric services
|
|
@@ -72,6 +72,7 @@ Answer before writing code, and record the answers in the project README:
|
|
|
72
72
|
| Another agent on this machine | MCP `2026-07-28`, `streamable-http`, at `/mcp` on the service origin, token in a header | one process serves every session; stdio-per-session once left twenty stray servers on this machine |
|
|
73
73
|
| The operator or a script | a CLI: `<tool> service status\|start\|stop\|restart`, `<tool> dashboard` (opens only in Fabric Dashboards, below), `<tool> doctor --json` | every verb delegates to launchd and the well-known document, never to its own process table |
|
|
74
74
|
| A remote agent | A2A `1.0` over HTTPS, or MCP behind an authenticated gateway | loopback reachability is not authorization |
|
|
75
|
+
| An A2A peer on this computer | `surfaces.a2a` names a public Agent Card, served without the token; its interface URLs are loopback (DEC-0122); the kits read it, serving one is FAA-10 | the card carries no credential, process member or required Fabric extension |
|
|
75
76
|
| The dashboard page | same-origin REST under `/api`, session cookie plus a custom request header; every button on it also a tool on the MCP surface (Step 5) | the CSRF defence all four existing dashboards use |
|
|
76
77
|
|
|
77
78
|
Read [the surfaces and auth reference](references/surfaces-and-auth.md) when wiring
|
|
@@ -298,7 +299,9 @@ launchd pid equals answering pid, `dashboard.declared` (ED-9), `well-known.actio
|
|
|
298
299
|
opener aimed at the service's pages in its checkout; `--repo PATH` names the checkout, and a service installed
|
|
299
300
|
from a release folder needs it), and the `interop.*` rules:
|
|
300
301
|
manifest link, capability list, tools equal to the manifest, job tools, unknown job, trace
|
|
301
|
-
propagation, event trace pairs, `interop.action-tools` (every declared action's tool is served), and
|
|
302
|
+
propagation, event trace pairs, `interop.action-tools` (every declared action's tool is served), `well-known.a2a` and
|
|
303
|
+
`a2a.card` (DEC-0122: the card is fetched without the token and its interfaces are never called; the surfaces
|
|
304
|
+
reference's *A2A for remote agents* section), and
|
|
302
305
|
`board.responder` for a service that declares the project board) and exits 1 on any `FAIL`. A `NOT_RUN` is not a pass;
|
|
303
306
|
name it in the report. A service with a dashboard also runs the layout probe,
|
|
304
307
|
`node <skill-dir>/scripts/check_dashboard_layout.mjs --service <id>` (WebKit at 360, 480, 768
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md
CHANGED
|
@@ -101,7 +101,9 @@ version is still read and never written; delete it once its messages are past th
|
|
|
101
101
|
- Replace the example knowledge with the product's own, and run real past questions through it ([practices](../../../creating-fabric-agents/references/practices.md#judges-and-evals), *Judges
|
|
102
102
|
and evals*): count handoffs and read a sample of replies.
|
|
103
103
|
- Show spend: `fabric_usage.usage_report` over the ledger, or `fabric_openrouter.totals`.
|
|
104
|
-
- One decision per message id holds in one process
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
- One decision per message id holds in one process. Each process reads `decisions.jsonl` once and keeps the index in
|
|
105
|
+
memory, so a second process writing the same state folder is not seen: a service runs one instance per context with
|
|
106
|
+
`fabric_context.hold_context_instance` (Node: `holdContextInstance`) and points `stateDir` at that instance's data
|
|
107
|
+
folder, so a second copy for the same context exits 75 and an organization's copy keeps its own folder
|
|
108
|
+
([one instance per context](../../references/estate-record.md#one-instance-per-context-the-helper)).
|
|
107
109
|
- Set the outbox's retention and schedule `prune`.
|
|
@@ -86,7 +86,9 @@ 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.
|
|
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
|
|
91
|
+
once the kit reads it (FAA-135).
|
|
90
92
|
|
|
91
93
|
## Effects, observation, evidence and the reply
|
|
92
94
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md
CHANGED
|
@@ -140,5 +140,8 @@ or instructions alone do not prove all final answers from every agent are filter
|
|
|
140
140
|
|
|
141
141
|
Syntax is owned by Fabric Dashboards and `@passioncode-ai/fabric-service-host`:
|
|
142
142
|
[ADR-0005](https://github.com/passioncode-ai/fabric-dashboards/blob/51a7a80acf42781d7bec304cc87c655d48cc309b/docs/adr/0005-service-links.md).
|
|
143
|
-
The gate accepts
|
|
144
|
-
|
|
143
|
+
The gate accepts only the service form, `fabric-dashboards://service/SERVICE_ID.INSTANCE`
|
|
144
|
+
with at most one `path`; since 0.51.0 it refuses the legacy `open?service=` form, as contract
|
|
145
|
+
DEC-0123 (PL-1, checker rule `FAC-SEM-143`) and its vectors do. A host that still returns the
|
|
146
|
+
legacy form fails the gate: report it as an installed host that did not open the page. The gate
|
|
147
|
+
does not generate links or redefine the Fabric wire contract.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md
CHANGED
|
@@ -100,7 +100,10 @@ operator's decision), `decision-invalid` (a decision for another action or objec
|
|
|
100
100
|
and `human-step-required`, which carries `data.humanStep` (`sign-in`, `consent`, `payment`, `secret`,
|
|
101
101
|
`spend-above-limit`, `passkey`) and `data.url` (an https URL or a path on the service's origin) beside `detail`.
|
|
102
102
|
`refusal_from_action` / `refusalFromAction` turn an action decision into that refusal; a service's own fault never
|
|
103
|
-
reaches the caller and reads as `unknown-state`.
|
|
103
|
+
reaches the caller and reads as `unknown-state`. Since 0.51.0 the `operator-decision-required` message (free text in
|
|
104
|
+
DL-19, at most 500 characters) also names, in order, the routes for a client that cannot set `_meta`: Fabric's
|
|
105
|
+
`agent.call` with `call.decision` once the hub forwards it (CT-125), the service's own elicitation, else the action's
|
|
106
|
+
page as Fabric Dashboards' `open_link`; both kits carry the same sentence. The gate that answers them is in
|
|
104
107
|
[the interop reference](interop.md#actions).
|
|
105
108
|
|
|
106
109
|
## Keys and the cache
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md
CHANGED
|
@@ -21,6 +21,7 @@ derived from its own digest. Python: [`scripts/fabric_estate.py`](../scripts/fab
|
|
|
21
21
|
- Applying a revision
|
|
22
22
|
- Trusting a delegation key
|
|
23
23
|
- Contexts: one agent for you and for an organization
|
|
24
|
+
- One instance per context: the helper
|
|
24
25
|
- Machine profiles: full, member, view
|
|
25
26
|
- The delta anchor
|
|
26
27
|
- The estate ceremonies
|
|
@@ -133,6 +134,65 @@ Every rule is total on hostile JSON, as the contract's are: an `instancePath` na
|
|
|
133
134
|
non-object before it does not shift it), and a value whose own `toString` member is data is shown as its JSON. Both kits
|
|
134
135
|
answer the contract's contexts fixtures and variants of them (`test/fixtures/contract/estate-vectors.json`).
|
|
135
136
|
|
|
137
|
+
## One instance per context: the helper
|
|
138
|
+
|
|
139
|
+
A service that runs for the person and for an organization runs once per context on a machine (contract
|
|
140
|
+
`contexts.md`, *One instance per context on a machine*; FAA-125). `fabric_context` / `fabric-context.mjs` turns the
|
|
141
|
+
context it is started for into that context's one instance. It is not in the facade: install it with
|
|
142
|
+
`fabric_kit.py install <dir> --module fabric_context` (Node: `--lang node --module fabric-context`), so a service that
|
|
143
|
+
does not run per context keeps the estate reader out of its closure.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import fabric_context as fc
|
|
147
|
+
info, lock = fc.hold_context_instance("example-agent", "org:acme-rehearsal", record=applied_revision,
|
|
148
|
+
machine_id="machine-a", agent_ref="urn:agent:example-agent")
|
|
149
|
+
serve(data=info["dirs"]["data"], instance=info["instance"]) # org-acme-rehearsal; lock held until exit
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
import { holdContextInstance } from './fabric-context.mjs';
|
|
154
|
+
const { dirs, instance, lock } = await holdContextInstance('example-agent', 'org:acme-rehearsal', { record, machineId, agentRef });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **The instance name.** `personal` is `default` (`personal_instance` / `personalInstance`), so an existing install
|
|
158
|
+
keeps its descriptor `<id>.default.json`, its folders and its lock. `org:<orgId>` is `org-<orgId>` when the id is
|
|
159
|
+
lower-case words joined by single hyphens, at most 28 characters, and does not end in a hyphen and 10 hex digits
|
|
160
|
+
(`org:acme-rehearsal` is `org-acme-rehearsal`); any other id is `org-<slug>-<first 10 hex digits of sha256(orgId)>`
|
|
161
|
+
(`org:Acme` is `org-acme-37036cd8f9`). `context_instance_name` / `contextInstanceName` answer it alone.
|
|
162
|
+
- **Its folders.** `instance_dirs(id, instance)` / `instanceDirs(id, instance, platform)`: `default` is
|
|
163
|
+
`service_dirs`; any other is a sibling named as its descriptor, `<id>.<instance>` (on macOS
|
|
164
|
+
`~/Library/Application Support/<id>.<instance>`, `~/Library/Logs/<id>.<instance>`, `~/Library/Caches/<id>.<instance>`),
|
|
165
|
+
never inside the personal tree. `launchd_uninstall(..., purge=True, service_id=..., instance=...)` /
|
|
166
|
+
`launchdUninstall(..., {purge, serviceId, instance})` purge only that instance's folders.
|
|
167
|
+
- **What it checks, before anything is created.** `context_instance` / `contextInstance` is pure and answers
|
|
168
|
+
`{context, kind, org?, instance, key, dirs, descriptorPath, grant?}`, never the record. An organization's instance
|
|
169
|
+
needs the estate record revision this machine applied (verified with `verify_estate_record`; the helper verifies no
|
|
170
|
+
signature), the machine id and the agentRef, all passed by whoever installed the instance. It finds this machine's
|
|
171
|
+
placement of that agent in that context, as written (FAC-SEM-074), runs FAC-SEM-111 on it
|
|
172
|
+
(`placement_grant_findings` / `placementGrantFindings`), and refuses outside the grant's window (`startsAt` <= now <
|
|
173
|
+
`expiresAt`); `instance_share_grant` / `instanceShareGrant` answers the three alone. A personal instance needs no
|
|
174
|
+
record; given one, a personal placement that names a grant is refused.
|
|
175
|
+
- **What it holds.** `hold_context_instance` / `holdContextInstance` then takes the instance lock on the instance's
|
|
176
|
+
data folder (`hold_single_instance`: a second copy for the same context exits 75, a supervised one waits first),
|
|
177
|
+
then the marker `<data>/fabric-context.json` (`{service, instance, context}`, 0600): absent, it is written; equal,
|
|
178
|
+
kept; another context or instance, or unreadable, releases the lock and refuses. The folder never changes context.
|
|
179
|
+
|
|
180
|
+
A refusal is one sentence on stderr naming the code and the context, never a record or grant value, and exit 78
|
|
181
|
+
(`EXIT_CONTEXT_REFUSED`, EX_CONFIG; 75 stays "already running"). `ContextRefused` carries the code:
|
|
182
|
+
|
|
183
|
+
| Code | When |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `context-invalid` | the context is neither `personal` nor `org:<orgId>` as written (`Personal`, `ORG:x`) |
|
|
186
|
+
| `record-required` | an organization's context with no record, machine id or agentRef (fail closed) |
|
|
187
|
+
| `placement-missing` | the record places no such agent on this machine in that context |
|
|
188
|
+
| `grant-refused` | the placement names no share grant of the record for that context, or a revoked one (FAC-SEM-111); a personal placement names one |
|
|
189
|
+
| `grant-window` | now is before the grant's `startsAt`, at or after its `expiresAt`, or an instant cannot be read |
|
|
190
|
+
| `context-mismatch` | the data folder's `fabric-context.json` names another context or instance, or cannot be read |
|
|
191
|
+
|
|
192
|
+
Under launchd's `KeepAlive` a refused instance is started again every 10 s until the node's next revision removes it;
|
|
193
|
+
a revoked grant stops calls at once at the hub and the meter (DL-24), not here: the helper reads revisions only.
|
|
194
|
+
Tests: `test/test_fabric_context.py`, `test/fabric-context.test.mjs` over `test/fixtures/kit-context-cases.json`.
|
|
195
|
+
|
|
136
196
|
## Machine profiles: full, member, view
|
|
137
197
|
|
|
138
198
|
DEC-0106 gives a machine an optional `profile`. It says what the machine may be when other machines of the estate call
|
|
@@ -12,7 +12,7 @@ A service that other agents (and Fabric) call does five things. Each has one hel
|
|
|
12
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
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
|
-
| 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` |
|
|
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. A URL-mode request is an `https` URL or, for a page of this service, its Fabric Dashboards open_link (DEC-0123 PL-4): `url_request` refuses a raw loopback address (`http://127.0.0.1:4100/x`, `http://127.1/`, `http://[::1]/`, `http://localhost/` in any spelling) and a `fabric-dashboards:` link outside the grammar when the request is built, and `request_input` refuses a hand-built request either way (FAC-SEM-143). A tool answer that hands a person a page is `page_answer`'s `{open_link, http_url?}`; `http_url` is a diagnostic | `choice_request`, `form_request`, `url_request`, `page_answer` / `pageAnswer`; the rules in `fabric_page_links` / `fabric-page-links.mjs` |
|
|
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
|
|
|
18
18
|
The interop block sits in the manifest under the capability's `extensions`:
|
|
@@ -28,6 +28,7 @@ That key has one spelling (the contract's `src/extensions.ts`); copy it, never r
|
|
|
28
28
|
- [The kit](#the-kit)
|
|
29
29
|
- [Limits and uncertain effects](#limits-and-uncertain-effects)
|
|
30
30
|
- [Actions: every button has a tool](#actions)
|
|
31
|
+
- [The operator's mandate](#the-operators-mandate)
|
|
31
32
|
- [What the probe checks](#what-the-probe-checks)
|
|
32
33
|
|
|
33
34
|
## The kit
|
|
@@ -228,6 +229,64 @@ The probe reads the declaration (`well-known.actions`), checks that `tools/list`
|
|
|
228
229
|
(`interop.action-tools`) and reports a dashboard that declares no actions as `NOT_RUN` with the reason
|
|
229
230
|
(`dashboard.actions-declared`): absence does not prove a page-only button.
|
|
230
231
|
|
|
232
|
+
<a id="the-operators-mandate"></a>
|
|
233
|
+
|
|
234
|
+
## The operator's mandate
|
|
235
|
+
|
|
236
|
+
Contract DEC-0125 (`mandate/1`, rule `FAC-SEM-144`, `docs/specification/delegation.md#the-operators-mandate`, MD-1 to
|
|
237
|
+
MD-6): the operator's instruction for one task travels with the task, narrowed at each hand-over, so a service the
|
|
238
|
+
task reaches takes a call the mandate covers as his decision without asking him again. It is the machine form of his
|
|
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.
|
|
242
|
+
|
|
243
|
+
**As a service.** Name your own agent id and each relayed action's effect class; the gate then reads the call's
|
|
244
|
+
`_meta.mandate` when it carries no decision:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
server = fi.McpToolServer("example-agent", "1.0.0", jobs=jobs, authorize=gate, agent_id="example-agent")
|
|
248
|
+
server.add_tool(approve_tool, approve_reply, action=reply_action, target="reply_id", effect="reply",
|
|
249
|
+
spend=lambda arguments: estimate_usd(arguments), project="example-site")
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- `McpToolServer(agent_id=)` is this service's agent id (a lower-case id, optionally `.instance`). Without it a mandate
|
|
253
|
+
is not read and every call is answered as before DEC-0125.
|
|
254
|
+
- A `spend` or `project` function that raises, or a spend that is not a finite number, fails closed: the call is
|
|
255
|
+
answered, and a mandate with a `perCallMaxUsd` ceiling or a `projects` list refuses it (`outside-mandate: spend` or
|
|
256
|
+
`outside-mandate: project`); the estimate is read as a double, so an integer past the float range is infinite and
|
|
257
|
+
refused under a ceiling, and a project function that answers anything but a string or `None` names no project a
|
|
258
|
+
mandate can list. A refusal outside a mandate
|
|
259
|
+
tells the caller the MD-4 order: call again within its own mandate, else pass the refusal up; only the leading agent
|
|
260
|
+
asks the operator.
|
|
261
|
+
- `add_tool(tool, handler, action=, effect=, spend=, project=)`: on a server with `agent_id`, a `relayed` action names
|
|
262
|
+
its `effect` (governance's class: `none`, `draft`, `publish`, `send`, `reply`, `charge`, `delete`, `merge`, `deploy`,
|
|
263
|
+
`change-policy`) or `add_tool` refuses it; `spend` is the service's own estimate of a call's USD (a number, or a
|
|
264
|
+
function of the arguments), read against the mandate's `perCallMaxUsd`; `project` the project a call acts in.
|
|
265
|
+
- The mandate is read from `_meta.mandate` only, never from its arguments. Its chain must end in `agent_id`; when
|
|
266
|
+
`authorize` accepted a delegation token, the token's caller must be the agent before it in the chain
|
|
267
|
+
(`mandate-not-for-this-call` otherwise). A T2 caller's mandate changes nothing (MD-5): it still gets
|
|
268
|
+
`approval-required`.
|
|
269
|
+
- A covered call is the operator's act: `ctx.record` is `{by: "operator", via: "mandate", task, chain, caller}` and
|
|
270
|
+
`ctx.mandate` the mandate it took (its `instruction` stays in the audit record, never in events or a page). Outside
|
|
271
|
+
it the answer is `operator-decision-required` with `data.detail` `outside-mandate: <action|capability|effect|spend|project>`
|
|
272
|
+
(and `estimateUsd`, `perCallMaxUsd` for spend); an invalid one is `decision-invalid` with `mandate-malformed`,
|
|
273
|
+
`mandate-widened`, `mandate-expired`, `mandate-early` or `mandate-not-for-this-call`. A `change-policy` effect is
|
|
274
|
+
never covered (AP-5); human steps never travel (MD-6).
|
|
275
|
+
- `action_gate(..., self_id=, effect=, caller_agent=, spend_usd=, project=)` / `actionGate({..., self, effect,
|
|
276
|
+
callerAgent, spendUsd, project})` is the same gate for a server of your own; `action_decision` / `actionDecision`
|
|
277
|
+
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.
|
|
280
|
+
|
|
281
|
+
**As a caller.** The agent leading a task states the mandate from the operator's own words with
|
|
282
|
+
`operator_mandate(task, instruction, scope, lead, at=, expires_at=)` / `operatorMandate({task, instruction, scope,
|
|
283
|
+
lead, at, expiresAt})` (a day unless told, at most seven), hands it to the agent it calls with
|
|
284
|
+
`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.
|
|
289
|
+
|
|
231
290
|
## What the probe checks
|
|
232
291
|
|
|
233
292
|
`check_service.py` adds these rules; they read, and never call a capability.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -23,6 +23,7 @@ python3 $K/fabric_kit.py install <service-dir> --module fabric_service --module
|
|
|
23
23
|
# what sample_service.py imports
|
|
24
24
|
python3 $K/fabric_kit.py install <service-dir> --lang node # fabric-service.mjs and its closure
|
|
25
25
|
python3 $K/fabric_kit.py install <service-dir> --module fabric_openrouter --module fabric_board
|
|
26
|
+
python3 $K/fabric_kit.py install <service-dir> --module fabric_service --module fabric_context # one instance per context
|
|
26
27
|
python3 $K/fabric_kit.py install <service-dir> --all --dry-run # every module, nothing written
|
|
27
28
|
python3 $K/fabric_kit.py status <service-dir> [--json] # current, older (the release), modified, missing
|
|
28
29
|
python3 $K/fabric_kit.py build [--check] # maintainers: refresh kit.json from the sources (refuses a tagged version)
|
|
@@ -56,20 +57,24 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
56
57
|
| L1 | `fabric_crypto` | Ed25519 signing (a Montgomery ladder that does not branch on the key) and verification, P-256 verification, base64url, sha256 |
|
|
57
58
|
| L1 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
58
59
|
| L1 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
|
|
59
|
-
| L1 | `fabric_paths` | the services directory, a service's folders, atomic and private writes (L1 since 0.44.2, so `fabric_delegation` writes its consumed approval ids with them, FAA-85) |
|
|
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) |
|
|
60
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
63
|
| L2 | `fabric_delegation` | the callee's side of `fabric-delegation/0.1` (DEC-0039) |
|
|
62
|
-
| L2 | `fabric_share` | share grants and registrations (DEC-0086) |
|
|
64
|
+
| L2 | `fabric_share` | share grants and registrations (DEC-0086); whether an organization's instance may run under its grant (`instance_share_grant`, FAA-125) |
|
|
63
65
|
| L2 | `fabric_ceremonies` | the headless join token (DEC-0084) |
|
|
64
66
|
| L2 | `fabric_package_schema` | the reading of `fabric-package.schema.json` with JavaScript's pattern and format semantics (FAA-92) |
|
|
65
67
|
| L2 | `fabric_package` | `fabric-package/1`: pack, sign, verify; re-exports `schema_problems` (DEC-0050, DEC-0051) |
|
|
66
68
|
| L3 | `fabric_protocols` | protocol revisions (DEC-0067) and feed data (DEC-0070) |
|
|
67
69
|
| L3 | `fabric_actions` | every action a person has and its agent tool, `fabric-actions/0.1`: the gate a tool runs (`action_decision`), the reader and FAC-SEM-142, the declaration (DEC-0121) |
|
|
68
70
|
| L3 | `fabric_connections` | what an agent uses, `fabric-connections/0.1`: the reference reading and the declaration (DEC-0096, FAC-SEM-127) |
|
|
71
|
+
| L3 | `fabric_a2a` | the reading side of an A2A Agent Card: `surfaces.a2a` as the schema reads it, and what a public card must not carry (DEC-0122) |
|
|
72
|
+
| L3 | `fabric_page_links` | a service's pages as Fabric Dashboards' open_link: build and read it, a raw local address, FAC-SEM-143 on a link and on a URL-mode input request (DEC-0123); the Python twin reads URLs as WHATWG does by hand |
|
|
69
73
|
| L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
|
|
70
74
|
| L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
|
|
71
75
|
| L3 | `fabric_backups` | settings backups (DEC-0025) |
|
|
72
76
|
| L3 | `fabric_descriptor` | the descriptor, peers, app descriptors, the well-known document |
|
|
77
|
+
| L3 | `fabric_context` | one instance per context (FAA-125, N-11): a context's instance, folders, descriptor, lock and marker; not in the facade, installed with `--module fabric_context` ([contexts](estate-record.md#one-instance-per-context-the-helper)) |
|
|
73
78
|
| L3 | `fabric_auth` | the token, the network guard, login codes |
|
|
74
79
|
| L3 | `fabric_events` | events, paging, the JSONL event log (DEC-0029) |
|
|
75
80
|
| L3 | `fabric_logs` | rotating logs, the stop signal and the drain (LC-12) |
|
|
@@ -121,8 +121,10 @@ PA-03).
|
|
|
121
121
|
|
|
122
122
|
- **ES256.** It waits for Fabric's decision D2; until then a verifier refuses it as `unknown-algorithm` (FAA-29).
|
|
123
123
|
- **Hub signing for the person's own machines, and the passkey.** Stage 3 of target architecture v4.
|
|
124
|
-
- **The organization's countersignature as a passkey assertion.** It waits for
|
|
125
|
-
`org-
|
|
124
|
+
- **The organization's countersignature as a passkey assertion.** It waits for the decision that defines the
|
|
125
|
+
`org-countersign/1` verifier (contract `docs/specification/package.md`, "Signatures" and "Not decided here"; its
|
|
126
|
+
challenge type is registered with DEC-0063). DEC-0129 decided `org-authority/1` only as the organization's member
|
|
127
|
+
list for relay admission. This version's countersignature is a detached Ed25519 signature.
|
|
126
128
|
- **Fabric recording packages by `agentRef`.** It waits for the ADR-0138 D2b amendment.
|
|
127
129
|
- **`publish --to org`** (FAA-28; an organization cloud's import API is FAA-63, under FAA-28's rule that publishing to
|
|
128
130
|
an organization is the person's own act).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fabric-service/0.1: wire reference
|
|
2
2
|
|
|
3
|
-
Pinned to `fabric-agent-contract` commit `
|
|
3
|
+
Pinned to `fabric-agent-contract` commit `e6c4fc16b415eec3190dd34d924d269405359dc9`
|
|
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;
|
|
@@ -17,7 +17,9 @@ no-token `401` DEC-0109; money apart from a subscription's estimate DEC-0110; th
|
|
|
17
17
|
protocols of DEC-0111 in the register; a check-in's credential members DEC-0112, a revocation that names its request
|
|
18
18
|
DEC-0113, a node summary against the certificate DEC-0114, key windows read strictly DEC-0115 and DEC-0119, which list
|
|
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
|
-
has as an agent tool, `fabric-actions/0.1` and the operator's relayed decision DEC-0121
|
|
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
|
+
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;
|
|
21
23
|
this page is the working summary. Extension key:
|
|
22
24
|
`https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
|
|
23
25
|
|
|
@@ -96,6 +98,13 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
96
98
|
`operatorChannel.path` (DEC-0034) when the service has an operator channel; its status
|
|
97
99
|
document follows the contract's `operator-channel-status.schema.json` and is token-protected
|
|
98
100
|
like the events feed. The kits do not serve it yet.
|
|
101
|
+
- `surfaces.a2a` (DEC-0122) SHOULD name the public A2A Agent Card of a service that serves one: `{path,
|
|
102
|
+
protocolVersion}`, `path` a path on the service origin (normally `/.well-known/agent-card.json`), `protocolVersion`
|
|
103
|
+
`"1.0"`, nothing else; the card itself names its interfaces, bindings and security schemes. A reader pinned before
|
|
104
|
+
DEC-0122 refuses the member. The kits build it with `a2a_surface()` / `a2aSurface()` (`fabric_a2a`), read it with
|
|
105
|
+
`a2a_view` / `a2aView`, and `build_well_known(a2a=...)` / `buildWellKnown({a2a})` refuse one that is not the
|
|
106
|
+
schema's, passed either way; `agent_card_findings` / `agentCardFindings` say what a public card must not carry
|
|
107
|
+
([surfaces and auth](surfaces-and-auth.md#a2a-for-remote-agents)). The kits do not serve a card yet (FAA-10).
|
|
99
108
|
- `extensions` MAY carry the embedded dashboard declaration under
|
|
100
109
|
`https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1` (`fabric-dashboard/0.1`,
|
|
101
110
|
ED-9): `minWidth` 360…1280 (required), `appearance` ⊆ `scheme`, `theme`, `themes`, `kit`
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Surfaces and auth
|
|
2
2
|
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- [MCP for agents](#mcp-for-agents)
|
|
6
|
+
- [CLI for people and scripts](#cli-for-people-and-scripts)
|
|
7
|
+
- [A2A for remote agents](#a2a-for-remote-agents)
|
|
8
|
+
- [Token](#token)
|
|
9
|
+
- [Dashboard session](#dashboard-session)
|
|
10
|
+
- [Headers every response carries](#headers-every-response-carries)
|
|
11
|
+
|
|
3
12
|
## MCP for agents
|
|
4
13
|
|
|
5
14
|
- One server inside the service process, `streamable-http`, at `/mcp` on the service
|
|
@@ -45,6 +54,32 @@ MCP, never by binding a LAN address. A tunnel (Tailscale Serve and the like) is
|
|
|
45
54
|
outward surface with its own token lifetime; watch it like a service, because nothing
|
|
46
55
|
else will notice it expire.
|
|
47
56
|
|
|
57
|
+
A service that answers A2A peers names its public Agent Card in the well-known document's `surfaces.a2a`
|
|
58
|
+
(DEC-0122; [the wire reference](protocol.md)). What that card may say:
|
|
59
|
+
|
|
60
|
+
- On a local placement the public card answers without the token (Host and Origin checks still apply) and its
|
|
61
|
+
interface URLs are on the service's loopback origin, for peers on this computer; on a remote placement they are
|
|
62
|
+
`https`, and a card that is not open is gated by the service's own credentials or an identity proxy.
|
|
63
|
+
- An A2A peer is never given the descriptor's token: no interface accepts it and no `securitySchemes` entry names
|
|
64
|
+
it. A2A callers bring the service's own credentials or the estate's delegation tokens; the extended card needs
|
|
65
|
+
one of them.
|
|
66
|
+
- The public card carries no secret and none of the well-known document's process members (`build`, `process`,
|
|
67
|
+
`pid`, `status`, `summary`, `degraded`). An extension it declares for a Fabric fact (`uri` under
|
|
68
|
+
`https://fabric.passioncode.ai/`) carries `required: false`; the probe also accepts it absent, which A2A reads as
|
|
69
|
+
false (a protobuf-written card drops a false default), and flags `required: true` or a non-boolean.
|
|
70
|
+
|
|
71
|
+
`agent_card_findings(card, origin=, remote=, descriptor_header=)` / `agentCardFindings(card, {origin, remote,
|
|
72
|
+
descriptorHeader})` report each of these by `code` and place, never the value. The probe runs two rules:
|
|
73
|
+
|
|
74
|
+
| Rule | PASS when | NOT_RUN when |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `well-known.a2a` | `surfaces.a2a` is the schema's (`FAIL` names each schema error's place and keyword) | no `surfaces.a2a` |
|
|
77
|
+
| `a2a.card` | the card at `surfaces.a2a.path`, asked with no `Authorization` header and never the descriptor's token, answers 200 with JSON that has no finding; on a local placement any other answer is a `FAIL` | no member, an unreadable one, or a remote card that answers 401 or 403 (a card that is not open; a refusal is not nonconformance) |
|
|
78
|
+
|
|
79
|
+
A probe cannot see whether an interface would take the descriptor's token without sending it, which DEC-0122
|
|
80
|
+
forbids, so it reads the card only and never calls an interface: an API-key scheme in the descriptor's own
|
|
81
|
+
`auth.header` is a finding, and with `Authorization` (the default) the card cannot show it.
|
|
82
|
+
|
|
48
83
|
## Token
|
|
49
84
|
|
|
50
85
|
- Created once with `ensure_token` at first start, after the instance lock.
|