@passioncode-ai/fabric-agent-adapter 0.3.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +41 -5
  3. package/SECURITY.md +14 -0
  4. package/bin/fabric-agent-adapter.js +87 -43
  5. package/package.json +10 -5
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +2 -3
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +240 -0
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +32 -0
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +44 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +63 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +32 -0
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +83 -0
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +58 -0
  15. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +373 -0
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +380 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +663 -0
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +226 -0
  19. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +8 -1
@@ -0,0 +1,32 @@
1
+ # Dashboard principles
2
+
3
+ The dashboard is where the operator decides; it must stay truthful when parts of the
4
+ service are not.
5
+
6
+ 1. **Same origin, static assets.** HTML shell plus hashed `app.js`/`app.css` from a
7
+ whitelist; no inline script; a strict CSP. Nothing from a CDN — the page must work
8
+ offline on a plane.
9
+ 2. **State first, then detail.** The top of every page answers "is it working, and
10
+ does it need me?" — the same `status`, `degraded` and attention tiles the
11
+ well-known document publishes, so the page and Fabric Dashboards never disagree.
12
+ 3. **Calm refresh.** Poll the page's own API every 5 s while visible, back off when
13
+ hidden, and pause while a field is focused, a form has unsaved input or a dialog is
14
+ open. Replace a region only when its data changed; keep focus, scroll and open
15
+ sections.
16
+ 4. **Partial failure stays partial.** Load panels independently (`Promise.allSettled`);
17
+ a failed panel shows its own one-sentence error and a retry, and the rest render.
18
+ 5. **Every error is a sentence and an action.** "Store listing push failed: App Store
19
+ Connect rejected the key. Settings → Keys" — never a status code, a stack trace or a
20
+ blank area. Show when data was last fresh.
21
+ 6. **Server down is a state, not a crash.** When the API stops answering, keep the
22
+ last data greyed with "Service not answering since 17:02", and recover by itself
23
+ when it returns.
24
+ 7. **Approvals show the diff and its hash.** Any write the operator approves shows
25
+ before → after and the hash the service will verify at apply time.
26
+ 8. **Theme.** Follow the operator's product design system (PassionCode.ai tokens for
27
+ Fabric-family services); respect `prefers-color-scheme` unless the product fixes a
28
+ theme; never encode meaning by colour alone.
29
+ 9. **Links into the page are stable.** Event `link`s point at hash routes that survive
30
+ a reload (`/dashboard#/approvals/77`) so a notification opens the exact item.
31
+ 10. **No second browser tab required.** With Fabric Dashboards installed, the page is
32
+ shown inside it; do not open windows or tabs on your own — use in-page dialogs.
@@ -0,0 +1,44 @@
1
+ # Events and notifications
2
+
3
+ ## Map the log you already keep
4
+
5
+ | Existing log | View |
6
+ |---|---|
7
+ | a SQL `events`/`jobs` table with an autoincrement id | `SELECT … WHERE id > ? ORDER BY id LIMIT ?`; `id` as string |
8
+ | per-job JSONL files | keep one append-only `activity.jsonl` index written beside them, or merge by `(ts, job, seq)` into a composite id `"<ts>-<job>-<seq>"` that still sorts |
9
+ | a journal with no id | add a monotonic counter when appending; backfill ids once on migration |
10
+ | nothing | `JsonlEventLog` from the kit |
11
+
12
+ The feed is a view: never copy rows into a second store that can drift from the first.
13
+
14
+ ## Writing an event
15
+
16
+ - `kind`: dotted, stable, past tense or state (`job.failed`, `job.awaiting_choice`,
17
+ `service.started`, `collector.stale`). Hosts filter on it.
18
+ - `level`: `info` (routine), `notice` (the operator may want to look), `warning`
19
+ (something is degrading), `error` (something failed and needs action).
20
+ - `text`: one sentence, in the operator's language, with the object named and the
21
+ number stated — "The Q3 report draft is ready for your approval (12 sections, 3
22
+ languages)." Never a machine id, never a stack trace.
23
+ - `subject`: what it is about (`{type: "report", id: "q3", label: "Q3 report"}`), so a host
24
+ can group.
25
+ - `link`: the page that resolves it.
26
+
27
+ ## When to set `notify: true`
28
+
29
+ Notify when the operator must act (an approval is waiting, a key expired, a job failed
30
+ after retries) or asked to be told (a long job finished). Do not notify for routine
31
+ progress, retries that will succeed, or anything that fires more than a few times an
32
+ hour. The host decides whether to show it — quiet hours, per-service settings — and
33
+ debounces; the service decides only what is notification-worthy.
34
+
35
+ ## Lifecycle events every service emits
36
+
37
+ `service.started` (with the build), `service.stopping`, `service.degraded` and
38
+ `service.recovered` when a degraded source appears or clears, `update.available` when
39
+ the service learns of a newer release.
40
+
41
+ ## Retention
42
+
43
+ At least seven days or 1000 events, whichever is more; trim oldest first; a cursor
44
+ older than retention returns the oldest retained page, not an error.
@@ -0,0 +1,63 @@
1
+ # Lifecycle — supervisor, install, release layout, state
2
+
3
+ ## launchd plist (generate it with `launchd_plist`)
4
+
5
+ | Key | Value | Why |
6
+ |---|---|---|
7
+ | `Label` | reverse-DNS, stable (`com.example.example-agent`) | the host controls the job by label |
8
+ | `ProgramArguments` | absolute interpreter + module/script inside a **release** directory | a worktree or checkout path breaks the day it moves |
9
+ | `RunAtLoad` | `true` | back at every login |
10
+ | `KeepAlive` | `true` | `{SuccessfulExit:false}` leaves a cleanly exited service down |
11
+ | `ThrottleInterval` | `10` | bounds a crash loop |
12
+ | `ExitTimeOut` | drain time + margin (40 s default) | SIGKILL arrives after it |
13
+ | `ProcessType` | `Background` | scheduler hint |
14
+ | `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets | never a secret value — the plist is world-readable in backups |
15
+ | `StandardOutPath`/`StandardErrorPath` | `~/Library/Logs/<id>/service.log` | one place the host tails |
16
+
17
+ ## Install, upgrade, uninstall
18
+
19
+ 1. Build or unpack the release into `~/.local/share/<id>/releases/<version>-<sha12>/`
20
+ (a venv or `node_modules` inside it). Never edit a release after it is written.
21
+ 2. `write_descriptor` (port and id claims are checked here).
22
+ 3. `launchd_install`: write + `plutil -lint`, `enable`, `bootout` and wait until
23
+ `launchctl print gui/<uid>/<label>` fails, `bootstrap` with up to five retries,
24
+ then poll the well-known document until it answers with this `id.instance`.
25
+ 4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
26
+ again. Keep the previous release for rollback; prune older ones by count.
27
+ 5. Uninstall = `launchd_uninstall` + `remove_descriptor`. Data stays.
28
+
29
+ A host stops a service with `bootout` **and** `disable` (off survives a login) and
30
+ starts it with `enable` + `bootstrap`; restart is `kickstart -k`. The service never
31
+ implements these verbs itself — its CLI calls launchctl the same way.
32
+
33
+ ## Directories
34
+
35
+ | What | macOS | Linux |
36
+ |---|---|---|
37
+ | data + config | `~/Library/Application Support/<id>/` | `${XDG_DATA_HOME:-~/.local/share}/<id>/` |
38
+ | logs | `~/Library/Logs/<id>/` | `${XDG_STATE_HOME:-~/.local/state}/<id>/logs/` |
39
+ | cache (deletable any time) | `~/Library/Caches/<id>/` | `${XDG_CACHE_HOME:-~/.cache}/<id>/` |
40
+ | code | `~/.local/share/<id>/releases/…` | same |
41
+
42
+ A repository that versions the data itself (a registry, a plan) may hold the data; the
43
+ service's own code checkout and its releases may not. Directories 0700, secret files 0600. A store carries a schema version and the service
44
+ refuses to open a store newer than its code. The cache holds only what can be rebuilt;
45
+ anything the operator would miss belongs in data.
46
+
47
+ ## Logs
48
+
49
+ One structured log (JSON lines) rotated by size (5 × 5 MB is a sane default) plus the
50
+ launchd stdout file, truncated at start. Configure logging once, in the entry point.
51
+ Never log a token, a cookie, a login code or a request body that may carry one.
52
+
53
+ ## Shutdown
54
+
55
+ On `SIGTERM`: stop accepting new work, append a `service.stopping` event, drain
56
+ in-flight work until `ExitTimeOut` minus a margin, persist what was interrupted so the
57
+ next start resumes it, release the lock, exit 0.
58
+
59
+ ## Linux
60
+
61
+ Until a systemd adapter exists, declare `lifecycle.manager: "none"`. A `systemd --user`
62
+ unit with `Restart=always` gives the same guarantees; the host will control it once the
63
+ adapter ships. Everything else in this skill applies unchanged.
@@ -0,0 +1,32 @@
1
+ # Migrating an existing service
2
+
3
+ Order matters: each step leaves the service answering.
4
+
5
+ 1. **Measure first.** Run `check_service.py --descriptor <draft>` against a descriptor
6
+ written by hand into a temporary `--services-dir`; keep the `FAIL` list as the
7
+ migration's checklist.
8
+ 2. **Add the well-known route and the build identity.** Read-only, no risk.
9
+ 3. **Add the events view** over the existing log, then the login-code routes. Keep the
10
+ service's old login path until the operator has switched.
11
+ 4. **Move the lock to the top of startup** — before the startup hook that resumes jobs.
12
+ Test: a second copy exits 75 and the first copy's running jobs are untouched.
13
+ 5. **Move state out of the code tree** if it lives there: copy to the OS data
14
+ directory, switch the paths, keep the old tree read-only for one release, then
15
+ delete it with the operator's consent.
16
+ 6. **Regenerate the plist** from `launchd_plist`, pointing at a release; delete stray
17
+ `.plist.bak` files from `~/Library/LaunchAgents` after moving them elsewhere.
18
+ 7. **Write the descriptor** from the installer; resolve any port claim it refuses by
19
+ moving the newcomer, never the established service.
20
+ 8. **Re-run the probe** until nothing `FAIL`s; record the table in the service's
21
+ verification document.
22
+
23
+ ## Gaps that existing services usually have
24
+
25
+ | Gap | What fixes it |
26
+ |---|---|
27
+ | A health route that answers `{"ok": true}` with no identity | the well-known document with `service.id`, `instance`, build and pid |
28
+ | The port bind treated as the single-instance guarantee, with a startup hook that runs first | the lock at the top of startup; a second copy exits 75 |
29
+ | Data, logs or caches inside the repository | the OS data, log and cache directories; the old tree read-only for one release |
30
+ | `KeepAlive {SuccessfulExit:false}`, a plist pointing at a checkout or a worktree | a plist generated by `launchd_plist`, pointing at a release |
31
+ | A port another service already uses by default | the port claim at install; move the newcomer |
32
+ | An event log in a private shape | the events view over it, with one sentence per event |
@@ -0,0 +1,83 @@
1
+ # fabric-service/0.1 — wire reference
2
+
3
+ Pinned to `fabric-agent-contract` commit `a5a27092ba0dcc5facfbeae8b359146dfb403e9a`
4
+ (`docs/specification/service.md`, DEC-0015). The contract's schemas are normative;
5
+ this page is the working summary. Extension key:
6
+ `https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
7
+
8
+ ## Descriptor — written by the installer
9
+
10
+ Directory: macOS `~/Library/Application Support/ai.passioncode.fabric/services/`,
11
+ Linux `${XDG_DATA_HOME:-~/.local/share}/passioncode-fabric/services/`, or
12
+ `FABRIC_SERVICES_DIR`. File `<id>.<instance>.json`, mode 0600, atomic.
13
+
14
+ | Field | Rule |
15
+ |---|---|
16
+ | `protocol` | `"fabric-service/0.1"` |
17
+ | `id`, `instance` | `^[a-z][a-z0-9-]{1,62}$`, `^[a-z][a-z0-9-]{0,31}$` (default `default`); the pair is unique per machine |
18
+ | `name`, `summary` | ≤ 80 and ≤ 200 characters |
19
+ | `origin` | `http://127.0.0.1:<port>`; the port is a machine-wide claim |
20
+ | `auth` | `tokenFile` (0600); `header` default `Authorization` with `scheme` `Bearer`; any other header carries the raw token with `scheme: "none"` |
21
+ | `lifecycle` | `manager` `launchd` (then `label` and `plist`) or `none` |
22
+ | `paths` | `data` (required), `logs[]`, optional `config`, `cache` |
23
+ | `commands` | only `doctor` and `update`, argument arrays whose first item is an absolute or `~/` path |
24
+ | `source.repository`, `fabricManifest` | optional |
25
+ | `installedAt`, `installedBy` | when and by which installer version |
26
+
27
+ ## Well-known document — `GET /.well-known/fabric-service`
28
+
29
+ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
30
+
31
+ ```json
32
+ {
33
+ "protocol": "fabric-service/0.1",
34
+ "service": { "id": "example-agent", "instance": "default", "name": "Example Agent",
35
+ "version": "0.2.0", "build": { "commit": "8b80be9", "dirty": false, "builtAt": "2026-09-28T17:40:00Z" } },
36
+ "process": { "pid": 58090, "startedAt": "2026-09-28T17:41:02Z" },
37
+ "status": "degraded",
38
+ "degraded": [{ "source": "llm", "reason": "No model key: drafting is paused." }],
39
+ "summary": [{ "label": "Jobs running", "value": 2 }, { "label": "Awaiting you", "value": 1, "attention": true }],
40
+ "surfaces": { "dashboard": { "path": "/dashboard", "login": true },
41
+ "mcp": { "path": "/mcp", "transport": "streamable-http" },
42
+ "events": { "path": "/fabric/v1/events" } },
43
+ "update": { "available": null }
44
+ }
45
+ ```
46
+
47
+ - `status`: `starting | ready | degraded | stopping`. No answer means `down`.
48
+ - `build` needs `commit` or `digest` (`sha256:<64 hex>`).
49
+ - `summary`: at most six tiles; `attention: true` counts toward the host's badge.
50
+ - `surfaces.events` is required; `dashboard` and `mcp` when the service has them.
51
+
52
+ ## Events page — `GET /fabric/v1/events?after=<cursor>&limit=<n>`
53
+
54
+ Token required. `limit` default 50, maximum 200. Without `after`: the newest `limit`,
55
+ ascending. `cursor` is the last id returned, or the `after` you sent when the page is
56
+ empty, or `null` for an empty log. Retain at least seven days or 1000 events.
57
+
58
+ ```json
59
+ { "events": [ { "id": "4213", "at": "2026-09-28T17:55:10Z", "kind": "job.awaiting_choice",
60
+ "level": "notice", "text": "The Q3 report draft is ready for your approval.",
61
+ "subject": { "type": "report", "id": "q3", "label": "Q3 report" },
62
+ "link": "/dashboard#/approvals/77", "notify": true } ],
63
+ "cursor": "4213" }
64
+ ```
65
+
66
+ `kind` is dotted lowercase; `level` is `info | notice | warning | error`; `link` is a
67
+ path on the service origin, never an absolute URL. Optional SSE stream at
68
+ `surfaces.events.stream` carrying the same objects.
69
+
70
+ ## Login code — `POST /fabric/v1/login-code`
71
+
72
+ Token required → `{ "url": "/fabric/v1/login?code=<16–256 url-safe chars>", "expiresAt": "…" }`,
73
+ single use, at most 120 s, recorded as used before it is honoured. `GET` that URL →
74
+ `Set-Cookie` `HttpOnly; SameSite=Strict`, `302` to `surfaces.dashboard.path`.
75
+
76
+ ## Semantic rules a host applies
77
+
78
+ | Code | Rule |
79
+ |---|---|
80
+ | `FAC-SEM-009` | well-known `service.id.instance` equals the descriptor's, else the port holder is `foreign` |
81
+ | `FAC-SEM-010` | no two descriptors claim one port or one `id.instance` |
82
+ | `FAC-SEM-011` | `ready` carries no degraded source |
83
+ | `FAC-SEM-012` | commands start with an absolute or `~/` executable |
@@ -0,0 +1,58 @@
1
+ # Surfaces and auth
2
+
3
+ ## MCP for agents
4
+
5
+ - One server inside the service process, `streamable-http`, at `/mcp` on the service
6
+ origin, stateless JSON responses unless the tools stream.
7
+ - Authenticate with the service token in the declared header. OAuth is for remote
8
+ multi-user servers, not for a loopback service.
9
+ - Register it into a client config with the service's own command
10
+ (`<tool> mcp-register`), which writes `{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{...}}`
11
+ atomically, aborts if the file changed meanwhile, and keeps one 0600 backup. The token
12
+ goes in `headers`, never in the URL — every log that records a URL prints its query
13
+ string.
14
+ - Ship server `instructions` that tell a caller where to start and that every answer
15
+ carries `degraded`.
16
+
17
+ ## CLI for people and scripts
18
+
19
+ `<tool> service status|start|stop|restart` (launchctl on the label, then the
20
+ well-known document), `<tool> dashboard` (asks for a login code and opens it; with
21
+ Fabric Dashboards installed it opens the service there instead), `<tool> doctor --json`
22
+ (the same checks the probe runs, plus provider keys), `--json` on every command. Print
23
+ a sign-in link only when stdout is a terminal.
24
+
25
+ ## A2A for remote agents
26
+
27
+ Remote reach goes through A2A `1.0` over HTTPS or an authenticated gateway in front of
28
+ MCP — never by binding a LAN address. A tunnel (Tailscale Serve and the like) is an
29
+ outward surface with its own token lifetime; watch it like a service, because nothing
30
+ else will notice it expire.
31
+
32
+ ## Token
33
+
34
+ - Created once with `ensure_token` at first start, after the instance lock.
35
+ - Stored at the descriptor's `auth.tokenFile`, 0600, owner-checked, never a symlink.
36
+ - Compared in constant time (`token_matches`).
37
+ - Rotation is an operator act: write a new file, restart, re-run `mcp-register`.
38
+ - Same-user processes can read it. The protocol defends against web pages and
39
+ mistakes, not against hostile code running as the operator — say so in SECURITY.md.
40
+
41
+ ## Dashboard session
42
+
43
+ - `POST /fabric/v1/login-code` (token) → single-use code, at most 120 s, persisted as
44
+ used before it is honoured, burned even when expired.
45
+ - `GET /fabric/v1/login?code=` → `HttpOnly; SameSite=Strict` cookie, 302 to the
46
+ dashboard. Sessions are HMAC-signed; `revoke_all` rotates the key and ends every
47
+ session at once.
48
+ - Browser writes need the cookie **and** a custom header such as `X-<Tool>-Request: 1`,
49
+ which forces a preflight the service never answers.
50
+ - A dashboard that is deliberately read-open to local pages declares
51
+ `"login": false`; its writes still need the header.
52
+
53
+ ## Headers every response carries
54
+
55
+ `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, a CSP with no inline
56
+ script, and `frame-ancestors 'self'` unless the page must never be embedded. Fabric
57
+ Dashboards shows pages in a native view, not a frame, so `X-Frame-Options: DENY` does
58
+ not stop it.