@passioncode-ai/fabric-agent-adapter 0.77.0 → 0.78.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 +85 -0
- package/README.md +17 -4
- package/SECURITY.md +17 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +10 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +27 -39
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +69 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +43 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +325 -24
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +198 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +308 -22
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +208 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +113 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +10 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +179 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/references/channels.md +129 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/references/cycle.md +129 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/scripts/observer_inventory.py +259 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +22 -32
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.78.0 (2026-11-12)
|
|
4
|
+
|
|
5
|
+
The contract pin stays `18f8d487`. Kit modules change: a new `fabric_tailnet` and `fabric-tailnet.mjs` (layer L0); `fabric_supervisors` and `fabric-supervisors.mjs`, and the facades
|
|
6
|
+
`fabric_service` and `fabric-service.mjs` (new exports `launchd_domain` / `launchdDomain`, `SYSTEM_DAEMONS`,
|
|
7
|
+
`jobWellKnownOf`, `ForeignUserAnswer`); the probe `check_service.py` (`lifecycle.one-copy`); guidance in building-fabric-services and
|
|
8
|
+
working-with-fabric.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **A LaunchDaemon for a headless Mac** (FAA-278, with FAA-228): the launchd helpers in both kits work in launchd's system
|
|
13
|
+
domain, for a Mac that a FileVault restart (`fdesetup authrestart`) leaves with no GUI login, where a `gui/<uid>` job
|
|
14
|
+
never starts. A plist in `/Library/LaunchDaemons` (the folder's case ignored) or `domain="system"` /
|
|
15
|
+
`domain: 'system'` is a LaunchDaemon, macOS only; `launchd_plist(..., user_name=)` / `launchdPlist({userName})` writes
|
|
16
|
+
its `UserName`, which a system install requires; `launchd_domain` / `launchdDomain` names the domain.
|
|
17
|
+
`launchd_loaded`, `launchd_pid` and `launchd_override` read the system domain without root. `launchd_install` and
|
|
18
|
+
`launchd_uninstall` there need root, refuse before writing anything otherwise and name the command to run as root,
|
|
19
|
+
refuse `purge` (the data is the `UserName`'s), and accept the installed service's token-free answer only on a
|
|
20
|
+
connection launchd's own job accepted (its pid from `launchctl print system/<label>`, running as the `UserName`'s uid)
|
|
21
|
+
or, under a restricted socket
|
|
22
|
+
table, on a port that pid listens on (`lsof -p`, a weaker vouch, CT-261). A `gui/<uid>` bootstrap that fails for lack
|
|
23
|
+
of a GUI session (error 125, an SSH install) now says to remove that LaunchAgent and use the system domain. The probe's
|
|
24
|
+
`lifecycle.one-copy` asks launchd in the domain the descriptor's plist names. A LaunchDaemon's label is the machine's:
|
|
25
|
+
an install refuses, before writing anything, a label another plist holds, a plist at its path of another label or
|
|
26
|
+
another `UserName`, a `UserName` that is not a short name, is root or is no account of the Mac, and an origin other
|
|
27
|
+
than `http://127.0.0.1:<port>`; one refused after its bootstrap leaves the job loaded and names the `bootout`. An
|
|
28
|
+
uninstall refuses a label another plist holds before its `bootout`. The root installer's read of its own job's token-free answer, proved by the job's pid, its uid and a bind on
|
|
29
|
+
exactly `127.0.0.1`, departs from the pinned HA-2 (and the contract's later RT-3); the contract owner accepted it in
|
|
30
|
+
principle as DEC-0161, proposed (FAA-288). `references/lifecycle.md`
|
|
31
|
+
*The system domain* says which steps run as the `UserName` and which as root, and what a host must not do (FAA-287);
|
|
32
|
+
one scenario eval.
|
|
33
|
+
|
|
34
|
+
- **A kit module for an agent behind `tailscale serve`: `fabric_tailnet`** (FAA-297, a board request of 2026-11-11).
|
|
35
|
+
`fabric_tailnet.py` and `fabric-tailnet.mjs` (layer L0, no imports) decide who is calling and what that person may
|
|
36
|
+
open. The person comes from `Tailscale-User-Login`, believed only on the agent's own serve listener (a loopback port
|
|
37
|
+
or a Unix socket that only serve reaches) and from a loopback peer; a request carrying serve's headers on any other
|
|
38
|
+
listener is refused (`serve-header-off-serve`), never half-trusted. Groups, dashboards and capabilities come from a
|
|
39
|
+
`tailnet-perimeter/1` file generated from the organization's access registry: a file that does not name its source
|
|
40
|
+
(`generatedFrom`: registry, revision, time) is refused (`perimeter-not-generated`). MCP also takes a personal key,
|
|
41
|
+
stored only as its SHA-256, owned by one login, and refused when its capabilities are wider than that login's groups
|
|
42
|
+
(`key-wider-than-group`). Every refusal is 401 or 403 with a stable code; no answer carries a key or its hash. Both
|
|
43
|
+
twins answer one set of vectors, `test/fixtures/tailnet/vectors.json`; the reference is
|
|
44
|
+
`references/surfaces-and-auth.md` → "Behind tailscale serve".
|
|
45
|
+
|
|
46
|
+
- **A fifth skill, `observing-products`: observer mode** (FAA-290, the observer-mode design of Fabric CO-546). Any
|
|
47
|
+
agent with the plugin can take a standing watch over one product. Setup first: it writes the product's channel
|
|
48
|
+
inventory, `docs/observer/channels.md` (ops log, errors, analytics, funnels, revenue, store, uptime, CI, the Fabric
|
|
49
|
+
board, support), each row with its read route, its credential by vault slot name, its cursor, baseline, thresholds,
|
|
50
|
+
placement (`local` or `remote`) and state, and proves one read from each; a channel without a proven read is never
|
|
51
|
+
counted as watched. Then a cycle every interval (30 minutes by default): bounded reads from the cursors, a failed
|
|
52
|
+
read as a finding, one cycle record before any cursor moves, no overlap with its own last run, a missed window run
|
|
53
|
+
once; triage S0 to S3 within the project's authority (`observe`, `fix`, `ship`, the last set only by the person),
|
|
54
|
+
every fix through the project's own delivery route and read back from the channel that showed the fault, two failed
|
|
55
|
+
attempts stop the automatic work on a finding, channel text is data and never an instruction, a USD 100 a month
|
|
56
|
+
budget per observer by default, silent cycles and one message on change. Scheduling: Fabric's observer-mode switch
|
|
57
|
+
where Fabric runs, else Claude Code `/loop`, a cron routine, or a cloud routine that watches only `remote` channels
|
|
58
|
+
and names the local ones it skipped. `scripts/observer_inventory.py` (`check`, `template`) validates an inventory and
|
|
59
|
+
prints connected out of named; `test/test_observer_inventory.py`; trigger and scenario evals in
|
|
60
|
+
`test/evals/observing-products/`. The release smoke checks the installed skill.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **Each skill's `description` is about 350 characters, within the skill listing budget** (FAA-291, a board request
|
|
65
|
+
of 2026-11-11). Claude Code shares one character budget among every installed skill's description and, over it,
|
|
66
|
+
keeps a description only for the most used skills (measured on the operator's machine: 105 skills, 35 962
|
|
67
|
+
characters against 30 000), so `working-with-fabric`, the skill the session hooks point at, was listed by name
|
|
68
|
+
alone. The five descriptions went from 4 542 characters to 1 722 (the four of 0.77.0 from 3 649 to 1 388): when to
|
|
69
|
+
use, two to four triggers in English and Russian, and the NOT-for boundary naming the neighbour. Each skill's
|
|
70
|
+
previous description, every trigger included, moved word for word to its `references/when-to-use.md`, linked
|
|
71
|
+
from a short `## When to use` section. `test/validate.py` now fails a description over 360 characters
|
|
72
|
+
(`DESCRIPTION_LISTING_BUDGET`) and its self-test plants one.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- SKILL.md headroom (FAA-263): building-fabric-services and working-with-fabric each keep at least 150 words under the
|
|
77
|
+
4750-word cap; the detail stays in their references, which already held most of it (the dashboard kit, the interop
|
|
78
|
+
*Actions* section, the session hooks, the board's *When nobody answers*), and the remote placement's *On the
|
|
79
|
+
platform* and the native host hooks gained what they lacked; no rule changes.
|
|
80
|
+
|
|
81
|
+
### Upgrading
|
|
82
|
+
|
|
83
|
+
- A service that installs a LaunchDaemon vendors `fabric_supervisors` (or `fabric-supervisors.mjs`) with its facade and
|
|
84
|
+
`fabric_peer_owner` (or `fabric-peer-owner.mjs`) of 0.76.0 or later, which it imports. A LaunchAgent install behaves
|
|
85
|
+
as before, but in Node `fabric-supervisors.mjs` 0.78.0 imports `fabric-peer-owner.mjs` 0.76.0 or later in either
|
|
86
|
+
domain; `fabric_kit.py install` copies both.
|
|
87
|
+
|
|
3
88
|
## 0.77.0 (2026-11-08)
|
|
4
89
|
|
|
5
90
|
The contract pin stays `18f8d487`. Kit modules change: `fabric_events` and `fabric-events.mjs`, `fabric_peer_owner`
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**Fabric Agent Adapter** makes any agent Fabric-compatible. Fabric is PassionCode.ai's
|
|
10
10
|
product, the CEO AI agent: you talk to Fabric, and it chooses, binds and runs the agents that
|
|
11
|
-
do the work. This adapter is how an agent becomes one Fabric can bind:
|
|
11
|
+
do the work. This adapter is how an agent becomes one Fabric can bind: five Agent Skills,
|
|
12
12
|
Python and Node reference kits and a live conformance probe for the
|
|
13
13
|
[Fabric Agent Contract](https://github.com/passioncode-ai/fabric-agent-contract). It also works
|
|
14
14
|
on its own: the skills, kits and probe need no Fabric install.
|
|
@@ -18,7 +18,7 @@ on its own: the skills, kits and probe need no Fabric install.
|
|
|
18
18
|
[](LICENSE)
|
|
19
19
|
|
|
20
20
|
The skills are `adapting-projects-to-fabric`, `creating-fabric-agents`,
|
|
21
|
-
`building-fabric-services
|
|
21
|
+
`building-fabric-services`, `working-with-fabric` and `observing-products`.
|
|
22
22
|
|
|
23
23
|
`working-with-fabric` is for the other side: a coding agent (Claude Code, Codex, Kimi Code, Kilo, Hermes) opened in
|
|
24
24
|
a terminal that works in a project through Fabric. It finds Fabric (`hub.json`, its `pid`), has the machine's
|
|
@@ -58,6 +58,18 @@ for activation, reload and verification limits. The
|
|
|
58
58
|
they need from Fabric; the [board reference](plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md)
|
|
59
59
|
is the board-first workflow.
|
|
60
60
|
|
|
61
|
+
`observing-products` puts an agent on a standing watch over one product (observer mode). It first writes the
|
|
62
|
+
product's channel inventory, `docs/observer/channels.md`: every channel the product reports through (operations log,
|
|
63
|
+
errors, analytics, funnels, revenue, store, uptime, CI, the Fabric board, support), each with its read route, its
|
|
64
|
+
credential by vault slot name, its baseline and a proven read. Then every interval (30 minutes by default) it reads
|
|
65
|
+
each channel from its cursor, triages what changed (S0 to S3) and fixes it through the project's own delivery route,
|
|
66
|
+
within the authority the project sets (`observe`, `fix`, `ship`). `scripts/observer_inventory.py check` validates the
|
|
67
|
+
inventory and prints connected out of named:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 plugins/fabric-agent-adapter/skills/observing-products/scripts/observer_inventory.py template --project example-app
|
|
71
|
+
```
|
|
72
|
+
|
|
61
73
|
`building-fabric-services` makes an agent a long-lived local service with a dashboard
|
|
62
74
|
that is always alive, runs as one copy, keeps its state through a reinstall and appears
|
|
63
75
|
in Fabric Dashboards by itself: the `fabric-service/0.1` extension. It also makes an **online** agent
|
|
@@ -174,6 +186,7 @@ npx skills add passioncode-ai/fabric-agent-adapter --skill adapting-projects-to-
|
|
|
174
186
|
npx skills add passioncode-ai/fabric-agent-adapter --skill creating-fabric-agents
|
|
175
187
|
npx skills add passioncode-ai/fabric-agent-adapter --skill building-fabric-services
|
|
176
188
|
npx skills add passioncode-ai/fabric-agent-adapter --skill working-with-fabric
|
|
189
|
+
npx skills add passioncode-ai/fabric-agent-adapter --skill observing-products
|
|
177
190
|
```
|
|
178
191
|
|
|
179
192
|
### Configure
|
|
@@ -211,10 +224,10 @@ EOF
|
|
|
211
224
|
kill %1
|
|
212
225
|
```
|
|
213
226
|
|
|
214
|
-
Verified 2026-11-
|
|
227
|
+
Verified 2026-11-08 from the published npm package 0.77.0 (`npm pack` into a throwaway folder, `KIT`
|
|
215
228
|
pointing at its skill) with Claude Code 2.1.299: the probe reported 44 rules, 0 FAIL, `listener.owner` PASS;
|
|
216
229
|
the CLI connected to the sample over streamable HTTP, called `sample.echo` and printed `ping`
|
|
217
|
-
([record](docs/evidence/runs/2026-11-
|
|
230
|
+
([record](docs/evidence/runs/2026-11-08-0.77.0-quick-start.md)). The probe prints one line per rule
|
|
218
231
|
(`PASS`, `FAIL` or `NOT_RUN` with its evidence) and exits 1 on any `FAIL`; run it against your
|
|
219
232
|
own service by its id once its installer has written the descriptor.
|
|
220
233
|
`FABRIC_REAL_CLIENT=1 python3 -m unittest discover -s test -p test_real_client.py` repeats the connection check
|
package/SECURITY.md
CHANGED
|
@@ -25,7 +25,13 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
|
|
|
25
25
|
- The service kits create the service token (0600) and the instance lock in the
|
|
26
26
|
service's own data directory, and write supervisor files only when an installer calls
|
|
27
27
|
them: a launchd plist in `~/Library/LaunchAgents` (macOS), a systemd user unit (Linux) or
|
|
28
|
-
a per-user Scheduled Task (Windows), plus one descriptor in the services directory.
|
|
28
|
+
a per-user Scheduled Task (Windows), plus one descriptor in the services directory. A LaunchDaemon in
|
|
29
|
+
`/Library/LaunchDaemons` (FAA-278) is written only by an installer running as root, which the kits never become:
|
|
30
|
+
without root they refuse before writing and name the command, and refuse a plist without `UserName` (except one written
|
|
31
|
+
there with an explicit `domain="gui"`, not refused yet, FAA-286 item 21); the job runs as the account its `UserName`
|
|
32
|
+
names. The installer accepts the job's answer only on a connection launchd's own job accepted
|
|
33
|
+
(its pid from `launchctl print system/<label>`) or, where the socket table is restricted, on a port that pid listens
|
|
34
|
+
on (`lsof -p`, the weaker vouch below, CT-261). The
|
|
29
35
|
token is readable by any process of the same user; the protocol defends against web pages
|
|
30
36
|
and mistakes, not against hostile local code.
|
|
31
37
|
- The kits' connectors (`fabric_http`, `fabric_openrouter` and their Node twins) reach the
|
|
@@ -36,7 +42,12 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
|
|
|
36
42
|
`check_dashboard_layout.mjs` (a local login code) and the launchd installers' poll send
|
|
37
43
|
anything to `127.0.0.1:<port>`, they read the computer's socket table to tell which OS
|
|
38
44
|
account owns the listener (DEC-0154 HA-2, `fabric_peer_owner`), and send nothing to
|
|
39
|
-
another account's, root's, SYSTEM's or an untellable listener
|
|
45
|
+
another account's, root's, SYSTEM's or an untellable listener; the one exception is a LaunchDaemon's installer
|
|
46
|
+
(root, FAA-278), which sends a token-free well-known GET only on a connection the pid launchd names for its job
|
|
47
|
+
accepted while it runs as the plist's `UserName` and listens on exactly `127.0.0.1` (`lsof -p`), or, under a
|
|
48
|
+
restricted table, to a port that pid listens on there (the weaker vouch below): a departure from the pinned HA-2 and
|
|
49
|
+
the contract's later RT-3, accepted in principle by the contract owner as DEC-0161 (proposed; the restricted-table vouch
|
|
50
|
+
included) and kept as a departure until the next repin (FAA-288). They read, never change:
|
|
40
51
|
macOS `sysctl net.inet.tcp.pcblist_n` in the process, or the programs `/usr/sbin/netstat
|
|
41
52
|
-anv -p tcp` and `/bin/ps -o uid= -p <pid>` (the Node twin always); Linux
|
|
42
53
|
`/proc/net/tcp` and `/proc/net/tcp6`; Windows `GetExtendedTcpTable` and the process
|
|
@@ -45,7 +56,10 @@ bounded and non-publishing until explicit scoped grants exist in a project bindi
|
|
|
45
56
|
short-lived child of its own interpreter listening on a random `127.0.0.1` port; when the
|
|
46
57
|
table is restricted (macOS 27.2, FAA-267) the launchd installers also run `/usr/sbin/lsof
|
|
47
58
|
-nP -a -p <pid> -i4TCP -sTCP:LISTEN -Fn` on their own job's pid. That vouch shows the job listens on the port, not that it accepted
|
|
48
|
-
the connection (a more specific bind by root over a wildcard job would pass; contract CT-261). A
|
|
59
|
+
the connection (a more specific bind by root over a wildcard job would pass; contract CT-261). A LaunchDaemon's root
|
|
60
|
+
installer runs that `lsof` and `ps -o uid=` on its job's pid before it accepts an answer, and reads, never changes, every other
|
|
61
|
+
plist in `/Library/LaunchDaemons` (to find another holder of its label, `plutil -convert xml1` for a binary one in
|
|
62
|
+
Node), `launchctl print system/<label>` and the `UserName`'s uid (`pwd`, or `id -u` in Node). A token-free loopback door is not yet protected against
|
|
49
63
|
another account's process (HA-3, FAA-227). An MCP client (Claude Code, Codex) sends the
|
|
50
64
|
token it was registered with on every later connection without this check (the contract's
|
|
51
65
|
residue "a coding agent's bearer"): when the port may have changed hands, rotate the token
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
|
-
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
6
|
-
"version": "0.
|
|
5
|
+
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks; and put an agent on a standing watch over a product (observer mode).",
|
|
6
|
+
"version": "0.78.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: adapting-projects-to-fabric
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Use when adapting an existing agent, MCP server, A2A peer, HTTP service or CLI to the Fabric Agent Contract:
|
|
5
|
+
profile, provider bundle, compatibility check. Triggers: "adapt this to Fabric", «подключить проект к Fabric».
|
|
6
|
+
NOT for a new agent (creating-fabric-agents), a service (building-fabric-services) or a session
|
|
7
|
+
(working-with-fabric).
|
|
4
8
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
5
9
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
10
|
metadata:
|
|
7
11
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
12
|
+
version: "0.78.0"
|
|
9
13
|
contract-version: "0.1.0"
|
|
10
14
|
contract-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
|
|
11
15
|
---
|
|
@@ -16,6 +20,10 @@ Turn an existing stable interface into a proposed Fabric provider bundle, then p
|
|
|
16
20
|
only the compatibility gates for which receipts exist. The Fabric Agent Contract is
|
|
17
21
|
normative; this skill is an authoring workflow pinned to one immutable revision.
|
|
18
22
|
|
|
23
|
+
## When to use
|
|
24
|
+
|
|
25
|
+
The description above stays short for the skill listing budget; the whole reach, every trigger included, is in [references/when-to-use.md](references/when-to-use.md).
|
|
26
|
+
|
|
19
27
|
## Boundary
|
|
20
28
|
|
|
21
29
|
Use this skill to adapt or assess a provider project. Do not use it to:
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/when-to-use.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# When to use adapting-projects-to-fabric
|
|
2
|
+
|
|
3
|
+
The skill's full trigger description, kept here so the front matter's `description` stays within the client's skill listing budget (about 350 characters a skill). Read it when choosing between this skill and a neighbour.
|
|
4
|
+
|
|
5
|
+
Use when adapting an existing agent, MCP server, A2A peer, HTTP service, or terminal CLI to the Fabric Agent Contract (choosing MCP/A2A/local-runner, scaffolding a provider bundle, or checking Fabric compatibility); also for «подключить проект к Fabric» or «сделать агента совместимым с Fabric». NOT for building the Fabric host/orchestrator, configuring an MCP client, a brand-new agent (creating-fabric-agents), running an agent as a local or online Fabric service with a dashboard (building-fabric-services), or a coding session working through Fabric, its board or claims, «подключись к Fabric» (working-with-fabric).
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: building-fabric-services
|
|
3
3
|
description: >-
|
|
4
|
-
Use when running an agent as a
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
and signing it. «сделай агенту дашборд», «локальный сервис агента», «подключи агента к Fabric Dashboards», «отвечать
|
|
9
|
-
на запросы с доски проекта», «расходы агента на модели», "make this agent a local service", "make an online
|
|
10
|
-
dashboard a Fabric service", "add the well-known endpoint". NOT for a one-off script or cron job, a hosted product
|
|
11
|
-
with no agent, the provider manifest (adapting-projects-to-fabric), a new agent's design (creating-fabric-agents), a
|
|
12
|
-
session using Fabric or its board (working-with-fabric), or building the Dashboards app (window, tray).
|
|
4
|
+
Use when running an agent as a fabric-service/0.1 service, local or online: dashboard, auth, jobs, spend, board
|
|
5
|
+
requests. Triggers: "make this agent a local service", «сделай агенту дашборд», «локальный сервис агента». NOT
|
|
6
|
+
for a provider manifest (adapting-projects-to-fabric), a new agent (creating-fabric-agents) or a session
|
|
7
|
+
(working-with-fabric).
|
|
13
8
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
14
9
|
compatibility: Python 3.9+ or Node.js 20+ for the kits (Node.js 22+ for a Linux service's instance lock); the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
15
10
|
metadata:
|
|
16
11
|
author: PassionCode.ai
|
|
17
|
-
version: "0.
|
|
12
|
+
version: "0.78.0"
|
|
18
13
|
contract-version: "0.1.0"
|
|
19
14
|
extension: "fabric-service/0.1"
|
|
20
15
|
extension-commit: "18f8d48762edc5bfb1433657cbfe1c16196df1a6"
|
|
@@ -32,6 +27,10 @@ Every rule below exists because a real service on this estate broke it. Treat th
|
|
|
32
27
|
non-negotiable; the kit implements them so you call a function instead of
|
|
33
28
|
re-deriving a rule.
|
|
34
29
|
|
|
30
|
+
## When to use
|
|
31
|
+
|
|
32
|
+
The description above stays short for the skill listing budget; the whole reach, every trigger included, is in [references/when-to-use.md](references/when-to-use.md).
|
|
33
|
+
|
|
35
34
|
## Boundary
|
|
36
35
|
|
|
37
36
|
Use it to create a service, to bring an existing local server under the protocol, or
|
|
@@ -221,7 +220,9 @@ The installer, not the service, owns the plist and the descriptor. Sequence:
|
|
|
221
220
|
2. `launchd_plist(...)` then `launchd_install(...)`: writes and lints the plist,
|
|
222
221
|
`bootout` and waits for the unload, `bootstrap` with retries on the transient I/O
|
|
223
222
|
error, then polls the well-known document until **this** identity answers on this OS account's listener (HA-2).
|
|
224
|
-
A label the operator disabled stays disabled: only a first install enables.
|
|
223
|
+
A label the operator disabled stays disabled: only a first install enables. On a headless Mac (no GUI login after a
|
|
224
|
+
FileVault restart) install a LaunchDaemon instead: the plist in `/Library/LaunchDaemons` with `user_name`, as root
|
|
225
|
+
(the lifecycle reference, *The system domain*).
|
|
225
226
|
3. Uninstall: `launchd_uninstall` (waits for the unload, removes the plist, resets the
|
|
226
227
|
override; `purge=True` removes data only when the operator asks), `remove_descriptor`.
|
|
227
228
|
4. After an upgrade, `prune_releases(releases_dir, current=…)` keeps the running release
|
|
@@ -244,28 +245,19 @@ while the operator types or a dialog is open, one panel's failure never blanks t
|
|
|
244
245
|
others, and every error is a sentence with the next action. Read
|
|
245
246
|
[the dashboard reference](references/dashboard.md) before building the page.
|
|
246
247
|
|
|
247
|
-
The host shows the page in a pane
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
registered with `server.add_tool(tool, handler, action={id, title, tool, gate})` (`gate` is `none`, `relayed`
|
|
261
|
-
or `human` with its `humanStep`) and declared with `build_well_known(actions=server.action_table())` under
|
|
262
|
-
`extensions/actions/0.1`. A `relayed` action called by the operator's own agent with their decision in
|
|
263
|
-
`_meta.decision` is the operator's act: never answer it "operator session required" or send them to the page.
|
|
264
|
-
A service adopts the relayed gate once Fabric's hub forwards decisions (CT-125), as the contract says; until then its tools
|
|
265
|
-
answer as today, except the board responder's take-back action, declared `relayed` now: an agent whose client sets
|
|
266
|
-
`_meta` calls it with the operator's decision; Claude Code and Codex reach it through the page until CT-125
|
|
267
|
-
([board.md](references/board.md); the reference service's `board.take-back`). Read the interop reference's *Actions* section for when to adopt, the gate, its hooks and its
|
|
268
|
-
refusals.
|
|
248
|
+
The host shows the page in a pane the operator can drag wide, so it holds from **360 CSS px** up and follows the
|
|
249
|
+
host's colour scheme and theme (`fabric-dashboard/0.1`, contract DEC-0036). **Build it from the dashboard kit**
|
|
250
|
+
(`assets/dashboard-kit/`: `dashboard_kit.py install <static dir>`, seven AA themes, light and dark) and declare it with
|
|
251
|
+
`extensions=fs.dashboard_extension()`. [The dashboard kit reference](references/dashboard-kit.md) has the layout rules,
|
|
252
|
+
the markup, the themes, the declaration and how to upgrade a dashboard built before the kit, which the plugin's
|
|
253
|
+
session-start notice offers in a repository that owns the service.
|
|
254
|
+
|
|
255
|
+
**Every button has a tool** (`fabric-actions/0.1`, contract DEC-0121 AP-1): each action the page offers is an MCP tool
|
|
256
|
+
with the same effect, checks, audit record and idempotency key, registered with
|
|
257
|
+
`server.add_tool(tool, handler, action={id, title, tool, gate})` and declared with
|
|
258
|
+
`build_well_known(actions=server.action_table())` under `extensions/actions/0.1`. A `relayed` action called with the
|
|
259
|
+
operator's decision in `_meta.decision` is the operator's act, never answered "operator session required". When to
|
|
260
|
+
adopt, the gates, the hooks, the board responder's take-back and the refusals: the interop reference's *Actions* section.
|
|
269
261
|
|
|
270
262
|
**Say what the agent uses** (`fabric-connections/0.1`, DEC-0096) so Fabric Dashboards draws its chain: the other
|
|
271
263
|
agents, models, connectors, stores and channels, each with its state, from
|
|
@@ -377,12 +369,8 @@ things change and the kits implement each:
|
|
|
377
369
|
example is [`scripts/sample-remote-service.mjs`](scripts/sample-remote-service.mjs): it runs
|
|
378
370
|
with its own TLS or behind a platform router.
|
|
379
371
|
2. **Secrets live on the platform:** the service token and the session key (32 bytes) are
|
|
380
|
-
platform secrets, never in the repository, a URL or an argument vector
|
|
381
|
-
|
|
382
|
-
the movement recorded with
|
|
383
|
-
`python3 "$(project-observatory full-path)/tools/vault.py" moved <project> <env> <NAME> --how "<what moved, where, evidence>" --at <provider>`,
|
|
384
|
-
so the platform's copy is known rather than invisible. Without Project Observatory, record
|
|
385
|
-
where the key lives in the person's own secret manager.
|
|
372
|
+
platform secrets, never in the repository, a URL or an argument vector; a provider key stays in Project
|
|
373
|
+
Observatory or the person's secret manager, its move recorded ([remote placement](references/remote-placement.md), *On the platform*).
|
|
386
374
|
3. **Register it on the operator's computer**, the installer side, run there:
|
|
387
375
|
`registerRemote({ id, name, origin, token, source })` / `register_remote(...)` writes the token file
|
|
388
376
|
(0600) and the descriptor, with `source.repository` naming the code's repository; pass the token from a file or
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -60,6 +60,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
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
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 vendored from commit `f86f0622` with the readings its header names (`1.1.0+kit.3`), 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
|
+
| 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)) |
|
|
63
64
|
| L1 | `fabric_crypto` | Ed25519 signing (a Montgomery ladder that does not branch on the key) and verification, P-256 verification, base64url, sha256 |
|
|
64
65
|
| L1 | `fabric_base` | the protocol name, `ServiceError`, small shared helpers |
|
|
65
66
|
| L1 | `fabric_platform` | Windows: named pipes, owner-only files (DEC-0032) |
|
|
@@ -85,7 +86,7 @@ A module imports only modules listed before it in `kit.json`, so the kit has no
|
|
|
85
86
|
| L3 | `fabric_auth` | the token, the network guard, login codes |
|
|
86
87
|
| L3 | `fabric_events` | events, paging, the JSONL event log (DEC-0029) |
|
|
87
88
|
| L3 | `fabric_logs` | rotating logs, the stop signal and the drain (LC-12) |
|
|
88
|
-
| L3 | `fabric_supervisors` | launchd, systemd user units, Task Scheduler (DEC-0032, LC-14) |
|
|
89
|
+
| L3 | `fabric_supervisors` | launchd (a LaunchAgent, or a LaunchDaemon in the system domain, FAA-278), systemd user units, Task Scheduler (DEC-0032, LC-14) |
|
|
89
90
|
| L3 | `fabric_service` | the facade: every name 0.39.1 exported, re-exported from the modules above |
|
|
90
91
|
| L3 | `fabric_profiles` | runner and model profiles ([profiles](profiles.md)) |
|
|
91
92
|
| 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) |
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -13,6 +13,7 @@ PassionCode.ai the same rules make up the organization's product lifecycle contr
|
|
|
13
13
|
- [The rules](#the-rules)
|
|
14
14
|
- [launchd plist](#launchd-plist-generate-it-with-launchd_plist-or-launchdplist-both-kits-write-the-same-bytes)
|
|
15
15
|
- [Install, upgrade, uninstall](#install-upgrade-uninstall)
|
|
16
|
+
- [The system domain](#the-system-domain)
|
|
16
17
|
- [One copy under a supervisor](#one-copy-under-a-supervisor)
|
|
17
18
|
- [An app's own descriptor and the node](#an-apps-own-descriptor-and-the-node)
|
|
18
19
|
- [Directories](#directories)
|
|
@@ -49,7 +50,8 @@ carry a credential. Only `doctor` and `update` are commands a host may run.
|
|
|
49
50
|
|
|
50
51
|
**Started is not ready.** Readiness is the well-known document, `GET /.well-known/fabric-service`: answered without
|
|
51
52
|
authentication, from memory, in under 100 ms, with `service.id` and `service.instance` equal to the descriptor's,
|
|
52
|
-
on a listener of this OS account (DEC-0154 HA-2: another account's listener is `foreign-user` whatever it answers
|
|
53
|
+
on a listener of this OS account (DEC-0154 HA-2: another account's listener is `foreign-user` whatever it answers; a
|
|
54
|
+
LaunchDaemon's root installer reads it as *The system domain* says).
|
|
53
55
|
`status` is `starting`, `ready`, `degraded` or `stopping`; `degraded` is always present, and an empty list asserts
|
|
54
56
|
full health. A process that cannot serve does not answer, and a host derives `down`. An install or upgrade polls
|
|
55
57
|
that document until `service.id` matches (at most 40 s) before it calls the service started (the launchd installers;
|
|
@@ -76,7 +78,7 @@ systemd and Task Scheduler do not poll yet, FAA-253).
|
|
|
76
78
|
|---|---|---|
|
|
77
79
|
| `Label` | reverse-DNS, stable (`com.example.example-agent`) | the host controls the job by label |
|
|
78
80
|
| `ProgramArguments` | absolute interpreter + module/script inside a **release** directory | a worktree or checkout path breaks the day it moves |
|
|
79
|
-
| `RunAtLoad` | `true` | back at every login |
|
|
81
|
+
| `RunAtLoad` | `true` | back at every login (a LaunchDaemon: at every boot) |
|
|
80
82
|
| `KeepAlive` | `true` | `{SuccessfulExit:false}` leaves a cleanly exited service down |
|
|
81
83
|
| `ThrottleInterval` | `10` | bounds a crash loop |
|
|
82
84
|
| `ExitTimeOut` | above the drain and its hard exit (15 s default; `Drain` ends by 10 s) | SIGKILL arrives after it, so it must never come first |
|
|
@@ -102,11 +104,13 @@ systemd and Task Scheduler do not poll yet, FAA-253).
|
|
|
102
104
|
cannot prove reads `table-restricted` and the error names the way round it (FAA-267). The pid is the job's own, so a
|
|
103
105
|
`ProgramArguments` wrapper that starts the server as a child (`sh -c` without `exec`, `uv run`, `npm start`) cannot
|
|
104
106
|
vouch for it: on such a host start the server with `exec` or run the installer from Apple's `/usr/bin/python3`.
|
|
107
|
+
A LaunchDaemon reads its answer differently: *The system domain*, below.
|
|
105
108
|
4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
|
|
106
109
|
again, then `prune_releases(releases_dir, current=<new release>)`: the running release
|
|
107
110
|
and the one before it stay for rollback, older ones go (LC-11, LC-15).
|
|
108
111
|
5. Uninstall = `launchd_uninstall` (Node: `await launchdUninstall(...)`) + `remove_descriptor`. Data stays; `purge=True`
|
|
109
|
-
(with `service_id`) removes data, logs and cache when the operator asks
|
|
112
|
+
(with `service_id`) removes data, logs and cache when the operator asks; a LaunchDaemon refuses `purge` (*The system
|
|
113
|
+
domain*, below).
|
|
110
114
|
|
|
111
115
|
**The operator's intent wins (LC-14).** A host stops a service with `bootout` **and**
|
|
112
116
|
`disable` (off survives a login) and starts it with `enable` + `bootstrap`; restart is
|
|
@@ -123,6 +127,68 @@ that deletes an override, and a leftover `disabled` would make a later fresh ins
|
|
|
123
127
|
off. Tests use a throwaway label or a fake `launchctl` first on `PATH`, never the
|
|
124
128
|
operator's real jobs.
|
|
125
129
|
|
|
130
|
+
## The system domain
|
|
131
|
+
|
|
132
|
+
A `gui/<uid>` job (a LaunchAgent) runs only while its user is logged in. A headless Mac that a FileVault restart
|
|
133
|
+
(`fdesetup authrestart`) leaves with no GUI login, or an SSH session of a user with no GUI login, has none: `bootstrap`
|
|
134
|
+
answers error 125 ("Domain does not support specified action") and the installer's error says to remove the LaunchAgent
|
|
135
|
+
it wrote (`launchd_uninstall`) and use the system domain (FAA-278). There the job is a **LaunchDaemon**: launchd starts
|
|
136
|
+
it at boot, before any login, as the account its `UserName` names.
|
|
137
|
+
|
|
138
|
+
- **The plist**: `launchd_plist(..., user_name="<short name>")` (Node: `userName`) writes `UserName`, which a system
|
|
139
|
+
install requires (without it the job would run as root, which every client refuses); the file goes in
|
|
140
|
+
`/Library/LaunchDaemons/<label>.plist`. A plist there is in the system domain by its path (the folder's case is
|
|
141
|
+
ignored); `domain="system"` (Node: `domain: 'system'`) says so for any path, and an explicit domain wins over the
|
|
142
|
+
path. Everything else is a LaunchAgent's.
|
|
143
|
+
`UserName` is the account whose Fabric and agents call the service: clients of another account refuse it (HA-2).
|
|
144
|
+
- **Who runs which step.** The release, the descriptor and the service token are written as the `UserName` (steps 1 and
|
|
145
|
+
2 above, for example with `sudo -u <user> -H`), so they live in that user's home with its modes, and the plist's
|
|
146
|
+
working directory and log paths name that home; only the plist, `launchd_install` and `launchd_uninstall` run as root,
|
|
147
|
+
and at uninstall `remove_descriptor` runs as the `UserName` again. A service moved from a LaunchAgent is uninstalled
|
|
148
|
+
as one first, so launchd never holds both. The descriptor's `lifecycle.plist` names the path exactly,
|
|
149
|
+
`/Library/LaunchDaemons/<label>.plist`: a host that reads the folder by its spelling then knows the domain too.
|
|
150
|
+
- **The label is the machine's.** `/Library/LaunchDaemons` and the system domain are shared by every account of the
|
|
151
|
+
Mac, so a LaunchDaemon's label is unique on the machine, not per account (put the account in it on a Mac with a
|
|
152
|
+
personal and an organization account); an install refuses, before writing anything, a label another plist holds
|
|
153
|
+
(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
|
|
154
|
+
names another `UserName` or another label; an uninstall refuses a label another plist holds before its `bootout`,
|
|
155
|
+
which would stop that plist's job, so pass it the label the plist at its path names.
|
|
156
|
+
- **Root installs and removes it.** `launchd_install` and `launchd_uninstall` in the system domain refuse, before writing
|
|
157
|
+
anything, in a process that is not root, and name the command to run as root (`launchctl bootstrap system <plist>`,
|
|
158
|
+
`launchctl bootout system/<label>`, removing the plist and `launchctl enable system/<label>`, as `launchd_uninstall`
|
|
159
|
+
does). `purge` is refused: the data belongs to the `UserName`, so remove it as that user. Upgrade is the same install
|
|
160
|
+
again as root, so a descriptor's `commands.update` for a LaunchDaemon runs as root, writes the new release (and any
|
|
161
|
+
descriptor it rewrites) as the `UserName` as an install does, and runs only the plist and `launchd_install` as root; a
|
|
162
|
+
host without root reports it as the operator's. A host's verbs need root too and keep LC-14 (off stays off, across a boot as well as a login): stop is
|
|
163
|
+
`bootout` + `disable system/<label>`, start is `enable` + `bootstrap system <plist>`, restart is
|
|
164
|
+
`kickstart -k system/<label>`; a host or broker without root reports the job read-only.
|
|
165
|
+
- **Status and pid read without root**: `launchd_loaded(label, "system")` and `launchd_pid(label, "system")` ask
|
|
166
|
+
`launchctl print system/<label>`, and `launchd_override(label, "system")` asks `launchctl print-disabled system`
|
|
167
|
+
(Node: the `domain: 'system'` option), both of which any account may read; the probe's `lifecycle.one-copy` reads the
|
|
168
|
+
domain from the descriptor's plist the same way.
|
|
169
|
+
- **The answer the installer waits for**: root reads the service's listener as another account's, so the installer takes
|
|
170
|
+
the job's pid from `launchctl print system/<label>` and reads the token-free well-known document only on a connection
|
|
171
|
+
that pid accepted, while the pid runs as the `UserName`'s uid (`ps`) and listens on exactly `127.0.0.1` (`lsof -p`);
|
|
172
|
+
an install refuses a `UserName` that is not a short name, no account of the Mac or root, and an origin other than
|
|
173
|
+
`http://127.0.0.1:<port>`, where nothing could prove it. Where the socket table is restricted it accepts a port that
|
|
174
|
+
pid listens on at exactly `127.0.0.1`, the weaker vouch SECURITY.md names (CT-261). A `ProgramArguments` wrapper that
|
|
175
|
+
starts the server as a child cannot pass either check, and the refusal says so: start the server with `exec`. An
|
|
176
|
+
install refused after its bootstrap leaves the job loaded, and its plist starts it again at boot; the error says so,
|
|
177
|
+
and as root `launchctl bootout system/<label>` and removing the plist (or `launchd_uninstall`) take it away. For hosts and the probe the domain comes from the plist's folder alone, so a LaunchDaemon's plist must live
|
|
178
|
+
in `/Library/LaunchDaemons` even when the installer is given `domain="system"`: the installer neither moves nor checks
|
|
179
|
+
it. A descriptor field for the domain is the contract's CT-234, open.
|
|
180
|
+
- **A contract exception, accepted in principle.** At the pin, HA-2 reads a launch daemon's listener as another
|
|
181
|
+
account's and nothing it answers as the service's; the contract's later RT-3 vouches only for the caller's own uid. The
|
|
182
|
+
root installer reads its own job's token-free well-known answer anyway, proved by launchd's pid, that pid's uid and its
|
|
183
|
+
bind on exactly `127.0.0.1` (`lsof -p`, never a wildcard), and sends no credential; under a restricted socket table the
|
|
184
|
+
first proof is the CT-261 weaker vouch (the pid listens on the port; that it accepted the connection is not shown). The contract owner accepted this in
|
|
185
|
+
principle as DEC-0161 (RT-3 extended to launchd's system domain, the restricted-table vouch included; proposed, its
|
|
186
|
+
text after its readings), so it stays a departure from the pin until the next repin (FAA-288).
|
|
187
|
+
Every other client keeps HA-2 as written.
|
|
188
|
+
- **Hosts.** A host or broker that reads launchd only in `gui/<uid>` sees a LaunchDaemon as not loaded and must not load
|
|
189
|
+
its plist into `gui/<uid>`. Fabric Dashboards from 0.8.39 takes the domain from the plist's folder and offers no verbs
|
|
190
|
+
for a system job; the lifecycle broker is still asked (FAA-287).
|
|
191
|
+
|
|
126
192
|
## One copy under a supervisor
|
|
127
193
|
|
|
128
194
|
Started by hand, a copy that finds `service.lock` held prints one sentence naming the
|
|
@@ -212,7 +212,9 @@ Loopback is one per computer, not one per OS account. Before a client sends any
|
|
|
212
212
|
`http://127.0.0.1:<port>` (a descriptor's token, a delegation token, a login code's request, a session cookie, a door's
|
|
213
213
|
token), and before a host calls a local service healthy, it tells the owner of the server end of its own connection
|
|
214
214
|
from the computer's socket table. A listener of another OS account, of root, SYSTEM or a service account, or one whose
|
|
215
|
-
owner cannot be told, is `foreign-user`: nothing is sent and nothing it answers is read as the service's.
|
|
215
|
+
owner cannot be told, is `foreign-user`: nothing is sent and nothing it answers is read as the service's. One exception:
|
|
216
|
+
a LaunchDaemon's root installer reads its own job's token-free well-known answer, proved by the job's pid, uid and exact
|
|
217
|
+
`127.0.0.1` bind (lifecycle.md, *The system domain*; DEC-0161 accepted in principle, proposed; FAA-288). The order of
|
|
216
218
|
a local service's states is `down`, `foreign-user`, `foreign` (`FAC-SEM-009`), then the well-known `status`; the
|
|
217
219
|
well-known `process.pid` is never proof (`docs/specification/service.md#ha-2` at the pin).
|
|
218
220
|
|
|
@@ -232,14 +234,14 @@ listener of this account's pid on the port counts as this account's (`CheckedHTT
|
|
|
232
234
|
`checkedSocket({vouchPid})`). The launchd installers vouch with their own job's pid; elsewhere a restricted table
|
|
233
235
|
refuses (the probe, the hub client and `fabric_http` included, FAA-272), and Apple's `/usr/bin/python3` sees the
|
|
234
236
|
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
|
|
235
|
-
IPv4 wildcard), not that it accepted this connection, so a more specific bind by root over a wildcard job would pass;
|
|
237
|
+
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;
|
|
236
238
|
the contract's ruling on vouching is CT-261.
|
|
237
239
|
|
|
238
240
|
Every kit client that sends a credential to loopback checks first: `check_service.py` (rule `listener.owner`, the first that reads the port for
|
|
239
241
|
a local placement; `FAIL` `foreign-user` and no rule reads the port), the hub client (`HubError` `foreign-user`),
|
|
240
242
|
`fabric_http` / `fabric-http.mjs` for a plain `http://` loopback URL (`HttpError` kind `foreign-user`, never retried),
|
|
241
243
|
`check_dashboard_layout.mjs` before a local login code, and the launchd installers' poll (a listener of another account
|
|
242
|
-
is never a successful install; the systemd and Task Scheduler installers do not poll yet, FAA-253). A running board
|
|
244
|
+
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
|
|
243
245
|
session reports a door another account holds as `board.down` with reason `unreachable` until FAA-252. On macOS the Node
|
|
244
246
|
twin reads the pid from `netstat` and the uid from `ps` a few milliseconds apart, so a pid reused in between would be
|
|
245
247
|
misread; the Python twin's `sysctl` reader takes both from one record. A service's own loopback server refusing another account's peer (HA-3) is FAA-227, and the port
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -79,6 +79,11 @@ watch the gate fail.
|
|
|
79
79
|
"used before honoured" write); memory works for one process.
|
|
80
80
|
- **Behind a router**: check the platform-set forwarded scheme; `Host` is preserved by common
|
|
81
81
|
platforms and must equal the origin's host.
|
|
82
|
+
- **A provider key** the remote service needs is issued and kept in Project Observatory, pushed to the platform, and
|
|
83
|
+
the movement recorded with
|
|
84
|
+
`python3 "$(project-observatory full-path)/tools/vault.py" moved <project> <env> <NAME> --how "<what moved, where, evidence>" --at <provider>`,
|
|
85
|
+
so the platform's copy is known rather than invisible. Without Project Observatory, record where the key lives in
|
|
86
|
+
the person's own secret manager.
|
|
82
87
|
|
|
83
88
|
## Checking it
|
|
84
89
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- [MCP for agents](#mcp-for-agents)
|
|
6
6
|
- [CLI for people and scripts](#cli-for-people-and-scripts)
|
|
7
7
|
- [A2A for remote agents](#a2a-for-remote-agents)
|
|
8
|
+
- [Behind tailscale serve](#behind-tailscale-serve)
|
|
8
9
|
- [Token](#token)
|
|
9
10
|
- [Dashboard session](#dashboard-session)
|
|
10
11
|
- [Headers every response carries](#headers-every-response-carries)
|
|
@@ -40,7 +41,9 @@
|
|
|
40
41
|
|
|
41
42
|
## CLI for people and scripts
|
|
42
43
|
|
|
43
|
-
`<tool> service status|start|stop|restart` (launchctl on the label
|
|
44
|
+
`<tool> service status|start|stop|restart` (launchctl on the label in the domain `launchd_domain(plist)` names; for a
|
|
45
|
+
LaunchDaemon start, stop and restart are root's, and a CLI without root reports the job read-only, lifecycle.md, The system
|
|
46
|
+
domain; then the
|
|
44
47
|
well-known document from this OS account's listener, else `foreign-user`: protocol.md, The listener's owner), `<tool> dashboard` (resolve the exact registered service with the `fabric-dashboards`
|
|
45
48
|
MCP, run `check_dashboard_link.py` on the action, then open its `open_link` with the host's `open` and
|
|
46
49
|
`fallback=never`; never `open <url>`, `open -a Safari`, `xdg-open` or `webbrowser.open`; an installed host that
|
|
@@ -82,6 +85,45 @@ A probe cannot see whether an interface would take the descriptor's token withou
|
|
|
82
85
|
forbids, so it reads the card only and never calls an interface: an API-key scheme in the descriptor's own
|
|
83
86
|
`auth.header` is a finding, and with `Authorization` (the default) the card cannot show it.
|
|
84
87
|
|
|
88
|
+
## Behind tailscale serve
|
|
89
|
+
|
|
90
|
+
An agent on a server (a VM, a teammate's machine) can reach people on the tailnet through `tailscale serve`, which
|
|
91
|
+
proxies HTTPS on the node's tailnet name to a loopback listener and adds the caller's `Tailscale-User-Login`. The
|
|
92
|
+
kit's `fabric_tailnet.py` / `fabric-tailnet.mjs` (FAA-297) turn that into a decision; the agent wires it into its own
|
|
93
|
+
handler.
|
|
94
|
+
|
|
95
|
+
- **A listener of serve's own.** Any process on the machine can send `Tailscale-User-Login` to a loopback port, so the
|
|
96
|
+
header is believed only on a listener that serve alone uses: a second loopback port, or, where the installed
|
|
97
|
+
`tailscale serve` can proxy to one, a Unix socket whose mode keeps other accounts out. Pass `listener="serve"` and the
|
|
98
|
+
connection's peer (`"unix"` for a socket). On any other listener a request carrying serve's headers is refused
|
|
99
|
+
(`serve-header-off-serve`); the Fabric listener keeps its session key and never reads them.
|
|
100
|
+
- **A perimeter generated, not kept by hand.** `load_perimeter` / `loadPerimeter` take a `tailnet-perimeter/1`
|
|
101
|
+
document: `generatedFrom {registry, revision, at}` (required: a file without its source is refused,
|
|
102
|
+
`perimeter-not-generated`) and `groups {name: {members, dashboards, capabilities}}`. The organization's generator
|
|
103
|
+
writes it from its access registry; the agent reloads it when the file changes.
|
|
104
|
+
- **A personal key for MCP.** `load_keys` / `loadKeys` take `[{id, login, sha256, capabilities}]`, the key's
|
|
105
|
+
`hash_key` / `hashKey` only. A call on the `mcp` surface needs `Authorization: Bearer <key>`; the key must belong to
|
|
106
|
+
the caller's login and its capabilities may not be wider than that login's groups.
|
|
107
|
+
- **The answer.** `authorize(listener=, peer=, headers=, perimeter=, surface="dashboard"|"mcp", target=, keys=)`
|
|
108
|
+
answers `{allowed, status, code, login, groups}` (and `key`, the key's id, on an allowed MCP call). Send the
|
|
109
|
+
status and the code as they are; no answer carries a key or its hash.
|
|
110
|
+
|
|
111
|
+
| Code | Status | When |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `serve-header-off-serve` | 403 | serve's headers on a listener that is not serve's |
|
|
114
|
+
| `peer-not-loopback` | 403 | the serve listener was reached from another address |
|
|
115
|
+
| `identity-missing`, `identity-ambiguous`, `identity-malformed` | 401 | no login, two, or not a login |
|
|
116
|
+
| `not-in-perimeter` | 403 | the login is in no group |
|
|
117
|
+
| `dashboard-not-granted` | 403 | no group of the login lists the dashboard |
|
|
118
|
+
| `key-required`, `key-unknown` | 401 | an MCP call without a bearer key, or with one not on the list |
|
|
119
|
+
| `key-not-yours` | 403 | the key belongs to another login |
|
|
120
|
+
| `key-wider-than-group` | 403 | the key's capabilities exceed the login's groups |
|
|
121
|
+
| `capability-not-granted` | 403 | the key does not carry the capability called |
|
|
122
|
+
|
|
123
|
+
A tagged node (a server, not a person) gets no identity header from serve and is refused `identity-missing`; a
|
|
124
|
+
machine caller uses the estate's delegation tokens instead. The serve listener is an outward surface: watch it like
|
|
125
|
+
the A2A tunnel above.
|
|
126
|
+
|
|
85
127
|
## Token
|
|
86
128
|
|
|
87
129
|
- Created once with `ensure_token` at first start, after the instance lock.
|