@passioncode-ai/passioncode 0.2.38 → 0.2.39
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 +8 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +29 -11
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +53 -3
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +11 -3
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +47 -12
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +5 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +3 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +53 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +13 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +10 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +99 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +45 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +325 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +246 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +3 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +22 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +45 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +302 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +22 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +154 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +45 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +9 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +124 -16
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -11
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +53 -3
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +11 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +41 -9
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
|
@@ -95,6 +95,14 @@ profile does not allow). A published key the service cannot use (of small order,
|
|
|
95
95
|
issuer's fault, `issuer-unavailable` (DEC-0045); a gate without a trusted issuer refuses everything as
|
|
96
96
|
`no-trusted-issuer`. `detail` is a short machine word; the token, the approval and the keys never appear in a result or a log.
|
|
97
97
|
|
|
98
|
+
DEC-0121 adds three codes to the one set, each 403: `operator-decision-required` (a `relayed` action called without the
|
|
99
|
+
operator's decision), `decision-invalid` (a decision for another action or object, outside its 24 hours, or malformed)
|
|
100
|
+
and `human-step-required`, which carries `data.humanStep` (`sign-in`, `consent`, `payment`, `secret`,
|
|
101
|
+
`spend-above-limit`, `passkey`) and `data.url` (an https URL or a path on the service's origin) beside `detail`.
|
|
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`. The gate that answers them is in
|
|
104
|
+
[the interop reference](interop.md#actions).
|
|
105
|
+
|
|
98
106
|
## Keys and the cache
|
|
99
107
|
|
|
100
108
|
`JwksCache` holds the estate's key set at most 10 minutes and, on a `kid` it does not hold, re-reads at most once
|
|
@@ -124,4 +132,5 @@ current verified revision as `estate=` / `{ estate }`, as a value or as a functi
|
|
|
124
132
|
## What the kit does not do
|
|
125
133
|
|
|
126
134
|
It does not mint tokens, check attenuation against a parent (the issuer's rule, DL-5), check a grant or a spend
|
|
127
|
-
window (the meter's, DL-21), or decide which actions need an approval
|
|
135
|
+
window (the meter's, DL-21), or decide which actions need an approval or the operator's decision: the service
|
|
136
|
+
declares that in its action table (each action's gate, [interop](interop.md#actions)).
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md
CHANGED
|
@@ -27,6 +27,7 @@ That key has one spelling (the contract's `src/extensions.ts`); copy it, never r
|
|
|
27
27
|
|
|
28
28
|
- [The kit](#the-kit)
|
|
29
29
|
- [Limits and uncertain effects](#limits-and-uncertain-effects)
|
|
30
|
+
- [Actions: every button has a tool](#actions)
|
|
30
31
|
- [What the probe checks](#what-the-probe-checks)
|
|
31
32
|
|
|
32
33
|
## The kit
|
|
@@ -114,7 +115,8 @@ As a provider:
|
|
|
114
115
|
`tokens_measured=False` / `tokensMeasured: false` and the tokens you have, so your own usage report counts it in
|
|
115
116
|
`unmeasuredCalls` and does not show the lower bound as complete (DEC-0091, [usage](usage.md)).
|
|
116
117
|
|
|
117
|
-
As a caller: send `request_meta(traceparent)` as your `_meta` (it names `fabric-interop/0.1` and `0.2
|
|
118
|
+
As a caller: send `request_meta(traceparent)` as your `_meta` (it names `fabric-interop/0.1` and `0.2`; for a relayed
|
|
119
|
+
action it also carries the operator's decision, [Actions](#actions)), act on every
|
|
118
120
|
`fabric.job.get` answer as `job_caller_action(answer, now)` says, and resend only under
|
|
119
121
|
`intent_idempotency_key({agentId, capability, input})`, never a new key. Node names are the camelCase twins
|
|
120
122
|
(`jobs.quotaExhausted(id, message, {retryable, resetsAt})`, `jobCallerAction`, `intentIdempotencyKey`). The guidance
|
|
@@ -131,6 +133,101 @@ a caller today sees `product-error` instead, kept under the key for 24 hours, un
|
|
|
131
133
|
`fabric.job.list`, so its `unknown-job` and repeated keys keep the rules above. A node that will queue needs
|
|
132
134
|
`jobCallerKey`, the contract's `agent-network.json` vectors and the load report first (FAA-103).
|
|
133
135
|
|
|
136
|
+
<a id="actions"></a>
|
|
137
|
+
|
|
138
|
+
## Actions
|
|
139
|
+
|
|
140
|
+
Contract DEC-0121 (`fabric-actions/0.1`, rule `FAC-SEM-142`, `docs/specification/service.md#actions`, AP-1 to AP-5):
|
|
141
|
+
every button a person has, the person's agent has, and an operator's decision relayed by their own agent is theirs.
|
|
142
|
+
Both kits carry the reference (`fabric_actions.py`, `fabric-actions.mjs`: `action_decision` / `actionDecision`,
|
|
143
|
+
`actions_view` / `actionsView`) and answer the contract's `actions.json` vectors case for case.
|
|
144
|
+
|
|
145
|
+
**When a service adopts it.** The contract says a service pins DEC-0121 after both the kits (CT-124, this release)
|
|
146
|
+
and Fabric's hub forwarding decisions (CT-125) have landed (DEC-0121 *Compatibility*, CT-127). The reason is the
|
|
147
|
+
operator's own coding agents: Claude Code and Codex cannot set `_meta`, so until the hub forwards `call.decision`
|
|
148
|
+
a `relayed` gate turns "approve it" in chat into `operator-decision-required` with the page as the only route.
|
|
149
|
+
Until then a service keeps its tools as they answer today, or gates a `relayed` tool only where it offers its own
|
|
150
|
+
elicitation (the `elicited` hook). This is D8's reason, which keeps `input_actions` off, applied to tools.
|
|
151
|
+
|
|
152
|
+
**Parity (AP-1).** Every action the dashboard, app or page lets a person take (approve, reject, choose, edit, pause,
|
|
153
|
+
resume, cancel, retry, send, publish, restore, set a policy, any other) has a tool on the MCP surface with the same
|
|
154
|
+
effect, the same checks, the same audit record and an idempotency key: the button's agent twin. A page's confirm
|
|
155
|
+
dialog is one of those checks; an instruction that names the action satisfies it. Each action has a gate:
|
|
156
|
+
|
|
157
|
+
| `gate` | For | The tool answers |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `none` | an act on the caller's own account: refresh, read, retry or cancel its own job | the caller's own act |
|
|
160
|
+
| `relayed` | a decision on another party's proposal or draft, a send, a publish, a deletion, a spend within a limit, a restore, a policy change | the operator's act when a T0 or T1 caller carries a valid decision; else a refusal below |
|
|
161
|
+
| `human` with `humanStep` | `sign-in`, `consent`, `payment`, `secret`, `spend-above-limit`, `passkey` (AP-4): they stay the person's | `human-step-required` with `data.humanStep` and `data.url`, the direct link to that step |
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
server = fi.McpToolServer("example-agent", "1.0.0", jobs=jobs, authorize=gate,
|
|
165
|
+
identify=caller_of, approve=approval_of, elicited=elicited_of)
|
|
166
|
+
server.add_tool(approve_tool, approve_reply, action={"id": "reply.approve", "title": "Approve a reply",
|
|
167
|
+
"tool": "replies.approve", "gate": "relayed"}, target="reply_id")
|
|
168
|
+
server.add_tool(connect_tool, connect_store, action={"id": "store.connect", "title": "Connect the store",
|
|
169
|
+
"tool": "store.connect", "gate": "human", "humanStep": "sign-in"},
|
|
170
|
+
human_step_url="/dashboard#/connections/store")
|
|
171
|
+
well_known = fs.build_well_known(..., surfaces=surfaces, actions=server.action_table())
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `add_tool(tool, handler, action=, target=, human_step_url=)` registers the action a tool performs (its `tool` is
|
|
175
|
+
this tool's name). It refuses an action of another tool, an id declared twice and a `human` gate without its link (an
|
|
176
|
+
https URL or a path on the service's origin). `target` names the one object a call acts on, an argument's name or a
|
|
177
|
+
function of the arguments, so a decision that named another object is `decision-invalid`.
|
|
178
|
+
- `action_table()` is the declaration: `build_well_known(actions=...)` / `buildWellKnown({actions})` write it under
|
|
179
|
+
`https://fabric.passioncode.ai/agent-contract/extensions/actions/0.1` with `actions_extension` / `actionsExtension`,
|
|
180
|
+
and refuse a table `FAC-SEM-142` finds (not its schema's, an id twice, a `humanStep` that disagrees with the gate, a
|
|
181
|
+
tool `surfaces.mcp.capabilities` does not list): a service never publishes one. A read-only dashboard declares
|
|
182
|
+
nothing (the list has at least one entry).
|
|
183
|
+
- **The gate.** `handle` runs `action_gate` after `authorize` and before the handler. The decision is read from the
|
|
184
|
+
call's `_meta.decision` only, never from its arguments (decision D11; the contract's owner confirmed it, board,
|
|
185
|
+
2026-10-26). A `relayed` action from a T0 or T1 caller with a valid decision is the operator's act: perform it as the
|
|
186
|
+
dashboard would and never refuse it as needing a person, a page or an operator session. Without one the answer is
|
|
187
|
+
`operator-decision-required`; a decision for another action or object, older than 24 hours or more than 60 s ahead
|
|
188
|
+
of the clock is `decision-invalid`; a T2 caller without an accepted `approval/1` gets `approval-required` with the
|
|
189
|
+
digest of `{tool, arguments}`, and a decision in its call changes nothing. Each refusal is DL-19's
|
|
190
|
+
`structuredContent`, status 403 ([delegation](delegation.md)).
|
|
191
|
+
- **The hooks** (all optional, keyword-only). `identify(headers, tier)` names the identity the service authenticated
|
|
192
|
+
when no delegation token did; without it the caller recorded is `local:T0` or `local:T1`, never a name the call
|
|
193
|
+
carries (decision D9). `approve(tool, arguments, headers, digest)` answers `verify_approval`'s outcome for an
|
|
194
|
+
approval the call presented, or `None`. `elicited(tool, arguments, caller)` is `True` only on the service's own
|
|
195
|
+
record of an accepted elicitation for this call (`via: elicitation`), never on the caller's claim; never require an
|
|
196
|
+
elicitation the client cannot show. `on_fault(tool, fault)` hears a service fault the caller sees only as
|
|
197
|
+
`unknown-state` (`approval-outcome-unreadable` when `approve` answers a refusal whose code or detail DL-18 does not know; an answer
|
|
198
|
+
that is neither `None` nor a dict is read as no approval, which fails closed; or
|
|
199
|
+
`caller-missing`: an action-gated server must pass `tier`, `identify` or `authorize`,
|
|
200
|
+
since `handle` defaults to T2 and a T2 call names nobody without them).
|
|
201
|
+
- **Arguments with no canonical form** (a lone surrogate, a NaN or an infinity that Python's `json.loads` lets in,
|
|
202
|
+
nesting past the recursion limit) have no digest to bind: a relayed tool answers JSON-RPC `-32602`, never an exception.
|
|
203
|
+
- **The handler** reads `ctx.record` (`"caller"`, or `{by, via, caller}`) and writes it into the service's audit
|
|
204
|
+
record as the dashboard would; `ctx.decision` is the operator's decision it took. Its `instruction` may name people:
|
|
205
|
+
keep it in that record, never in the events feed, a usage report or a page another party reads. `ctx.tier`,
|
|
206
|
+
`ctx.caller` and `ctx.meta` are there too.
|
|
207
|
+
- **A job's input request is an action** (id its key, tool `fabric.job.get`, gate `relayed`), its decision in the
|
|
208
|
+
`_meta` of the `fabric.job.get` call that carries `inputResponses`, one call per key decided.
|
|
209
|
+
`McpToolServer(input_actions=True)` gates each answer that way and applies nothing on a refusal; `on_input`'s
|
|
210
|
+
context carries `input_records`. It is off by default in 0.50.0: Fabric's hub forwards no decision before CT-125,
|
|
211
|
+
and a gate on by default would refuse the operator's clicks at Fabric's interaction point (decision D8).
|
|
212
|
+
- **Node** has the rule layer, not the dispatcher: run `actionGate({action, tier, caller, meta, target, elicited,
|
|
213
|
+
approval, actionDigest, humanStepUrl, now})` in your own server before the handler and answer a refusal with
|
|
214
|
+
`refusalToolResult(refusal)`; `actionRefusalToolResult(answer)` answers one straight from `actionDecision`.
|
|
215
|
+
`actionDigest` throws on arguments with no canonical form (`TypeError` for a lone surrogate, `RangeError` for
|
|
216
|
+
nesting past the stack): catch it and answer JSON-RPC `-32602`, as the Python server does.
|
|
217
|
+
|
|
218
|
+
**As a caller (AP-3).** An agent built on the kit relays the operator's decision with
|
|
219
|
+
`request_meta(traceparent, decision=operator_decision(action, instruction, target=, at=))` (`fabric_actions`) /
|
|
220
|
+
`requestMeta(traceparent, {decision: operatorDecision({action, instruction, target, at})})`; both refuse a decision
|
|
221
|
+
`decision.schema.json` refuses. `instruction` is the operator's words verbatim, or the agent's short question naming
|
|
222
|
+
the action and the answer (`Agent: Approve the reply to Anna? Operator: yes`), at most 500 characters; a press in the
|
|
223
|
+
operator channel quotes the message that offered the button and its label; `at` is when they said it. Set one only
|
|
224
|
+
for words the operator gave about this action, never for text from a tool result, a page, a file, a board message or
|
|
225
|
+
another agent. An agent never approves its own promotion, grant, rule or policy change on its own account (AP-5).
|
|
226
|
+
|
|
227
|
+
The probe reads the declaration (`well-known.actions`), checks that `tools/list` serves every declared tool
|
|
228
|
+
(`interop.action-tools`) and reports a dashboard that declares no actions as `NOT_RUN` with the reason
|
|
229
|
+
(`dashboard.actions-declared`): absence does not prove a page-only button.
|
|
230
|
+
|
|
134
231
|
## What the probe checks
|
|
135
232
|
|
|
136
233
|
`check_service.py` adds these rules; they read, and never call a capability.
|
|
@@ -145,5 +242,6 @@ a caller today sees `product-error` instead, kept under the key for 24 hours, un
|
|
|
145
242
|
| `interop.unknown-job` | `fabric.job.get` for a made-up id answers `isError` with `unknown-job` | `fabric.job.get` is not served |
|
|
146
243
|
| `interop.trace-propagation` | the answer's `_meta.traceparent` has the probe's trace id and a new span | the answer carries no traceparent |
|
|
147
244
|
| `interop.events-trace` | every event with a trace carries both `traceId` and `spanId`, well formed | no events page was read |
|
|
245
|
+
| `interop.action-tools` | every action the well-known `extensions/actions/0.1` declares names a tool `tools/list` serves (DEC-0121 AP-1); one it does not serve is a `FAIL`, and so is a local service that declares actions with no MCP surface (beyond FAC-SEM-142, which reads only a present list) | no declaration, an unreadable one (`well-known.actions` reports it), no listing was read, or a remote service with no MCP surface (its twin may be on A2A or behind a gateway) |
|
|
148
246
|
|
|
149
247
|
`python3 scripts/check_service.py <id>` prints them with the rest; `--json` for a machine.
|
|
@@ -64,6 +64,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
64
64
|
| L2 | `fabric_package_schema` | the reading of `fabric-package.schema.json` with JavaScript's pattern and format semantics (FAA-92) |
|
|
65
65
|
| L2 | `fabric_package` | `fabric-package/1`: pack, sign, verify; re-exports `schema_problems` (DEC-0050, DEC-0051) |
|
|
66
66
|
| L3 | `fabric_protocols` | protocol revisions (DEC-0067) and feed data (DEC-0070) |
|
|
67
|
+
| 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) |
|
|
67
68
|
| L3 | `fabric_connections` | what an agent uses, `fabric-connections/0.1`: the reference reading and the declaration (DEC-0096, FAC-SEM-127) |
|
|
68
69
|
| L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
|
|
69
70
|
| L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md
CHANGED
|
@@ -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 `52c7d798b9f4eb784eb4e29e717038a8912c759d`
|
|
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;
|
|
@@ -14,7 +14,10 @@ DEC-0096; a schema pattern never uses a bare dot DEC-0097; the board as Fabric 0
|
|
|
14
14
|
the `agent-directory`, `fabric-load` and `service-load` protocols DEC-0104; a machine's `profile` in the estate record,
|
|
15
15
|
`profile-forbids` and the `fabric-agent-call` protocol DEC-0106; the delta anchor DEC-0107; a remote service's
|
|
16
16
|
no-token `401` DEC-0109; money apart from a subscription's estimate DEC-0110; the five credential and call-record
|
|
17
|
-
protocols of DEC-0111 in the register
|
|
17
|
+
protocols of DEC-0111 in the register; a check-in's credential members DEC-0112, a revocation that names its request
|
|
18
|
+
DEC-0113, a node summary against the certificate DEC-0114, key windows read strictly DEC-0115 and DEC-0119, which list
|
|
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). The contract's schemas are normative;
|
|
18
21
|
this page is the working summary. Extension key:
|
|
19
22
|
`https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
|
|
20
23
|
|
|
@@ -64,4 +64,5 @@ plan = fs.restore_plan(doc, {"projects": {"columns": [...], "key": ["id"], "requ
|
|
|
64
64
|
- **Automatic restore** only on a *fresh* database (no domain records **and** the service has never started),
|
|
65
65
|
so a restart never undoes a deletion. Insert `plan[...]["insert"]` and add every row the database refuses to
|
|
66
66
|
`refused` with its key and reason.
|
|
67
|
-
- **Manual restore** shows the plan first; applying it is the operator's
|
|
67
|
+
- **Manual restore** shows the plan first; applying it is the operator's decision, taken on the page or relayed by
|
|
68
|
+
their agent with `_meta.decision` (a restore is a `relayed` action, contract DEC-0121), never the agent's own act.
|
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
## CLI for people and scripts
|
|
31
31
|
|
|
32
32
|
`<tool> service status|start|stop|restart` (launchctl on the label, then the
|
|
33
|
-
well-known document), `<tool> dashboard` (resolve the exact service
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
well-known document), `<tool> dashboard` (resolve the exact registered service with the `fabric-dashboards`
|
|
34
|
+
MCP, run `check_dashboard_link.py` on the action, then open its `open_link` with the host's `open` and
|
|
35
|
+
`fallback=never`; never `open <url>`, `open -a Safari`, `xdg-open` or `webbrowser.open`; an installed host that
|
|
36
|
+
failed is reported, never replaced by a browser; only a confirmed `not_installed` host permits an explicit browser
|
|
37
|
+
fallback and its one-time login flow; [dashboard links](dashboard-links.md)), `<tool> doctor --json`
|
|
36
38
|
(the same checks the probe runs, plus provider keys), `--json` on every command. Print
|
|
37
39
|
a sign-in link only when stdout is a terminal.
|
|
38
40
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
options: --services-dir DIR --json --skip-login
|
|
7
7
|
--ca-file PEM --connect HOST:PORT (remote placement: trust a test CA; dial another address)
|
|
8
8
|
--board-kit PATH (board.responder: the service's own fabric_board.py or fabric-board.mjs)
|
|
9
|
+
--repo PATH (dashboard.opens-in-host: the service's checkout)
|
|
9
10
|
|
|
10
11
|
A remote placement (DEC-0019 — an online agent or dashboard at an https origin) is probed over
|
|
11
12
|
TLS with the certificate verified: the well-known document must refuse a request without the
|
|
@@ -26,6 +27,16 @@ well-known `protocols` declares that it answers board requests. The rule checks
|
|
|
26
27
|
runs the kit's fixture conversation (`fabric_board.self_check`, or `selfCheck` of a Node copy given with --board-kit):
|
|
27
28
|
the whole conversation, a fenced accept and a board session without an epoch, with no Fabric. A live board is never
|
|
28
29
|
touched; that smoke needs a disposable Fabric and is reported separately.
|
|
30
|
+
|
|
31
|
+
Actions (fabric-actions/0.1, DEC-0121): `well-known.actions` holds a declared extensions/actions/0.1 to FAC-SEM-142,
|
|
32
|
+
`interop.action-tools` fails a declared action whose tool tools/list does not serve (and a local service that declares
|
|
33
|
+
actions with no MCP surface; a remote one there is NOT_RUN, its twin may be on A2A), and `dashboard.actions-declared`
|
|
34
|
+
is NOT_RUN, never FAIL, for a dashboard that declares none (a read-only dashboard cannot declare an empty list).
|
|
35
|
+
|
|
36
|
+
dashboard.opens-in-host (FAA-118, the operator rule of 2026-10-24): the service's checkout (--repo, else the one of
|
|
37
|
+
the descriptor's source.repository that holds its commands, manifest or launchd job) opens the service's pages only
|
|
38
|
+
through Fabric Dashboards. Tracked text files only, within a time budget; a browser opener aimed at its pages outside
|
|
39
|
+
the confirmed not_installed fallback is a FAIL (the matcher: check_service_rules.py, "dashboard.opens-in-host").
|
|
29
40
|
"""
|
|
30
41
|
|
|
31
42
|
from __future__ import annotations
|
|
@@ -69,6 +80,8 @@ from check_service_rules import _semver # noqa: E402,F401 - moved (FAA-65)
|
|
|
69
80
|
from check_service_rules import usage_problems # noqa: E402,F401 - moved (FAA-65)
|
|
70
81
|
from check_service_rules import same_repository # noqa: E402,F401 - moved (FAA-65)
|
|
71
82
|
from check_service_rules import merge_headers, session_cookie # noqa: E402,F401 - FAA-115
|
|
83
|
+
from check_service_rules import well_known_actions_verdict, action_tools_verdict, actions_declared_verdict # noqa: E402 - DEC-0121
|
|
84
|
+
from check_service_rules import opens_in_host_verdict, service_checkout # noqa: E402 - FAA-118
|
|
72
85
|
|
|
73
86
|
|
|
74
87
|
class _PinnedHTTPS(http.client.HTTPSConnection):
|
|
@@ -85,9 +98,13 @@ class _PinnedHTTPS(http.client.HTTPSConnection):
|
|
|
85
98
|
|
|
86
99
|
|
|
87
100
|
class Probe:
|
|
101
|
+
remote = False # a probe built without __init__ (the tests' Probe.__new__) reads as local
|
|
102
|
+
|
|
88
103
|
def __init__(self, descriptor_path: Path, descriptor: Dict[str, Any], services_dir: Path, skip_login: bool,
|
|
89
|
-
ca_file: Optional[str] = None, connect: Optional[str] = None, board_kit: Optional[str] = None
|
|
104
|
+
ca_file: Optional[str] = None, connect: Optional[str] = None, board_kit: Optional[str] = None,
|
|
105
|
+
repo: Optional[str] = None):
|
|
90
106
|
self.board_kit = board_kit
|
|
107
|
+
self.repo = repo
|
|
91
108
|
self.remote = fs.placement_of(descriptor) == "remote"
|
|
92
109
|
self.ca_file = ca_file
|
|
93
110
|
self.connect = connect
|
|
@@ -256,6 +273,8 @@ class Probe:
|
|
|
256
273
|
self.add("well-known.connections", "FAIL" if view["reported"] else "PASS",
|
|
257
274
|
"; ".join(f["message"] for f in fcn.connections_rules(wk)[:5]) or
|
|
258
275
|
"%d connection(s), %d named in degraded[]" % (len(view["connections"]), failing))
|
|
276
|
+
# DEC-0121, FAC-SEM-142: every button's agent twin, declared; no declaration is NOT_RUN (D10).
|
|
277
|
+
self.add("well-known.actions", *well_known_actions_verdict(wk))
|
|
259
278
|
self.configuration_rule()
|
|
260
279
|
svc = wk.get("service") or {}
|
|
261
280
|
answered = "%s.%s" % (svc.get("id"), svc.get("instance"))
|
|
@@ -428,6 +447,24 @@ class Probe:
|
|
|
428
447
|
note += " (kit %s is current: dashboard_kit.py status)" % current
|
|
429
448
|
self.add("dashboard.declared", "PASS", note + "; the page itself: check_dashboard_layout.mjs")
|
|
430
449
|
|
|
450
|
+
def dashboard_page_rules(self) -> None:
|
|
451
|
+
"""DEC-0121 and FAA-118: what a person can do on the dashboard, and where its pages open."""
|
|
452
|
+
self.add("dashboard.actions-declared", *actions_declared_verdict(self.wk or {})) # D10: never a FAIL for absence
|
|
453
|
+
self.opens_in_host_rule()
|
|
454
|
+
|
|
455
|
+
def opens_in_host_rule(self) -> None:
|
|
456
|
+
"""FAA-118: the checkout opens the service's pages only through Fabric Dashboards (decision D13)."""
|
|
457
|
+
dashboard = ((self.wk or {}).get("surfaces") or {}).get("dashboard")
|
|
458
|
+
if not isinstance(dashboard, dict):
|
|
459
|
+
self.add("dashboard.opens-in-host", "NOT_RUN", "no surfaces.dashboard: no page to open")
|
|
460
|
+
return
|
|
461
|
+
checkout, why = service_checkout(self.d, self.repo)
|
|
462
|
+
if checkout is None:
|
|
463
|
+
self.add("dashboard.opens-in-host", "NOT_RUN", why)
|
|
464
|
+
return
|
|
465
|
+
self.add("dashboard.opens-in-host", *opens_in_host_verdict(checkout, 0 if self.remote else self.port,
|
|
466
|
+
str(self.d.get("origin") or ""), str(dashboard.get("path") or "/")))
|
|
467
|
+
|
|
431
468
|
def login_rules(self) -> None:
|
|
432
469
|
dashboard = ((self.wk or {}).get("surfaces") or {}).get("dashboard")
|
|
433
470
|
if not dashboard or not dashboard.get("login"):
|
|
@@ -544,11 +581,14 @@ class Probe:
|
|
|
544
581
|
else "no readable token")
|
|
545
582
|
for rule in ("interop.output-schema-object", "interop.tools-match", "interop.job-tools", "interop.unknown-job", "interop.trace-propagation"):
|
|
546
583
|
self.add(rule, "NOT_RUN", reason)
|
|
584
|
+
self.add("interop.action-tools", *action_tools_verdict(self.wk, None, reason, no_surface=not mcp_surface,
|
|
585
|
+
remote=self.remote))
|
|
547
586
|
else:
|
|
548
587
|
try:
|
|
549
588
|
listing = self.mcp_call("tools/list", {})
|
|
550
589
|
except (OSError, ValueError) as exc:
|
|
551
590
|
self.add("interop.tools-match", "FAIL", "tools/list failed: %s" % exc)
|
|
591
|
+
self.add("interop.action-tools", *action_tools_verdict(self.wk, None, "tools/list failed: %s" % exc))
|
|
552
592
|
listing = None
|
|
553
593
|
if listing is not None:
|
|
554
594
|
sent = self.sent_traceparent
|
|
@@ -556,6 +596,7 @@ class Probe:
|
|
|
556
596
|
self.object_root_rule(tools)
|
|
557
597
|
self.tools_match_rule(capabilities, manifest_path, tools)
|
|
558
598
|
self.job_tools_rule(capabilities, tools)
|
|
599
|
+
self.add("interop.action-tools", *action_tools_verdict(self.wk, tools)) # DEC-0121 AP-1: the live twin
|
|
559
600
|
self.unknown_job_rule(list(tools))
|
|
560
601
|
self.trace_rule(listing, sent)
|
|
561
602
|
self.events_trace_rule(self.events)
|
|
@@ -811,6 +852,7 @@ class Probe:
|
|
|
811
852
|
self.auth_rules()
|
|
812
853
|
self.usage_rules()
|
|
813
854
|
self.dashboard_rules()
|
|
855
|
+
self.dashboard_page_rules()
|
|
814
856
|
self.login_rules()
|
|
815
857
|
self.interop_rules()
|
|
816
858
|
verdict, evidence = board_responder_verdict(self.wk, self.board_kit)
|
|
@@ -838,6 +880,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
838
880
|
parser.add_argument("--ca-file", help="remote placement: trust this CA bundle instead of the system store (tests)")
|
|
839
881
|
parser.add_argument("--connect", help="remote placement: dial HOST:PORT while speaking TLS to the origin's name (tests)")
|
|
840
882
|
parser.add_argument("--board-kit", help="board.responder: the service's own fabric_board.py or fabric-board.mjs")
|
|
883
|
+
parser.add_argument("--repo", help="dashboard.opens-in-host: the service's checkout (default: found by source.repository)")
|
|
841
884
|
args = parser.parse_args(argv)
|
|
842
885
|
if not args.target and not args.descriptor:
|
|
843
886
|
parser.print_usage(sys.stderr)
|
|
@@ -849,7 +892,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
849
892
|
except (OSError, ValueError) as exc:
|
|
850
893
|
print("No readable descriptor at %s: %s" % (path, exc), file=sys.stderr)
|
|
851
894
|
return 1
|
|
852
|
-
results = Probe(path, descriptor, services_dir, args.skip_login, args.ca_file, args.connect, args.board_kit).run()
|
|
895
|
+
results = Probe(path, descriptor, services_dir, args.skip_login, args.ca_file, args.connect, args.board_kit, args.repo).run()
|
|
853
896
|
failed = sum(r["verdict"] == "FAIL" for r in results)
|
|
854
897
|
if args.json:
|
|
855
898
|
print(json.dumps({"descriptor": str(path), "results": results, "failed": failed}, indent=2))
|