@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/README.md +13 -2
- package/SECURITY.md +23 -8
- package/bin/fabric-agent-adapter.js +50 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
- 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-secrets.mjs +10 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
- 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_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
|
@@ -125,6 +125,7 @@ action it also carries the operator's decision, [Actions](#actions)), act on eve
|
|
|
125
125
|
(`jobs.quotaExhausted(id, message, {retryable, resetsAt})`, `jobCallerAction`, `intentIdempotencyKey`). The guidance
|
|
126
126
|
for an agent's own design is in `creating-fabric-agents/references/resilience-and-autonomy.md`.
|
|
127
127
|
|
|
128
|
+
<a id="a-nodes-call-queue"></a>
|
|
128
129
|
**A node's call queue (DEC-0104).** A provider that queues refuses a start past its queue with `queue-full`: an `isError`
|
|
129
130
|
result with one text item whose JSON is `{error: {code: "queue-full", message, data: {retryable, retryAfterSeconds}}}`,
|
|
130
131
|
never `structuredContent`; the caller starts again after `retryAfterSeconds` under the same key. Read that refusal
|
|
@@ -132,9 +133,58 @@ from the start's own `isError` result, before any answer reaches `job_caller_act
|
|
|
132
133
|
job, and `queue-full` never ends one, so they answer a job it ended `invalid` (`QUEUE_FULL` names the code, and the
|
|
133
134
|
kit's job store refuses to end a job with it). The kit has no reader for the refusal yet (FAA-103). Through Fabric's hub
|
|
134
135
|
a caller today sees `product-error` instead, kept under the key for 24 hours, until the hub relays `queue-full`
|
|
135
|
-
(Fabric CO-376). The kit does not queue: it declares no load extension (`extensions/load/0.1`)
|
|
136
|
-
`fabric.job.list`, so its `unknown-job` and repeated keys keep the rules above.
|
|
137
|
-
|
|
136
|
+
(Fabric CO-376). The kit's job store does not queue by itself: it declares no load extension (`extensions/load/0.1`)
|
|
137
|
+
and serves no `fabric.job.list`, so its `unknown-job` and repeated keys keep the rules above.
|
|
138
|
+
|
|
139
|
+
A node that queues takes the contract's reference functions from the kit instead of copying them (since 0.80.0,
|
|
140
|
+
FAA-289): `fabric_load.py` and `fabric-load.mjs`, exported by both facades, held to the contract's
|
|
141
|
+
`fixtures/vectors/agent-network.json` and its `job-queue`, `service-load` and `service-well-known-load` fixtures at the
|
|
142
|
+
pin (vendored as `test/fixtures/contract/load-vectors.json` by `make_load_vectors.mjs`):
|
|
143
|
+
|
|
144
|
+
| Python | Node | What |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `job_caller_key(accepted, credential)` | `jobCallerKey` | the caller's opaque key: an accepted delegation's `principal`, `caller` and a partner organization ending the chain, else the credential; `None` / `undefined` for a refused or unreadable call |
|
|
147
|
+
| `next_queued_job(queue, running, limits)` | `nextQueuedJob` | the id to start next per (agentRef, context): nothing at `concurrency`, a caller at `perCaller` waits, the caller running fewest first, then the oldest arrival, ties by id |
|
|
148
|
+
| `admit_job(queue, running, job, limits, retry_after_seconds)` | `admitJob` | `{admit: True}` when the job would start at once or fewer than `queueMax` wait, else the `queue-full` error with `retryAfterSeconds` (1 to 3600, else 60) |
|
|
149
|
+
| `queue_status_message(position, length)` | `queueStatusMessage` | a waiting job's `statusMessage`, `queued <position> of <length>` |
|
|
150
|
+
| `read_queue_position(status_message)` | `readQueuePosition` | `{position, length}` from that message, or none |
|
|
151
|
+
| `job_queue_rules(value)` | `jobQueueRules` | FAC-SEM-131 on `{queue, running, limits, started}` |
|
|
152
|
+
| `load_report_rules(report)` | `loadReportRules` | FAC-SEM-132 on a `service-load/1` report: windows one `windowSeconds` apart, none after `generatedAt`, one row per context, p50 at most p95, a run time only where a job ended |
|
|
153
|
+
| `load_report_problems(report)` | `loadReportProblems` | the report against `service-load.schema.json` (no caller, no job id, counts and durations only) |
|
|
154
|
+
| `load_view(well_known)`, `load_extension_rules(well_known)` | `loadView`, `loadExtensionRules` | the declaration as a reader reads it, and FAC-SEM-133: an invalid one is treated as not declared and reported |
|
|
155
|
+
| `load_extension(path, window_seconds, limits)` | `loadExtension({path, windowSeconds, limits})` | the `extensions` entry a service declares; raises `ServiceError` on one a reader would hide |
|
|
156
|
+
|
|
157
|
+
Queue times are UTC instants (`T`, `Z`, at most three fraction digits); report times are RFC 3339 read strictly. A
|
|
158
|
+
node keys every per-caller rule with `job_caller_key`, counts every started job that has not ended (`input_required`
|
|
159
|
+
and paused ones too) as running, and answers another caller's job id `unknown-job`. The text-only `queue-full` result
|
|
160
|
+
and `fabric.job.list` are still the node's own (FAA-103).
|
|
161
|
+
|
|
162
|
+
### Work held by the service's own limits
|
|
163
|
+
|
|
164
|
+
A service that holds work by a limit of its own (a daily quota, a per-caller share, a concurrency cap, a money cap)
|
|
165
|
+
says so, so a hold is never seen only by a caller who waits (FAA-298). `fabric_holds` (Node `fabric-holds.mjs`) does
|
|
166
|
+
the three parts:
|
|
167
|
+
|
|
168
|
+
- **On the job.** `hold_job(jobs, job_id, reason, since, until, message)` (Node `holdJob`) pauses the job with the
|
|
169
|
+
statusMessage `waiting_message` writes: `quota-exhausted[ until <until>]: waiting <reason> since <since>[: <message>]`,
|
|
170
|
+
`reason` one of `daily-quota`, `caller-share`, `concurrency`, `money`. The pinned `interop-job` schema is closed
|
|
171
|
+
(`additionalProperties: false`), so `waiting {reason, since, until, message}` is carried in `statusMessage`, the
|
|
172
|
+
contract's pause grammar, never as a field of the job: every caller already reads it as wait (until `until` when
|
|
173
|
+
given), and `read_waiting(answer)` (Node `readWaiting`) gives the four parts back. A job waiting its normal turn in
|
|
174
|
+
the call queue keeps `queued <position> of <length>`; a hold is the service's own limit.
|
|
175
|
+
- **One push per hold.** `HoldBook(path, events=log)` (Node `new HoldBook(file, {events})`) opens a hold per reason and
|
|
176
|
+
caller: `hold(caller=, work=, reason=, until=, job_id=)` returns `{hold, statusMessage, push}`, and only the call that
|
|
177
|
+
opens the hold pushes: one `limit.held` event, level `notice`, `notify: true`, naming the caller, the work, the reason
|
|
178
|
+
and when it frees by itself, and asking whether to raise the limit. More work held by the same limit for the same
|
|
179
|
+
caller joins the hold silently. A hold closes by `release`, when its `until` passes, or by a lift that covers it;
|
|
180
|
+
the next hold pushes again. Names that read as a credential are refused.
|
|
181
|
+
- **The operator's answer as a scoped lift.** `lift(reason=, decision=, caller=, job_id=, jobs= | usd= | limit=,
|
|
182
|
+
until=)` records the operator's words (`fabric_actions.operator_decision("limit.lift", words)`) with its scope (this
|
|
183
|
+
order, one caller, or every caller) and its size: a number of jobs, an amount of money, or a raised limit for a window
|
|
184
|
+
(100 a day for today: `limit=100, until=<midnight>`), and writes `limit.lifted`. `admit(reason=, caller=, job_id=,
|
|
185
|
+
cost_usd=)` spends a jobs or money lift (the narrowest scope first) and answers whether the work may pass the limit;
|
|
186
|
+
pass its answer to `jobs.create(..., limit_holds=not admitted)` for a repeated key. `limit(reason=, base=, caller=)`
|
|
187
|
+
is the limit in force. A lift falls back by itself when its window ends or its amount is spent.
|
|
138
188
|
|
|
139
189
|
<a id="actions"></a>
|
|
140
190
|
|
|
@@ -389,5 +439,6 @@ your own record, keep it (OA-6).
|
|
|
389
439
|
| `interop.events-trace` | every event with a trace carries both `traceId` and `spanId`, well formed | no events page was read |
|
|
390
440
|
| `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) |
|
|
391
441
|
| `channel-agent.declaration` | a channel agent's declaration (DEC-0150) passes the contract's FAC-SEM-162 to 165 at the pin, read from `provider.extensions` of the manifest the descriptor names, else from `extensions` of the well-known answer (CA-1); a `FAIL` on the obligations of a declaration the reader still reads (FAC-SEM-164) or a string shaped like a credential (FAC-SEM-165, not a declaration too large to read for credentials) in the read declaration or in any copy beside it, the first five findings shown as code and path, never a declared value | nothing declared, the named manifest unreadable (`interop.manifest-link` fails it), or a key read as not declared (a manifest's own `extensions`, a capability's, another version's key, a well-known copy beside a manifest, a form the schema or FAC-SEM-163 refuses), each named in the evidence, never a failure while producers move to this revision unless a copy holds a string shaped like a credential (that fails in any copy the probe reads: a manifest's own `extensions`, a capability's, the well-known copy, CA-5); no line when the well-known answer is not read; credentials are read as the contract reads them, within its reading budget and under the `0.1` key only (a key of another version is named, not read); a well-known answer carrying the declaration at 75 % of the 2 MiB cap or over is named, never a failure (until the contract's CT-223) |
|
|
442
|
+
| `limits.holds` | a service that declares limits (a load declaration's `limits`, the usage report's `budget` or `budgets`) holds work through `fabric_holds` (`HoldBook`, `waiting_message`, `hold_job`) in a tracked code file (`.py`, `.mjs`, `.js`, `.ts`) of its checkout outside the kit's copy, a vendored `check_service_rules.py` and the tests (FAA-298) | no limits declared, or no checkout found (`--repo` names it), git missing or out of time; and, since 0.81.0, a service that declares limits and holds none through the kit: a migration note naming `fabric_holds`, never a `FAIL`, since services built before the module could not have adopted it |
|
|
392
443
|
|
|
393
444
|
`python3 scripts/check_service.py <id>` prints them with the rest; `--json` for a machine.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -59,7 +59,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
59
59
|
| L0 | `fabric_time` | strict RFC 3339, UTC days, formatting, day windows, the clock ([dates](#dates-and-times)) |
|
|
60
60
|
| L0 | `fabric_identifiers` | identifier equality by normal form (DEC-0076) |
|
|
61
61
|
| L0 | `fabric_secrets` | the contract's one credential detector (DEC-0044) |
|
|
62
|
-
| L0 | `fabric_peer_owner` | which OS account owns each end of a loopback connection, from the computer's socket table (DEC-0154 HA-2, FAA-226): `listener`, `server_end`, `peer`, `CheckedHTTPConnection`; the Python file is Project Observatory's `peer_owner.py` 1.1.0
|
|
62
|
+
| L0 | `fabric_peer_owner` | which OS account owns each end of a loopback connection, from the computer's socket table (DEC-0154 HA-2, FAA-226): `listener`, `server_end`, `peer`, `CheckedHTTPConnection`; the Python file is Project Observatory's `peer_owner.py`, vendored from commit `f86f0622` (1.1.0) and read against `2180235c` (1.2.0, whose Linux rule it carries), with the readings its header names (`1.2.0+kit.5`; Observatory's 1.3.x libproc proof is FAA-292), the Node twin reads netstat, `/proc` and the owner SID where Python reads `sysctl`, `/proc` and `GetExtendedTcpTable`; on a restricted macOS table (FAA-267) a caller may vouch with a pid it started, proved by `lsof -p` |
|
|
63
63
|
| L0 | `fabric_tailnet` | who calls an agent behind `tailscale serve` and what that person may open (FAA-297): `Tailscale-User-Login` only on the agent's own serve listener from a loopback peer, groups from a generated `tailnet-perimeter/1` file, a personal SHA-256-stored key for MCP no wider than the groups; `authorize`, `load_perimeter`, `load_keys`, `hash_key`, refusals 401/403 with a code ([surfaces-and-auth.md](surfaces-and-auth.md#behind-tailscale-serve)) |
|
|
64
64
|
| L1 | `fabric_crypto` | Ed25519 signing (a Montgomery ladder that does not branch on the key) and verification, P-256 verification, base64url, sha256 |
|
|
65
65
|
| L1 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
@@ -78,10 +78,11 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
78
78
|
| L3 | `fabric_connections` | what an agent uses, `fabric-connections/0.1`: the reference reading and the declaration (DEC-0096, FAC-SEM-127) |
|
|
79
79
|
| 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) |
|
|
80
80
|
| 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 |
|
|
81
|
+
| L3 | `fabric_load` | the call queue and the load report of DEC-0104 (FAA-289): `job_caller_key`, `next_queued_job`, `admit_job`, `queue_status_message`, `read_queue_position`, FAC-SEM-131 to 133, the `service-load/1` and `extensions/load/0.1` shapes, `load_extension`; imports `fabric_connections` for ajv's `date-time` ([interop](interop.md#a-nodes-call-queue)) |
|
|
81
82
|
| L3 | `fabric_lock` | the instance lock and the supervisor marker (LC-01, DEC-0057) |
|
|
82
83
|
| L3 | `fabric_usage` | usage receipts, the ledger, the usage report, attribution, spending limits ([usage](usage.md)) |
|
|
83
84
|
| L3 | `fabric_backups` | settings backups (DEC-0025) |
|
|
84
|
-
| L3 | `fabric_descriptor` | the descriptor, peers, app descriptors, the well-known document |
|
|
85
|
+
| L3 | `fabric_descriptor` | the descriptor, peers, app descriptors, the well-known document, the loopback HTTP server that refuses another OS account's peer before reading it (`LoopbackHTTPServer`, Node `loopbackServer`; FAA-227) |
|
|
85
86
|
| 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)) |
|
|
86
87
|
| L3 | `fabric_auth` | the token, the network guard, login codes |
|
|
87
88
|
| L3 | `fabric_events` | events, paging, the JSONL event log (DEC-0029) |
|
|
@@ -91,6 +92,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
91
92
|
| L3 | `fabric_profiles` | runner and model profiles ([profiles](profiles.md)) |
|
|
92
93
|
| L3 | `fabric_interop_rules` | `fabric-interop/0.1` rules without a service: constants, `InteropError`, trace context, job limits (DEC-0087; FAA-92), a job's approval step under the mandate (`job_step`, C3.3) |
|
|
93
94
|
| L3 | `fabric_interop` | `fabric-interop/0.1`: jobs, envelopes, MCP tools; re-exports the rule layer ([interop](interop.md)) |
|
|
95
|
+
| L3 | `fabric_holds` | work a service holds by its own limits (FAA-298): the held job's `statusMessage` in the pause grammar (`waiting_message`, `read_waiting`, `hold_job`), one push per hold and the operator's scoped lift (`HoldBook`: `hold`, `release`, `lift`, `admit`, `limit`) ([interop](interop.md#work-held-by-the-services-own-limits)) |
|
|
94
96
|
| L4 | `fabric_hub` | the Fabric hub's door (`fabric-hub/0.1`) |
|
|
95
97
|
| L4 | `fabric_board_shapes` | the project board's wire: constants, refusal codes, shapes |
|
|
96
98
|
| L4 | `fabric_board_link` | the board's link: the MCP session over the hub, the fsynced checkpoint, a claimed request as data, and the operator's mandate a request carries (FAA-135, imports `fabric_mandate` and `fabric_interop_rules` for the FAA-139 reading) |
|
|
@@ -105,7 +107,8 @@ Beside the kit, not in `kit.json`: the probe (`check_service.py`, `check_service
|
|
|
105
107
|
`check_service_channel_agent.py`, `check_service_channel_agent_schema.py` and the
|
|
106
108
|
`service-channel-agent-extension.schema.json` it reads at run time), the sample
|
|
107
109
|
(`sample_service.py`, `sample-remote-service.mjs`), `fabric_kit.py` itself and the dashboard tools. Recipes built on
|
|
108
|
-
the kit live in `../recipes/` ([the draft-and-check step of a support agent](../recipes/support-agent/README.md)
|
|
110
|
+
the kit live in `../recipes/` ([the draft-and-check step of a support agent](../recipes/support-agent/README.md), [a service that is only a
|
|
111
|
+
dashboard](../recipes/dashboard-only/README.md), [a key-delivery door](../recipes/key-delivery-door/README.md)).
|
|
109
112
|
|
|
110
113
|
## What 0.39 code keeps
|
|
111
114
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -45,7 +45,8 @@ Starting, stopping and restarting go through the supervisor and nothing else:
|
|
|
45
45
|
atomic, mode 0600) and the uninstaller removes it; a running service never writes its own, except an app
|
|
46
46
|
([below](#an-apps-own-descriptor-and-the-node)). It names the supervisor and its file (`label` and `plist`, `unit`,
|
|
47
47
|
or `task`; none for `node` and `person`), the `origin` `http://127.0.0.1:<port>` (a port is a claim: an installer
|
|
48
|
-
refuses a port another instance declares
|
|
48
|
+
refuses a port another instance declares, and one another OS account listens on, read with the kit's
|
|
49
|
+
`fabric_peer_owner`, FAA-225; the recorded port per account and its claim file wait on contract DEC-0160), the service's directories, and commands as argument arrays that never
|
|
49
50
|
carry a credential. Only `doctor` and `update` are commands a host may run.
|
|
50
51
|
|
|
51
52
|
**Started is not ready.** Readiness is the well-known document, `GET /.well-known/fabric-service`: answered without
|
|
@@ -144,27 +145,34 @@ it at boot, before any login, as the account its `UserName` names.
|
|
|
144
145
|
install requires (without it the job would run as root, which every client refuses); the file goes in
|
|
145
146
|
`/Library/LaunchDaemons/<label>.plist`. A plist there is in the system domain by its path (the folder's case is
|
|
146
147
|
ignored); `domain="system"` (Node: `domain: 'system'`) says so for any path, and an explicit domain wins over the
|
|
147
|
-
path
|
|
148
|
+
path, except that an install refuses a pair launchd would contradict: `domain="system"` with a plist in a
|
|
149
|
+
`LaunchAgents` folder (launchd also loads that into `gui/<uid>` at login) and `domain="gui"` with a plist in
|
|
150
|
+
`/Library/LaunchDaemons` (launchd loads it at boot as root). A domain name given as the plist path (`"system"`,
|
|
151
|
+
`"gui"`) is refused, never read as a relative path (FAA-286). Everything else is a LaunchAgent's.
|
|
148
152
|
`UserName` is the account whose Fabric and agents call the service: clients of another account refuse it (HA-2).
|
|
149
153
|
- **Who runs which step.** The release, the descriptor and the service token are written as the `UserName` (steps 1 and
|
|
150
154
|
2 above, for example with `sudo -u <user> -H`), so they live in that user's home with its modes, and the plist's
|
|
151
155
|
working directory and log paths name that home; only the plist, `launchd_install` and `launchd_uninstall` run as root,
|
|
152
156
|
and at uninstall `remove_descriptor` runs as the `UserName` again. A service moved from a LaunchAgent is uninstalled
|
|
153
|
-
as one first, so launchd never holds both
|
|
157
|
+
as one first, so launchd never holds both: the install refuses a plist of the same label left in the `UserName`'s
|
|
158
|
+
`~/Library/LaunchAgents`, and the probe's `lifecycle.one-copy` fails a label loaded in both domains (FAA-286). The descriptor's `lifecycle.plist` names the path exactly,
|
|
154
159
|
`/Library/LaunchDaemons/<label>.plist`: a host that reads the folder by its spelling then knows the domain too.
|
|
155
160
|
- **The label is the machine's.** `/Library/LaunchDaemons` and the system domain are shared by every account of the
|
|
156
161
|
Mac, so a LaunchDaemon's label is unique on the machine, not per account (put the account in it on a Mac with a
|
|
157
162
|
personal and an organization account); an install refuses, before writing anything, a label another plist holds
|
|
158
163
|
(the one launchd loaded the job from, or any plist of that label in the same folder) and the plist at its path when it
|
|
159
164
|
names another `UserName` or another label; an uninstall refuses a label another plist holds before its `bootout`,
|
|
160
|
-
which would stop that plist's job, so pass it the label the plist at its path names
|
|
165
|
+
which would stop that plist's job, so pass it the label the plist at its path names; in either domain an uninstall
|
|
166
|
+
refuses a plist at its path that names another label, before its `bootout` (FAA-286).
|
|
161
167
|
- **Root installs and removes it.** `launchd_install` and `launchd_uninstall` in the system domain refuse, before writing
|
|
162
168
|
anything, in a process that is not root, and name the command to run as root (`launchctl bootstrap system <plist>`,
|
|
163
169
|
`launchctl bootout system/<label>`, removing the plist and `launchctl enable system/<label>`, as `launchd_uninstall`
|
|
164
170
|
does). `purge` is refused: the data belongs to the `UserName`, so remove it as that user. Upgrade is the same install
|
|
165
171
|
again as root, so a descriptor's `commands.update` for a LaunchDaemon runs as root, writes the new release (and any
|
|
166
172
|
descriptor it rewrites) as the `UserName` as an install does, and runs only the plist and `launchd_install` as root; a
|
|
167
|
-
host without root reports it as the operator's.
|
|
173
|
+
host without root reports it as the operator's. Its commands name absolute paths: `~` would be root's home, and the
|
|
174
|
+
probe's `lifecycle.plist` fails a LaunchDaemon command that starts with `~`, as it fails a LaunchDaemon plist without
|
|
175
|
+
`UserName` (FAA-286). A host's verbs need root too and keep LC-14 (off stays off, across a boot as well as a login): stop is
|
|
168
176
|
`bootout` + `disable system/<label>`, start is `enable` + `bootstrap system <plist>`, restart is
|
|
169
177
|
`kickstart -k system/<label>`; a host or broker without root reports the job read-only.
|
|
170
178
|
- **Status and pid read without root**: `launchd_loaded(label, "system")` and `launchd_pid(label, "system")` ask
|
|
@@ -176,23 +184,32 @@ it at boot, before any login, as the account its `UserName` names.
|
|
|
176
184
|
that pid accepted, while the pid runs as the `UserName`'s uid (`ps`) and listens on exactly `127.0.0.1` (`lsof -p`);
|
|
177
185
|
an install refuses a `UserName` that is not a short name, no account of the Mac or root, and an origin other than
|
|
178
186
|
`http://127.0.0.1:<port>`, where nothing could prove it. Where the socket table is restricted it accepts a port that
|
|
179
|
-
pid listens on at exactly `127.0.0.1`, the weaker vouch SECURITY.md names (
|
|
187
|
+
pid listens on at exactly `127.0.0.1`, the weaker vouch SECURITY.md names (DEC-0155 RT-3). A `ProgramArguments` wrapper that
|
|
180
188
|
starts the server as a child cannot pass either check, and the refusal says so: start the server with `exec`. An
|
|
181
189
|
install refused after its bootstrap leaves the job loaded, and its plist starts it again at boot; the error says so,
|
|
182
|
-
and as root `launchctl bootout system/<label>` and removing the plist (or `launchd_uninstall`) take it away.
|
|
190
|
+
and as root `launchctl bootout system/<label>` and removing the plist (or `launchd_uninstall`) take it away. A
|
|
191
|
+
bootstrap that fails all five tries removes the plist the call wrote, and names one that was there before, which
|
|
192
|
+
would load at the next boot (FAA-286). A plist that does not parse is refused as such before anything is written,
|
|
193
|
+
never as a missing `UserName`; the Node installer refuses a binary plist and reads only the root dict's keys. For hosts and the probe the domain comes from the plist's folder alone, so a LaunchDaemon's plist must live
|
|
183
194
|
in `/Library/LaunchDaemons` even when the installer is given `domain="system"`: the installer neither moves nor checks
|
|
184
195
|
it. A descriptor field for the domain is the contract's CT-234, open.
|
|
185
196
|
- **A contract exception, accepted in principle.** At the pin, HA-2 reads a launch daemon's listener as another
|
|
186
|
-
account's and nothing it answers as the service's; the
|
|
197
|
+
account's and nothing it answers as the service's; the pin's RT-3 (DEC-0155) vouches only for the caller's own uid. The
|
|
187
198
|
root installer reads its own job's token-free well-known answer anyway, proved by launchd's pid, that pid's uid and its
|
|
188
199
|
bind on exactly `127.0.0.1` (`lsof -p`, never a wildcard), and sends no credential; under a restricted socket table the
|
|
189
|
-
first proof is the
|
|
200
|
+
first proof is the RT-3 weaker vouch (the pid listens on the port; that it accepted the connection is not shown). The contract owner accepted this in
|
|
190
201
|
principle as DEC-0161 (RT-3 extended to launchd's system domain, the restricted-table vouch included; proposed, its
|
|
191
202
|
text after its readings), so it stays a departure from the pin until the next repin (FAA-288).
|
|
192
203
|
Every other client keeps HA-2 as written.
|
|
193
204
|
- **Hosts.** A host or broker that reads launchd only in `gui/<uid>` sees a LaunchDaemon as not loaded and must not load
|
|
194
205
|
its plist into `gui/<uid>`. Fabric Dashboards from 0.8.39 takes the domain from the plist's folder and offers no verbs
|
|
195
|
-
for a system job; the lifecycle broker is still asked (FAA-287).
|
|
206
|
+
for a system job; the lifecycle broker is still asked (FAA-287). A product with its own launchd reader (a status
|
|
207
|
+
page's `launchd_pid`, an installer of its own in another language) that asks only `gui/<uid>` reports a LaunchDaemon
|
|
208
|
+
as not loaded: move that reader to the plist's domain before the product moves to the system domain (FAA-286).
|
|
209
|
+
- **Off macOS the twins differ, on purpose.** Python's `launchd_domain()` answers `gui/<uid>` on Linux (it refuses only
|
|
210
|
+
Windows), so the kit's launchd tests run on Linux CI against a fake `launchctl`; Node's `launchdDomain()` refuses off
|
|
211
|
+
`darwin` unless a test passes `platform: 'darwin'`. Neither runs launchd off macOS: install with
|
|
212
|
+
`systemd_install` / `task_install` there (FAA-286 item 5, recorded rather than aligned).
|
|
196
213
|
|
|
197
214
|
## One copy under a supervisor
|
|
198
215
|
|
|
@@ -205,6 +222,10 @@ if the holder leaves, and exits 75 only when the wait runs out: a few starts an
|
|
|
205
222
|
360. It touches nothing while it waits. Two installs sharing one data directory are still the
|
|
206
223
|
defect to remove; the back-off only stops the loop from costing the machine.
|
|
207
224
|
|
|
225
|
+
A context refusal (`fabric_context`, exit 78: no record, a revoked grant, a folder of another context) is the same
|
|
226
|
+
loop under `KeepAlive`, so a supervised copy waits the same back-off, holding nothing and with the lock released,
|
|
227
|
+
before it exits 78 (FAA-279); started by hand it exits 78 at once.
|
|
228
|
+
|
|
208
229
|
The marker (contract DEC-0057, CT-01) is the variable `FABRIC_SERVICE_SUPERVISOR=<manager>` or
|
|
209
230
|
the argument `--fabric-supervisor <manager>` (also `--fabric-supervisor=<manager>`), after the
|
|
210
231
|
executable and before any `--`; `<manager>` is `launchd`, `systemd`, `task-scheduler` or `node`
|
|
@@ -323,7 +344,7 @@ descriptor says `"lifecycle": {"manager": "systemd", "unit": "<id>.<instance>.se
|
|
|
323
344
|
|
|
324
345
|
| Verb | Command |
|
|
325
346
|
|---|---|
|
|
326
|
-
| install, upgrade | `systemd_install` / `systemdInstall`: write `~/.config/systemd/user/<unit>`, `daemon-reload`, `enable`, `restart` (so the new unit is what runs). A unit already installed that `is-enabled` reports `disabled` stays off: the file is rewritten and nothing starts (LC-14), unless `force_enable` |
|
|
347
|
+
| install, upgrade | `systemd_install` / `systemdInstall`: write `~/.config/systemd/user/<unit>`, `daemon-reload`, `enable`, `restart` (so the new unit is what runs). A unit already installed that `is-enabled` reports `disabled` stays off: the file is rewritten and nothing starts (LC-14), unless `force_enable`. With `origin` and `service_id` it then polls the well-known answer as `launchd_install` does until this account's listener answers (another account's is refused, DEC-0154 HA-2; FAA-253); Node's `systemdInstall` then returns a promise |
|
|
327
348
|
| stop, keep off | `systemctl --user disable --now <unit>` |
|
|
328
349
|
| uninstall | `systemd_uninstall` / `systemdUninstall`: `disable --now`, remove the unit, `daemon-reload` |
|
|
329
350
|
|
|
@@ -340,7 +361,7 @@ CI job has the real `schtasks` create, read back and delete such a task. The des
|
|
|
340
361
|
|
|
341
362
|
| Verb | Command |
|
|
342
363
|
|---|---|
|
|
343
|
-
| install, upgrade | `task_install` / `taskInstall`: `schtasks /Create /TN <task> /XML <file> /F` from a UTF-16 file it then removes, then `/End` (when it existed) and `/Run`. A task already installed and disabled stays off: it is recreated and disabled again (LC-14), unless `force_enable` |
|
|
364
|
+
| install, upgrade | `task_install` / `taskInstall`: `schtasks /Create /TN <task> /XML <file> /F` from a UTF-16 file it then removes, then `/End` (when it existed) and `/Run`. A task already installed and disabled stays off: it is recreated and disabled again (LC-14), unless `force_enable`. With `origin` and `service_id` it then polls as `launchd_install` does and returns the answer (FAA-253); Node's `taskInstall` then returns a promise |
|
|
344
365
|
| stop, keep off | `schtasks /End /TN <task>`, then `schtasks /Change /TN <task> /DISABLE` |
|
|
345
366
|
| uninstall | `task_uninstall` / `taskUninstall`: `/End`, then `/Delete /F` |
|
|
346
367
|
|
|
@@ -31,4 +31,5 @@ Order matters: each step leaves the service answering.
|
|
|
31
31
|
| Data, logs or caches inside the repository | the OS data, log and cache directories; the old tree read-only for one release |
|
|
32
32
|
| `KeepAlive {SuccessfulExit:false}`, a plist pointing at a checkout or a worktree | a plist generated by `launchd_plist`, pointing at a release |
|
|
33
33
|
| A port another service already uses by default | the port claim at install; move the newcomer |
|
|
34
|
+
| A port held by socket activation (launchd `Sockets`, a systemd `.socket` unit) or by a root proxy in front of the service | the service binds `127.0.0.1:<port>` itself as its own account; launchd's or root's socket reads as `system` and the hosts' and the probe's `listener.owner` refuse it (DEC-0154 HA-2) |
|
|
34
35
|
| An event log in a private shape | the events view over it, with one sentence per event |
|
|
@@ -42,6 +42,16 @@ python3 scripts/fabric_package.py pack <folder> # a dry run: the packa
|
|
|
42
42
|
python3 scripts/fabric_package.py pack <folder> --apply # writes fabric.package.json when nothing is wrong
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
When the folder carries the agent's source manifest, `fabric.source.json` (contract DEC-0149, `fabric-source/1`), `pack`
|
|
46
|
+
writes `secrets[]` from its `secret` slots (name, purpose and `optional`) and `state[]` from its `store` slots' paths,
|
|
47
|
+
says so in its notes, and lists the manifest in `files[]` like any other file; `pack`, `sign` and `verify` refuse a
|
|
48
|
+
package whose `secrets[]` or `state[]` differ from the slots (FAC-SEM-160). A hand-written `secrets[]` naming a secret
|
|
49
|
+
the manifest has no slot for is never dropped: `pack` refuses (rule `source`, `/secrets`) and names each one, so add
|
|
50
|
+
it as a `secret` slot (or take it out) and pack again. An author's `state: []` is a copy packed
|
|
51
|
+
without state and is kept. A manifest that is no `fabric-source/1` object is refused. The manifest's own rules and the
|
|
52
|
+
tree check under it are the source check's (`references/source-and-instance.md` of adapting-projects-to-fabric, *The
|
|
53
|
+
source manifest*).
|
|
54
|
+
|
|
45
55
|
Run `adapting-projects-to-fabric/scripts/fabric_source_check.py check <folder> --every-file` on the folder you pack first (a floor under your own
|
|
46
56
|
review, not a secret scanner): a package carries
|
|
47
57
|
the source, and the instance (the owner's paths, keys, strings) stays out of it (`references/source-and-instance.md` of
|
|
@@ -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 `93e7db164c7829e2783b4b7705c4113bb5201131`
|
|
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;
|
|
@@ -224,7 +224,8 @@ well-known `process.pid` is never proof (`docs/specification/service.md#ha-2` at
|
|
|
224
224
|
The kit's module is `fabric_peer_owner` / `fabric-peer-owner.mjs` (layer L0, [kit modules](kit-modules.md)): `listener(port)`,
|
|
225
225
|
`server_end(sock)` / `serverEnd(socket)`, `peer`, and a connection checked before a byte is written
|
|
226
226
|
(`CheckedHTTPConnection`; Node `checkedSocket`, `CheckedAgent`). Python reads macOS through `sysctl
|
|
227
|
-
net.inet.tcp.pcblist_n` (calibrated on a connected pair of its own, else `netstat -anv
|
|
227
|
+
net.inet.tcp.pcblist_n` (calibrated on a connected pair of its own, else `netstat -anv`; the pinned HA-2 table names
|
|
228
|
+
`netstat -anv` and `pcblist64`, an older record layout of the same kernel table), Linux through `/proc/net/tcp`
|
|
228
229
|
and `/proc/net/tcp6`, Windows through `GetExtendedTcpTable` and the owner's token SID; Node reads `netstat -anv` and `ps`
|
|
229
230
|
on macOS, the same `/proc` files on Linux, and `netstat -ano` with the owner SID from PowerShell `GetOwnerSid` against `whoami /user` on Windows (SIDs, never names). Fail closed: everything but
|
|
230
231
|
`this-account` refuses, `unknown` included.
|
|
@@ -238,17 +239,34 @@ listener of this account's pid on the port counts as this account's (`CheckedHTT
|
|
|
238
239
|
refuses (the probe, the hub client and `fabric_http` included, FAA-272), and Apple's `/usr/bin/python3` sees the
|
|
239
240
|
whole table (FAA-267). A vouch is a weaker proof than the table's: it shows the job's pid listens on the port (`127.0.0.1` or the
|
|
240
241
|
IPv4 wildcard for a LaunchAgent's installer, exactly `127.0.0.1` for a LaunchDaemon's), not that it accepted this connection, so a more specific bind by root over a wildcard job would pass;
|
|
241
|
-
the
|
|
242
|
+
the pin's ruling on vouching is DEC-0155 RT-3 (the contract's CT-261), which vouches only for a listen on exactly
|
|
243
|
+
`127.0.0.1`: the kit's IPv4 wildcard vouch for a LaunchAgent departs from it until a later release (filed by FAA-258).
|
|
242
244
|
|
|
243
245
|
Every kit client that sends a credential to loopback checks first: `check_service.py` (rule `listener.owner`, the first that reads the port for
|
|
244
|
-
a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user
|
|
245
|
-
`
|
|
246
|
+
a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user`, its
|
|
247
|
+
`data.reason` and sentence the owner's reason: `table-restricted`, then not retryable, or the owner's kind or `no-row`, FAA-272),
|
|
248
|
+
`fabric_http` / `fabric-http.mjs` for a plain `http://` loopback URL (`HttpError` kind `foreign-user` with the same `reason`, never retried),
|
|
246
249
|
`check_dashboard_layout.mjs` before a local login code, and the launchd installers' poll (a listener of another account
|
|
247
250
|
is never a successful install, except a LaunchDaemon's job as its root installer proves it; the systemd and Task Scheduler installers do not poll yet, FAA-253). A running board
|
|
248
251
|
session reports a door another account holds as `board.down` with reason `unreachable` until FAA-252. On macOS the Node
|
|
249
252
|
twin reads the pid from `netstat` and the uid from `ps` a few milliseconds apart, so a pid reused in between would be
|
|
250
|
-
misread; the Python twin's `sysctl` reader takes both from one record.
|
|
251
|
-
|
|
253
|
+
misread; the Python twin's `sysctl` reader takes both from one record. The server side (HA-2, HA-3; FAA-227): the kit's `LoopbackHTTPServer` and Node's `loopbackServer(httpServer)` tell
|
|
254
|
+
each accepted connection's peer (`peer`) before a byte of the request is read, in the connection's own thread or
|
|
255
|
+
while the socket is still paused. `peer_check` / `peerCheck` (`PEER_CHECKS`): `refuse`, the default, closes a peer of
|
|
256
|
+
another OS account unread and serves the rest; `strict` also refuses an owner nobody can tell (a restricted table
|
|
257
|
+
included) and a system account other than root (uid 0, which can read every account's files anyway and installs a
|
|
258
|
+
LaunchDaemon; a Windows service account is refused); `off` reads no table. The check applies to loopback peers
|
|
259
|
+
(127.0.0.0/8, `::1`, either IPv4-mapped): HA-2 is about another account of this computer, so a peer at any other
|
|
260
|
+
address (a service bound to `0.0.0.0`) is served under `refuse` and `strict` alike without a table read, and its
|
|
261
|
+
owner is none (`peer_owner()` None, `connectionOwner` null); such a peer is never T0 or T1. Use `strict` for a dashboard, an MCP
|
|
262
|
+
surface or a token-free door on a computer with several counted accounts, where the interpreter sees the whole
|
|
263
|
+
table (Apple's `/usr/bin/python3`, not an ad-hoc-signed one on macOS 27.2). A refusal writes one line on stderr
|
|
264
|
+
(kind, uid, pid, never the peer's bytes); a handler finds its peer's owner with `self.server.peer_owner()` or
|
|
265
|
+
`connectionOwner(req.socket)`. The cost is one table read per connection, not per request: a few milliseconds from
|
|
266
|
+
`sysctl` or `/proc` in-process; Node on macOS and Windows runs `netstat` and `ps` or PowerShell for each, tens to
|
|
267
|
+
hundreds of milliseconds, so a Node service there that answers the well-known read within 100 ms keeps its
|
|
268
|
+
connections alive or chooses `off` where one account uses the computer. The port claim file (HA-1) waits on the
|
|
269
|
+
contract's CT-228.
|
|
252
270
|
|
|
253
271
|
## Semantic rules a host applies
|
|
254
272
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -10,6 +10,7 @@ DEC-0019, at the commit this skill pins. This page is the how-to.
|
|
|
10
10
|
- [On the platform](#on-the-platform)
|
|
11
11
|
- [Checking it](#checking-it)
|
|
12
12
|
- [What a host does and will not do](#what-a-host-does-and-will-not-do)
|
|
13
|
+
- [Registered from several hosts](#registered-from-several-hosts)
|
|
13
14
|
- [Rotation](#rotation)
|
|
14
15
|
|
|
15
16
|
## Is it a remote service?
|
|
@@ -68,8 +69,9 @@ watch the gate fail.
|
|
|
68
69
|
## On the platform
|
|
69
70
|
|
|
70
71
|
- **Token**: a random value of at least 16 characters (32 bytes recommended), set as a secret;
|
|
71
|
-
the same value is written on the
|
|
72
|
-
time (`tokenMatches`).
|
|
72
|
+
the same value is written on the registering host by `registerRemote`. Compare in constant
|
|
73
|
+
time (`tokenMatches`). A service registered from more than one host holds one token per host
|
|
74
|
+
([Registered from several hosts](#registered-from-several-hosts)).
|
|
73
75
|
- **Session key**: 32 random bytes, a separate secret; rotating it signs every operator out.
|
|
74
76
|
- **Codes and sessions**: `MemoryCodeStore`: a restart forgets every outstanding code, so none
|
|
75
77
|
can be replayed, and every session record, so a deploy signs the operator out (DEC-0039: the
|
|
@@ -122,10 +124,49 @@ The login rules keep every `Set-Cookie` of the answer and judge the session cook
|
|
|
122
124
|
| start, stop, restart, update | not offered |
|
|
123
125
|
| a host older than the remote placement | shows the service as invalid and never contacts it |
|
|
124
126
|
|
|
127
|
+
## Registered from several hosts
|
|
128
|
+
|
|
129
|
+
One cloud service is often registered from more than one host: the operator's two computers, or an organization's
|
|
130
|
+
space beside a personal one (FAA-280). Each registering host gets **its own token**; a token is never copied from one
|
|
131
|
+
host to another, so one host can be revoked or rotated without touching the others.
|
|
132
|
+
|
|
133
|
+
- **On the platform**, keep one secret per host (`FABRIC_HOST_TOKEN_<HOST>`, each at least 16 characters) and accept
|
|
134
|
+
a request whose bearer matches any of them. Compare it with every token, each with `tokenMatches` /
|
|
135
|
+
`token_matches`, with no early return, so the answer's timing does not say which host's token matched:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
def host_of(authorization, tokens): # tokens: {host name: token}; the kit's comparison, once per token
|
|
139
|
+
matched = [host for host, token in sorted(tokens.items()) if fs.token_matches(authorization, token)]
|
|
140
|
+
return matched[0] if matched else None # None: a 401 with an empty body, as with one token
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`well_known_allowed` / `wellKnownAllowed` take one token: call it once per token the same way, or gate the route on
|
|
144
|
+
`host_of(...) is not None`.
|
|
145
|
+
- **On each host**, run `registerRemote` / `register_remote` with that host's token only. The descriptor is the same
|
|
146
|
+
on every host but its `tokenFile`, which holds that host's value and nothing else (0600, never printed).
|
|
147
|
+
- **Sign-in**: `LoginCodes` binds a session to one token's revision. Give each host its own `LoginCodes` with
|
|
148
|
+
`token=` that host's token and its own store (`MemoryCodeStore`, or a durable row per host); the session key may be
|
|
149
|
+
shared. Issue the login code from the instance whose token the bearer matched; redeem a code and check a session by
|
|
150
|
+
trying each instance in turn (a code another instance issued is not in its store, and a session cookie carries
|
|
151
|
+
another token's revision, so neither changes anything there). Removing or rotating one host's token then ends that
|
|
152
|
+
host's sessions only. One `LoginCodes` whose `token=` is a callable returning every token joined also works, and
|
|
153
|
+
signs every host out when any one rotates.
|
|
154
|
+
- **The probe**: `check_service.py <id>` run on a host sends that host's token, so `well-known.requires-token` passes
|
|
155
|
+
on each; the request without a token, or with a value no host holds, still gets the `401` with an empty body.
|
|
156
|
+
- **Revoking a host**: delete its secret on the platform and restart; that host then sees `401` and shows the service
|
|
157
|
+
`down`, never `foreign`. Remove its descriptor there with `remove_descriptor` / `removeDescriptor`.
|
|
158
|
+
- **Adding a host**: create its secret, restart, then `registerRemote` on the new host. The other hosts' tokens and
|
|
159
|
+
sessions do not change.
|
|
160
|
+
|
|
161
|
+
A token per host still gives each host the descriptor token's whole authority over the service. A remote surface no
|
|
162
|
+
host token reaches (callers with their own delegation, the page opened only with Fabric Dashboards' one-time code)
|
|
163
|
+
is the "local and cloud" form, FAA-204, which waits on the contract recording that form.
|
|
164
|
+
|
|
125
165
|
## Rotation
|
|
126
166
|
|
|
127
167
|
Rotate the token by setting the new value on the platform and re-running `registerRemote` on the
|
|
128
|
-
|
|
168
|
+
registering host with it (on a service registered from several hosts, only that host's token,
|
|
169
|
+
[above](#registered-from-several-hosts)); the old token stops working the moment the platform restarts with the
|
|
129
170
|
new value, and so does every session begun under it when `LoginCodes` has `token=`. The re-run keeps the
|
|
130
171
|
installed `source` when it passes none (one the kit refuses, edited by hand, is dropped with the warning); pass
|
|
131
172
|
`runner`, `peer`, `summary`, `doctor` and `installedBy` / `installed_by` again, since a re-run writes only what it is
|
|
@@ -127,6 +127,25 @@ async function serviceTarget(spec, dir) {
|
|
|
127
127
|
return { url, declared, name: `${id}.${instance}` };
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// DEC-0154 PL-3 and HA-2 (FAA-260 items 2 and 14): a raw loopback address opens in the browser only while its port's
|
|
131
|
+
// listener is this OS account's, read from the socket table just before the page opens. The browser connects on its
|
|
132
|
+
// own, so this is the listener's owner, not that connection's: a listener swapped in between stays a residue
|
|
133
|
+
// (SECURITY.md). A copy of this file alone cannot tell the owner and opens no loopback page.
|
|
134
|
+
async function loopbackRefusal(target) {
|
|
135
|
+
let url;
|
|
136
|
+
try { url = new URL(target); } catch { return null; }
|
|
137
|
+
if (url.protocol !== 'http:') return null;
|
|
138
|
+
const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
139
|
+
if (!(host === 'localhost' || host.endsWith('.localhost') || /^127\./.test(host) || host === '::1'
|
|
140
|
+
|| host === '0.0.0.0' || /^::ffff:(127\.|7f)/.test(host))) return null;
|
|
141
|
+
const po = await import('./fabric-peer-owner.mjs').catch(() => null);
|
|
142
|
+
if (!po) return 'fabric-peer-owner.mjs is not beside this file: the loopback listener\'s owner cannot be told, so the page is not opened (DEC-0154 HA-2)';
|
|
143
|
+
const port = Number(url.port || 80);
|
|
144
|
+
const who = await po.listener(port);
|
|
145
|
+
if (who.kind === po.THIS_ACCOUNT) return null;
|
|
146
|
+
return `foreign-user: the listener on port ${port} reads as ${who.kind}${who.reason ? ` (${who.reason})` : ''}, so the page is not opened (DEC-0154 PL-3, HA-2)`;
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
// Runs inside the page. Everything it reports is measured from the rendered layout.
|
|
131
150
|
function measure([floor, cap]) {
|
|
132
151
|
const de = document.documentElement;
|
|
@@ -319,6 +338,8 @@ async function main(argv) {
|
|
|
319
338
|
try {
|
|
320
339
|
const context = await browser.newContext({ viewport: { width: Math.max(...opts.widths), height: opts.height } });
|
|
321
340
|
const page = await context.newPage();
|
|
341
|
+
const refused = await loopbackRefusal(target);
|
|
342
|
+
if (refused) return notRun(refused);
|
|
322
343
|
const response = await page.goto(target, { waitUntil: 'load' });
|
|
323
344
|
if (response && !response.ok()) {
|
|
324
345
|
return notRun(`the page answered HTTP ${response.status()}`);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py
CHANGED
|
@@ -28,8 +28,14 @@ CREDENTIAL_PAIRS = {
|
|
|
28
28
|
'session': {'token', 'id'},
|
|
29
29
|
'code': {'login', 'auth', 'access', 'session'}}
|
|
30
30
|
JWT_SHAPE = re.compile(r'eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*')
|
|
31
|
-
SECRET_VALUE
|
|
32
|
-
|
|
31
|
+
# The contract's SECRET_VALUE at the pin (DEC-0151): `Bearer ` at a value's start in any case, or a provider key with its
|
|
32
|
+
# prefix in its canonical case after a boundary (the start, a character that is no ASCII letter or digit, or an escape);
|
|
33
|
+
# an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more.
|
|
34
|
+
SECRET_VALUE = re.compile(r'(^[Bb][Ee][Aa][Rr][Ee][Rr]\s.+|(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt"\']|\\u[0-9A-Fa-f]{4}'
|
|
35
|
+
r'|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m'
|
|
36
|
+
r'|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)'
|
|
37
|
+
r'(?:sk-(?=[A-Za-z0-9_-]{0,255}?[0-9])(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})'
|
|
38
|
+
r'|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}))', re.S)
|
|
33
39
|
LONG_RUN = re.compile(r'[A-Za-z0-9+/=_-]{40,}')
|
|
34
40
|
|
|
35
41
|
|
|
@@ -115,6 +121,11 @@ def check(data):
|
|
|
115
121
|
require(all(isinstance(v, str) and 0 < len(v) <= 200 for v in (target, viewer)), 'missing_device')
|
|
116
122
|
fallback = data.get('fallback', 'if_absent')
|
|
117
123
|
require(fallback in ('if_absent', 'never'), 'invalid_fallback')
|
|
124
|
+
# DEC-0154 PL-3 and HA-2 (FAA-251): who listens on http_url's port, as the host read it from the socket table
|
|
125
|
+
# (fabric_peer_owner's kinds, or check_service.py's foreign-user). A browser opens a raw loopback address only
|
|
126
|
+
# after that listener is this OS account's; absent, it is unknown.
|
|
127
|
+
owner = data.get('listener_owner', 'unknown')
|
|
128
|
+
require(owner in ('this-account', 'another-account', 'system', 'unknown', 'none', 'foreign-user'), 'invalid_listener_owner')
|
|
118
129
|
kind = action.get('kind')
|
|
119
130
|
if kind == 'remote_open':
|
|
120
131
|
require(set(action) <= {'kind', 'device_id', 'service', 'path'}, 'unexpected_action_field')
|
|
@@ -128,17 +139,26 @@ def check(data):
|
|
|
128
139
|
require(set(action) == {'kind', 'url'} and action.get('url') == http, 'not_resolved_http_url')
|
|
129
140
|
require(viewer == target, 'wrong_viewer_device')
|
|
130
141
|
require(state == 'not_installed' and fallback == 'if_absent', 'browser_fallback_forbidden')
|
|
142
|
+
require(owner == 'this-account', 'listener_not_this_account')
|
|
131
143
|
else:
|
|
132
144
|
raise ValueError('unsupported_dashboard_action')
|
|
133
145
|
return {'ok': True, 'kind': kind}
|
|
134
146
|
|
|
135
147
|
|
|
148
|
+
def read_input(limit=65536):
|
|
149
|
+
# The handoff's bytes, decoded as UTF-8 with a byte order mark dropped: Windows before Python 3.15 decodes a text
|
|
150
|
+
# stdin with its code page, which misreads a Cyrillic handoff (FAA-210). A stream without bytes is read as text.
|
|
151
|
+
stream = getattr(sys.stdin, 'buffer', None)
|
|
152
|
+
raw = stream.read(limit + 1) if stream is not None else sys.stdin.read(limit + 1)
|
|
153
|
+
require(len(raw) <= limit, 'input_too_large')
|
|
154
|
+
return raw.decode('utf-8-sig') if isinstance(raw, bytes) else raw.lstrip('\ufeff')
|
|
155
|
+
|
|
156
|
+
|
|
136
157
|
def main():
|
|
137
158
|
try:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
except (ValueError, TypeError, KeyError, AttributeError):
|
|
159
|
+
result = check(json.loads(read_input()))
|
|
160
|
+
# RecursionError: Python 3.9 to 3.13 raise it on a deeply nested document under the cap
|
|
161
|
+
except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
|
|
142
162
|
# Fixed reasons only: never echo a submitted URL, token or JSON blob.
|
|
143
163
|
result = {'ok': False, 'reason': 'dashboard_handoff_refused'}
|
|
144
164
|
print(json.dumps(result))
|