@passioncode-ai/fabric-agent-adapter 0.77.0 → 0.79.1
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 +207 -0
- package/README.md +31 -11
- 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/hooks/hooks.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +15 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +37 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +13 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +2 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +14 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +27 -39
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +34 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +8 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +4 -1
- 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 +74 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +8 -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-base.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +30 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +54 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +339 -26
- 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_base.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +13 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +14 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +41 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +367 -27
- 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 +323 -25
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +19 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +37 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +6 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +13 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +6 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/when-to-use.md +5 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -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 +30 -39
- 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/on-demand-session.md +57 -37
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +50 -25
- 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_cli.py +26 -10
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +11 -5
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py
CHANGED
|
@@ -61,8 +61,12 @@ _URL = re.compile(URL_PATTERN)
|
|
|
61
61
|
_MANIFEST = re.compile(MANIFEST_PATTERN[:-1] + r"\Z")
|
|
62
62
|
# The manifest's name after a backslash: valid since DEC-0145, but no writer emits it until every reader accepts it.
|
|
63
63
|
_MANIFEST_AFTER_BACKSLASH = re.compile(r"\\fabric-agent\.json\Z")
|
|
64
|
-
# format: uri (RFC 3986 absolute URI)
|
|
65
|
-
|
|
64
|
+
# format: uri (RFC 3986 absolute URI) as ajv-formats reads it: RFC 3986 characters only (unreserved, reserved and %HH),
|
|
65
|
+
# the class the service kit's descriptor check uses (fabric_descriptor `_SOURCE_URI`, 0.49.3); a byte-order mark, a
|
|
66
|
+
# zero-width space, a non-ASCII path or an IDN host is refused (FAA-127).
|
|
67
|
+
_URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:(?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+\Z")
|
|
68
|
+
# Brackets belong to an IP-literal host only (RFC 3986 3.2.2); the literal is set aside before the character check.
|
|
69
|
+
_IP_LITERAL = re.compile(r"^([A-Za-z][A-Za-z0-9+.-]*://(?:[^/?#@\[\]]*@)?)\[[0-9A-Fa-f:.vV]+\]")
|
|
66
70
|
# format: date-time (RFC 3339).
|
|
67
71
|
_TIMESTAMP = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})\Z")
|
|
68
72
|
FIELDS = {"protocol", "id", "providerId", "name", "summary", "manifest", "run", "source", "installedAt", "installedBy", "extensions"}
|
|
@@ -145,11 +149,14 @@ def validate_provider_entry(entry: Any) -> List[str]:
|
|
|
145
149
|
source = entry["source"]
|
|
146
150
|
if not isinstance(source, dict) or not set(source) <= {"repository"}:
|
|
147
151
|
problems.append("source is {repository?}")
|
|
152
|
+
elif "repository" in source and isinstance(source["repository"], str) and (
|
|
153
|
+
_URI_PASSWORD.match(source["repository"]) or _secrets.repository_credential(source["repository"])):
|
|
154
|
+
# FAA-120, as the service kit: a repository is named, never reached with a credential in its URI. Said
|
|
155
|
+
# before the character check, so a password beside a character the URI class refuses (a backslash, as
|
|
156
|
+
# git reads one) is still named a credential (FAA-127).
|
|
157
|
+
problems.append("source.repository carries no password or credential: it names the repository")
|
|
148
158
|
elif "repository" in source and not _is_uri(source["repository"]):
|
|
149
159
|
problems.append("source.repository is an absolute URI")
|
|
150
|
-
elif "repository" in source and (_URI_PASSWORD.match(source["repository"]) or _secrets.repository_credential(source["repository"])):
|
|
151
|
-
# FAA-120, as the service kit: a repository is named, never reached with a credential in its URI.
|
|
152
|
-
problems.append("source.repository carries no password or credential: it names the repository")
|
|
153
160
|
if not isinstance(entry["installedAt"], str) or not _TIMESTAMP.fullmatch(entry["installedAt"]):
|
|
154
161
|
problems.append("installedAt is an RFC 3339 date-time")
|
|
155
162
|
if not isinstance(entry["installedBy"], str) or not 1 <= len(entry["installedBy"]) <= 200:
|
|
@@ -161,7 +168,7 @@ def validate_provider_entry(entry: Any) -> List[str]:
|
|
|
161
168
|
|
|
162
169
|
|
|
163
170
|
def _is_uri(value: Any) -> bool:
|
|
164
|
-
return isinstance(value, str) and bool(_URI.fullmatch(value))
|
|
171
|
+
return isinstance(value, str) and bool(_URI.fullmatch(_IP_LITERAL.sub(r"\1h", value, count=1)))
|
|
165
172
|
|
|
166
173
|
|
|
167
174
|
def _run_problems(run: Any) -> List[str]:
|
|
@@ -255,7 +262,7 @@ def write_provider_entry(entry: Dict[str, Any], directory: Optional[Path] = None
|
|
|
255
262
|
# re-pins (CT-192), so the writer does not emit it yet.
|
|
256
263
|
raise ProviderError("The manifest's name follows a backslash, which a reader that has not re-pinned to DEC-0145 refuses (CT-192). "
|
|
257
264
|
"Write the path with / before the name: ~/.../fabric-agent.json, which every Fabric release reads, or "
|
|
258
|
-
"C:/.../fabric-agent.json, which Fabric reads only
|
|
265
|
+
"C:/.../fabric-agent.json, which Fabric reads only from v0.3.16 (its repin, CO-479).")
|
|
259
266
|
problems = manifest_problems(entry)
|
|
260
267
|
if problems:
|
|
261
268
|
raise ProviderError("Provider entry is invalid: %s." % "; ".join(problems))
|
|
@@ -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.79.1"
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.6 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
|
|
2
2
|
Load it same-origin, after fabric-dashboard.css, with the service it opens Fabric on:
|
|
3
3
|
<script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
|
|
4
4
|
data-project="project-a" data-project-name="Project A"></script>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
(function (root) {
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
-
var VERSION = '1.1.
|
|
14
|
+
var VERSION = '1.1.6';
|
|
15
15
|
var COOKIE = 'fabric_button';
|
|
16
16
|
var GET_FABRIC = 'https://passioncode.ai/fabric';
|
|
17
17
|
var ABSENT_AFTER_MS = 1500;
|
|
@@ -69,7 +69,10 @@
|
|
|
69
69
|
if (piece === '') return;
|
|
70
70
|
var at = piece.indexOf('=');
|
|
71
71
|
var name = firstKey(at < 0 ? piece : piece.slice(0, at));
|
|
72
|
-
|
|
72
|
+
var value = at < 0 ? '' : decodeKeepingPlus(piece.slice(at + 1));
|
|
73
|
+
var list = values.get(name);
|
|
74
|
+
if (list) list.push(value); // appended in place, as the contract's secret-shape.ts does (FAA-245)
|
|
75
|
+
else values.set(name, [value]);
|
|
73
76
|
});
|
|
74
77
|
return values;
|
|
75
78
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.6 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
|
|
2
2
|
A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
|
|
3
3
|
reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
|
|
4
4
|
Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* fabric-dashboard-kit@1.1.
|
|
1
|
+
/* fabric-dashboard-kit@1.1.6 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
|
|
2
2
|
Load it same-origin with <script src="fabric-dashboard.js" defer></script>: no inline script, no
|
|
3
3
|
eval, nothing from a CDN. It applies the host's appearance (ED-6, ED-8), folds navigation that
|
|
4
4
|
does not fit into a More menu (ED-3), makes wide boxes keyboard-reachable (ED-4) and keeps
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
var KIT = 'fabric-dashboard-kit';
|
|
11
|
-
var VERSION = '1.1.
|
|
11
|
+
var VERSION = '1.1.6';
|
|
12
12
|
/* fabric-dashboard-kit:theme-ids:begin */
|
|
13
13
|
var THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
14
14
|
/* fabric-dashboard-kit:theme-ids:end */
|
|
@@ -148,6 +148,20 @@
|
|
|
148
148
|
root.addEventListener('focus', applyAppearance);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/* ---- resize observers write in the next frame (since 1.1.6) ----
|
|
152
|
+
A ResizeObserver callback that changes the size of what it watches, or of an ancestor, makes WebKit raise
|
|
153
|
+
"ResizeObserver loop completed with undelivered notifications". So a callback only schedules the work for the next
|
|
154
|
+
animation frame, cancelling one already scheduled, and the work itself skips a size it has already laid out. */
|
|
155
|
+
function nextFrame(work) {
|
|
156
|
+
var pending = null;
|
|
157
|
+
var raf = root.requestAnimationFrame;
|
|
158
|
+
return function () {
|
|
159
|
+
if (typeof raf !== 'function') { work(); return; }
|
|
160
|
+
if (pending !== null) root.cancelAnimationFrame(pending);
|
|
161
|
+
pending = raf.call(root, function () { pending = null; work(); });
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
/* ---- navigation: fold what does not fit into More (ED-3) ---- */
|
|
152
166
|
function foldNav(nav) {
|
|
153
167
|
if (typeof nav.fdRefresh === 'function') { nav.fdRefresh(); return; } // folded already: refit, no second observers
|
|
@@ -212,8 +226,16 @@
|
|
|
212
226
|
}
|
|
213
227
|
}
|
|
214
228
|
layout();
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
// The fit depends on the nav's width only: a callback for a height the fold itself changed lays nothing out.
|
|
230
|
+
var laidOutWidth = null;
|
|
231
|
+
var refit = nextFrame(function () {
|
|
232
|
+
var width = nav.clientWidth;
|
|
233
|
+
if (width === laidOutWidth) return;
|
|
234
|
+
laidOutWidth = width;
|
|
235
|
+
layout();
|
|
236
|
+
});
|
|
237
|
+
if (typeof root.ResizeObserver === 'function') new root.ResizeObserver(refit).observe(nav);
|
|
238
|
+
else root.addEventListener('resize', refit);
|
|
217
239
|
// A page that moves aria-current (its route changed) gets a fresh summary and fit, without telling the kit.
|
|
218
240
|
if (typeof root.MutationObserver === 'function') {
|
|
219
241
|
new root.MutationObserver(layout).observe(nav, { subtree: true, attributes: true, attributeFilter: ['aria-current'] });
|
|
@@ -255,12 +277,16 @@
|
|
|
255
277
|
var header = chrome.querySelector('.fd-header');
|
|
256
278
|
var lift = 0;
|
|
257
279
|
if (height > limit && header) lift = Math.min(Math.ceil(header.getBoundingClientRect().height), height);
|
|
258
|
-
|
|
259
|
-
|
|
280
|
+
var top = lift ? -lift + 'px' : '0px';
|
|
281
|
+
var pinned = Math.max(0, height - lift) + 'px';
|
|
282
|
+
// An unchanged value is not written again: a write restyles the page and can wake the observer once more.
|
|
283
|
+
if (html.style.getPropertyValue('--fd-chrome-top') !== top) html.style.setProperty('--fd-chrome-top', top);
|
|
284
|
+
if (html.style.getPropertyValue('--fd-chrome-h') !== pinned) html.style.setProperty('--fd-chrome-h', pinned);
|
|
260
285
|
}
|
|
261
286
|
set();
|
|
262
|
-
|
|
263
|
-
root.
|
|
287
|
+
var reset = nextFrame(set);
|
|
288
|
+
if (typeof root.ResizeObserver === 'function') new root.ResizeObserver(reset).observe(chrome);
|
|
289
|
+
root.addEventListener('resize', reset);
|
|
264
290
|
}
|
|
265
291
|
|
|
266
292
|
function init() {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-dashboard-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"profile": "fabric-dashboard/0.1",
|
|
5
5
|
"decision": "DEC-0036",
|
|
6
6
|
"declarationKey": "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"contrast"
|
|
16
16
|
],
|
|
17
17
|
"files": {
|
|
18
|
-
"fabric-dashboard.css": "
|
|
19
|
-
"fabric-dashboard.js": "
|
|
20
|
-
"fabric-button.js": "
|
|
18
|
+
"fabric-dashboard.css": "7a90bbe1f1a2170df5dac0f596a3f302167bef9a7243457bb21dd0eb34f4d359",
|
|
19
|
+
"fabric-dashboard.js": "146ec9a0fd50af2df3245c02ec7dfba75b7d4e8cb5476fedb5ef727869fef1cc",
|
|
20
|
+
"fabric-button.js": "60c2b6d44571213e0237c29a94a4cf220f9de3efcfa697ac6a33777d1ad25fd6",
|
|
21
21
|
"themes.json": "511839f1a1a83eb37fadfd132791474497d5c28c5091700b9aa31ad076e359b5"
|
|
22
22
|
},
|
|
23
23
|
"history": {
|
|
@@ -54,6 +54,11 @@
|
|
|
54
54
|
"fabric-dashboard.css": "cc7a96fa2c5eeb03d113e7a543ed88d43ce78c2808c411de199ea0fc6d55b957",
|
|
55
55
|
"fabric-dashboard.js": "5df01193c590a1d8aa82a521954902fecf2d7f655df482165f90d7291b00087f",
|
|
56
56
|
"fabric-button.js": "e945b3252f9a246826d06dbe7bd4be66e82d539a33728c3d3043fd61d4cbd681"
|
|
57
|
+
},
|
|
58
|
+
"1.1.6": {
|
|
59
|
+
"fabric-dashboard.css": "7a90bbe1f1a2170df5dac0f596a3f302167bef9a7243457bb21dd0eb34f4d359",
|
|
60
|
+
"fabric-dashboard.js": "146ec9a0fd50af2df3245c02ec7dfba75b7d4e8cb5476fedb5ef727869fef1cc",
|
|
61
|
+
"fabric-button.js": "60c2b6d44571213e0237c29a94a4cf220f9de3efcfa697ac6a33777d1ad25fd6"
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -26,8 +26,10 @@ skill's business (working-with-fabric, `references/project-board.md`).
|
|
|
26
26
|
|
|
27
27
|
A **request** is addressed to a Project and one of its capabilities (`video.render`), never to a service. One
|
|
28
28
|
**responder slot** exists per Project and capability; a service holds it under a **`board.respond` grant** the
|
|
29
|
-
operator gives in Fabric (
|
|
30
|
-
`board:<project id>/<capability>`. A pid, a descriptor or a localhost origin authorizes nothing.
|
|
29
|
+
operator gives in Fabric (Settings → Agent access): callee `fabric`, capability `board.respond`, resource
|
|
30
|
+
`board:<project id>/<capability>`. A pid, a descriptor or a localhost origin authorizes nothing. The capability is
|
|
31
|
+
never `project.work`: DEC-0140 PR-1 reserves it for a Project's primary responder, a coding session, and `Responder`
|
|
32
|
+
refuses it with `invalid_arguments` (FAA-244).
|
|
31
33
|
|
|
32
34
|
1. Ask once, through the hub's door: `HubClient().request_board_access(agent_id, project_id, [capability], reason)`
|
|
33
35
|
(`requestBoardAccess`). Keep the answer's `pollSecret` in memory or a 0600 file.
|
|
@@ -37,8 +39,9 @@ operator gives in Fabric (Agents & access): callee `fabric`, capability `board.r
|
|
|
37
39
|
3. Each connection joins with `fabric.board.join {projectId}` (`join_board` / `joinBoard`). It re-reads `hub.json`,
|
|
38
40
|
checks the pid it names is alive, tells the owner of the door's listener from the socket table (DEC-0154 HA-2:
|
|
39
41
|
another OS account's process, or one whose owner cannot be told, is `HubError` code `foreign-user`, retryable, with
|
|
40
|
-
nothing sent; a retry never clears a restricted macOS table, `table-restricted` in protocol.md, FAA-272), and only then sends the credential (a session already running
|
|
41
|
-
|
|
42
|
+
nothing sent; a retry never clears a restricted macOS table, `table-restricted` in protocol.md, FAA-272), and only then sends the credential (a session already running carries that refusal as `BoardError` code
|
|
43
|
+
`foreign-user`, one of `SESSION_CODES`, to `board.down` with reason `foreign-user` and a sentence that says nothing
|
|
44
|
+
was sent, never `unreachable`'s "a write may have been stored", FAA-252). A missing grant is `HubError` code
|
|
42
45
|
`not_authorized` whose `data.ask` holds the arguments to ask with.
|
|
43
46
|
|
|
44
47
|
## Wiring it into the service
|
|
@@ -302,7 +305,7 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
302
305
|
after the slot had moved past the record, reads as a move: the safe side; a slot such a replay took is left to lapse
|
|
303
306
|
unused. A generation written `2.0` is `2`.
|
|
304
307
|
- The operator may move the slot back to this session in Fabric (DEC-0140 PR-2), which this client cannot hear. No
|
|
305
|
-
Fabric release offers the operator's move
|
|
308
|
+
Fabric release through v0.3.19 (2026-11-11) offers the operator's move (the control is CT-174's in Fabric, its agent twin CT-181 in the contract, then CT-125),
|
|
306
309
|
so today every move is another session taking the slot and take-back is the route back; the check below is ready
|
|
307
310
|
for the move. While
|
|
308
311
|
a slot is moved the responder reads `com.status` once a renewal (20 s) and, when the slot's generation is past the
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md
CHANGED
|
@@ -81,7 +81,7 @@ const answer = await client.chat(messages, { model, fallback, maxTokens: 800 });
|
|
|
81
81
|
| Receipts | every status-200 answer records one, and so does an attempt that may have been billed without an answer to read (a timeout, a cut or refused connection, a 200 over the size cap or not JSON: no tokens, `costUsd: null`, `tokensMeasured: false`; over-counting a refused connection is the safe side): the served model, the provider's tokens (cache reads and writes kept apart), `costUsd` from `usage.cost`, else from the price list (`costBasis: price-list`), else `null` with `tokensMeasured: false`; `route: key`, the client's `project_id` and the call's `task_run_id`. A request the provider answered with a 4xx or 5xx records nothing: it was not billed |
|
|
82
82
|
| What is an answer | `finish_reason` `stop` or `tool_calls` with content or tool calls; `error` is retried on the same model; `length` (the budget ran out, reasoning included), `content_filter` and empty content go to the fallback |
|
|
83
83
|
| Retries | 408, 425, 429, 500, 502, 503, 504 and connection errors on the same model, `retries` times (default 1), `Retry-After` honoured, within one deadline (`deadline_s` 120) for the whole call |
|
|
84
|
-
| Stop at once | 401, 402 and 403 (kinds `auth`, `payment`): a key, credit or permission problem is the same on every model |
|
|
84
|
+
| Stop at once | 401, 402 and 403 (kinds `auth`, `payment`): a key, credit or permission problem is the same on every model; a loopback `base_url` another OS account's process holds (kind `foreign-user`, nothing sent, nothing counted): the fallback would reach the same listener (FAA-257) |
|
|
85
85
|
| The fallback | required, of another vendor (the part before `/` differs) |
|
|
86
86
|
| Content | text only: a string, null, or parts that are all `{"type": "text"}`; an image, audio or file part is refused (`config`), because what it is billed cannot be bounded from its bytes |
|
|
87
87
|
| What the ceiling does not cover | the `provider` routing param can send a call to a provider priced above the catalog; two `Client` objects sharing one ledger each hold only their own calls in flight, so give an agent one client per key |
|
|
@@ -90,7 +90,7 @@ const answer = await client.chat(messages, { model, fallback, maxTokens: 800 });
|
|
|
90
90
|
| Request body | `model`, `messages`, `max_tokens`, `usage: {include: true}` and your params (temperature, reasoning, response_format, tools); `stream`, `model`, `messages`, `max_tokens` and `usage` as params are refused |
|
|
91
91
|
|
|
92
92
|
`OpenRouterError.attempts` lists every attempt as `{model, status, outcome}` (`answered`, `retry`, `error`,
|
|
93
|
-
`length`, `filtered`, `empty`, `refused`, `bad-answer`, `auth`, `payment`); nothing in it holds the key or the
|
|
93
|
+
`length`, `filtered`, `empty`, `refused`, `bad-answer`, `auth`, `payment`, `foreign-user`); nothing in it holds the key or the
|
|
94
94
|
prompt. `totals(receipts)` gives calls, priced spend and unpriced calls for a dashboard line. Choosing the models
|
|
95
95
|
themselves: creating-fabric-agents, `references/models-and-providers.md`.
|
|
96
96
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md
CHANGED
|
@@ -59,7 +59,7 @@ Never edit the copied files: the page's own rules go in its own stylesheet, cons
|
|
|
59
59
|
| Grids | `.fd-grid` (cards, 320 px columns), `.fd-grid--tiles` (160 px), `.fd-grid--wide` (480 px); `--fd-col` sets your own |
|
|
60
60
|
| Wide content | `<div class="fd-scroll" data-fd-label="Jobs table"><table class="fd-table">…</table></div>`, `<pre class="fd-scroll">` |
|
|
61
61
|
| Atoms | `.fd-card`, `.fd-tile`, `.fd-chip` + `.fd-dot` + `.fd-s-ok\|warn\|danger\|info`, `.fd-btn`, `.fd-btn--primary`, `.fd-input`, `.fd-select`, `.fd-banner`, `.fd-empty`, `.fd-pairs` |
|
|
62
|
-
| Navigation that folds | with the script, links that do not fit move into **More** (`data-fd-more-label` names it, `data-fd-no-fold` keeps a nav as is); the summary names the current section (`aria-current`) or counts the folded links, and the fit is measured with that text in place. Moving `aria-current` refits on its own; after changing the links' text call `FabricDashboard.refreshNav(nav)` (since 1.1.3) |
|
|
62
|
+
| Navigation that folds | with the script, links that do not fit move into **More** (`data-fd-more-label` names it, `data-fd-no-fold` keeps a nav as is); the summary names the current section (`aria-current`) or counts the folded links, and the fit is measured with that text in place. Moving `aria-current` refits on its own; after changing the links' text call `FabricDashboard.refreshNav(nav)` (since 1.1.3). Since 1.1.6 a resize refits in the next animation frame and only when the nav's width changed, and the chrome's `--fd-chrome-h` and `--fd-chrome-top` are written the same way and only when they change, so no observer resizes what it watches inside its own callback (WebKit's "ResizeObserver loop completed with undelivered notifications" at 390 px, seen on a service page) |
|
|
63
63
|
| Own appearance controls | `<select data-fd-theme-picker>`, `<select data-fd-scheme-picker>` (the script fills them; hidden while the host owns appearance) |
|
|
64
64
|
|
|
65
65
|
Tokens are the workbench layer (`--bg`, `--panel`, `--panel-2`, `--ink`, `--muted`, `--border`,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md
CHANGED
|
@@ -125,11 +125,43 @@ its acceptance does not authorize or deliver it. Enforce grants/dedup/expiry in
|
|
|
125
125
|
the transport. Unknown or failed host status produces no executable local action.
|
|
126
126
|
A separately labelled copyable link for diagnosis is not an open action.
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
The gate is **agent-side**: the agent runs it from this skill's directory before it
|
|
129
|
+
delivers an action. It is never copied into the generated project and never run by
|
|
130
|
+
that project's CI: the adapter's versioned plugin folder has no stable path there, so
|
|
131
|
+
its result in a project's pipeline would always be NOT_RUN. A raw-HTTP primary action
|
|
132
|
+
with an available host is the gate's negative fixture; a valid native action is the
|
|
133
|
+
positive one. Keep diagnostics separate. This gate checks structured actions, not
|
|
134
|
+
arbitrary prose: a CLI invocation or instructions alone do not prove all final answers
|
|
135
|
+
from every agent are filtered.
|
|
136
|
+
|
|
137
|
+
The generated project pins the same negative fixture in its own test suite, without
|
|
138
|
+
calling the gate: a test that renders its completion and notification output with an
|
|
139
|
+
available host and fails when a raw loopback `http_url` or a legacy `open?service=`
|
|
140
|
+
link appears in it. The snippet is self-contained; `render` stands for the project's
|
|
141
|
+
own renderer:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import re
|
|
145
|
+
import unittest
|
|
146
|
+
|
|
147
|
+
RAW_LOOPBACK = re.compile(r"https?://(?:127\.0\.0\.1|localhost|\[::1\])")
|
|
148
|
+
LEGACY = "fabric-dashboards://open?service="
|
|
149
|
+
RESOLVED = {"service": "example-agent.default",
|
|
150
|
+
"open_link": "fabric-dashboards://service/example-agent.default?path=%2Fdashboard",
|
|
151
|
+
"http_url": "http://127.0.0.1:47195/dashboard"}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def render(resolved, host_state): # replace with the project's completion/notification renderer
|
|
155
|
+
raise NotImplementedError
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class RenderedDashboardLinks(unittest.TestCase):
|
|
159
|
+
def test_rendered_output_has_no_raw_loopback_or_legacy_link(self):
|
|
160
|
+
out = render(RESOLVED, host_state="available")
|
|
161
|
+
self.assertIn(RESOLVED["open_link"], out)
|
|
162
|
+
self.assertIsNone(RAW_LOOPBACK.search(out), out)
|
|
163
|
+
self.assertNotIn(LEGACY, out)
|
|
164
|
+
```
|
|
133
165
|
|
|
134
166
|
## Degradation
|
|
135
167
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md
CHANGED
|
@@ -127,7 +127,10 @@ page as Fabric Dashboards' `open_link`; both kits carry the same sentence. The g
|
|
|
127
127
|
|
|
128
128
|
`JwksCache` holds the estate's key set at most 10 minutes and, on a `kid` it does not hold, re-reads at most once
|
|
129
129
|
a minute (DL-15), and waits 10 s after a failed read before trying again; after 10 minutes without a successful read it answers nothing, and the verifier says
|
|
130
|
-
`issuer-unavailable`. It reads https, http on loopback only, or a file; a URL with credentials is refused.
|
|
130
|
+
`issuer-unavailable`. It reads https, http on loopback only, or a file; a URL with credentials is refused. On loopback
|
|
131
|
+
http it tells the listener's owner from the socket table before a byte is sent and reads no key set another OS
|
|
132
|
+
account's process serves (DEC-0154 HA-2, `fabric_peer_owner`; the read fails `foreign-user`, so the set stays unread,
|
|
133
|
+
FAA-255); no proxy from the environment is used there. Where the socket table hides other processes (macOS 27.2 with an ad-hoc-signed Homebrew Python, FAA-267), the owner cannot be told, so a loopback key set fails `foreign-user` and every delegated call is refused `issuer-unavailable`: run the service on Apple's `/usr/bin/python3`, or give the verifier the key set as a file. The
|
|
131
134
|
Python kit also refuses, https included, a URL holding a space or ASCII control character before its first `#` (it
|
|
132
135
|
reads the URL up to there, as Node does), or a character outside ASCII in its path or query (urllib would send it
|
|
133
136
|
and every read fail; a trailing ASCII space or line break is trimmed as urllib reads it; a Unicode space ending the
|
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,13 @@ 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). `well_known_of(origin)` / `wellKnownOf(origin)` read
|
|
55
|
+
it so a status tells HA-2's order apart: None / null when nothing answers (`down`), a `ForeignUserAnswer` whose
|
|
56
|
+
`reason` starts `foreign-user` or `table-restricted`, else the document; `fetch_well_known` keeps answering None for
|
|
57
|
+
both (FAA-259). Every loopback spelling of an origin (`127.0.0.1`, another `127/8` address, `localhost`, `[::1]`) is read
|
|
58
|
+
on a checked connection (FAA-257). `launchd_install` takes an injected reader, `fetch_well_known=` and
|
|
59
|
+
`fetch_job_well_known=`, as the Node `launchdInstall` takes `fetchWellKnown` and `fetchJobWellKnown`.
|
|
53
60
|
`status` is `starting`, `ready`, `degraded` or `stopping`; `degraded` is always present, and an empty list asserts
|
|
54
61
|
full health. A process that cannot serve does not answer, and a host derives `down`. An install or upgrade polls
|
|
55
62
|
that document until `service.id` matches (at most 40 s) before it calls the service started (the launchd installers;
|
|
@@ -76,7 +83,7 @@ systemd and Task Scheduler do not poll yet, FAA-253).
|
|
|
76
83
|
|---|---|---|
|
|
77
84
|
| `Label` | reverse-DNS, stable (`com.example.example-agent`) | the host controls the job by label |
|
|
78
85
|
| `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 |
|
|
86
|
+
| `RunAtLoad` | `true` | back at every login (a LaunchDaemon: at every boot) |
|
|
80
87
|
| `KeepAlive` | `true` | `{SuccessfulExit:false}` leaves a cleanly exited service down |
|
|
81
88
|
| `ThrottleInterval` | `10` | bounds a crash loop |
|
|
82
89
|
| `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 +109,13 @@ systemd and Task Scheduler do not poll yet, FAA-253).
|
|
|
102
109
|
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
110
|
`ProgramArguments` wrapper that starts the server as a child (`sh -c` without `exec`, `uv run`, `npm start`) cannot
|
|
104
111
|
vouch for it: on such a host start the server with `exec` or run the installer from Apple's `/usr/bin/python3`.
|
|
112
|
+
A LaunchDaemon reads its answer differently: *The system domain*, below.
|
|
105
113
|
4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
|
|
106
114
|
again, then `prune_releases(releases_dir, current=<new release>)`: the running release
|
|
107
115
|
and the one before it stay for rollback, older ones go (LC-11, LC-15).
|
|
108
116
|
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
|
|
117
|
+
(with `service_id`) removes data, logs and cache when the operator asks; a LaunchDaemon refuses `purge` (*The system
|
|
118
|
+
domain*, below).
|
|
110
119
|
|
|
111
120
|
**The operator's intent wins (LC-14).** A host stops a service with `bootout` **and**
|
|
112
121
|
`disable` (off survives a login) and starts it with `enable` + `bootstrap`; restart is
|
|
@@ -123,6 +132,68 @@ that deletes an override, and a leftover `disabled` would make a later fresh ins
|
|
|
123
132
|
off. Tests use a throwaway label or a fake `launchctl` first on `PATH`, never the
|
|
124
133
|
operator's real jobs.
|
|
125
134
|
|
|
135
|
+
## The system domain
|
|
136
|
+
|
|
137
|
+
A `gui/<uid>` job (a LaunchAgent) runs only while its user is logged in. A headless Mac that a FileVault restart
|
|
138
|
+
(`fdesetup authrestart`) leaves with no GUI login, or an SSH session of a user with no GUI login, has none: `bootstrap`
|
|
139
|
+
answers error 125 ("Domain does not support specified action") and the installer's error says to remove the LaunchAgent
|
|
140
|
+
it wrote (`launchd_uninstall`) and use the system domain (FAA-278). There the job is a **LaunchDaemon**: launchd starts
|
|
141
|
+
it at boot, before any login, as the account its `UserName` names.
|
|
142
|
+
|
|
143
|
+
- **The plist**: `launchd_plist(..., user_name="<short name>")` (Node: `userName`) writes `UserName`, which a system
|
|
144
|
+
install requires (without it the job would run as root, which every client refuses); the file goes in
|
|
145
|
+
`/Library/LaunchDaemons/<label>.plist`. A plist there is in the system domain by its path (the folder's case is
|
|
146
|
+
ignored); `domain="system"` (Node: `domain: 'system'`) says so for any path, and an explicit domain wins over the
|
|
147
|
+
path. Everything else is a LaunchAgent's.
|
|
148
|
+
`UserName` is the account whose Fabric and agents call the service: clients of another account refuse it (HA-2).
|
|
149
|
+
- **Who runs which step.** The release, the descriptor and the service token are written as the `UserName` (steps 1 and
|
|
150
|
+
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
|
+
working directory and log paths name that home; only the plist, `launchd_install` and `launchd_uninstall` run as root,
|
|
152
|
+
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. The descriptor's `lifecycle.plist` names the path exactly,
|
|
154
|
+
`/Library/LaunchDaemons/<label>.plist`: a host that reads the folder by its spelling then knows the domain too.
|
|
155
|
+
- **The label is the machine's.** `/Library/LaunchDaemons` and the system domain are shared by every account of the
|
|
156
|
+
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
|
+
personal and an organization account); an install refuses, before writing anything, a label another plist holds
|
|
158
|
+
(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
|
+
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.
|
|
161
|
+
- **Root installs and removes it.** `launchd_install` and `launchd_uninstall` in the system domain refuse, before writing
|
|
162
|
+
anything, in a process that is not root, and name the command to run as root (`launchctl bootstrap system <plist>`,
|
|
163
|
+
`launchctl bootout system/<label>`, removing the plist and `launchctl enable system/<label>`, as `launchd_uninstall`
|
|
164
|
+
does). `purge` is refused: the data belongs to the `UserName`, so remove it as that user. Upgrade is the same install
|
|
165
|
+
again as root, so a descriptor's `commands.update` for a LaunchDaemon runs as root, writes the new release (and any
|
|
166
|
+
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. 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
|
+
`bootout` + `disable system/<label>`, start is `enable` + `bootstrap system <plist>`, restart is
|
|
169
|
+
`kickstart -k system/<label>`; a host or broker without root reports the job read-only.
|
|
170
|
+
- **Status and pid read without root**: `launchd_loaded(label, "system")` and `launchd_pid(label, "system")` ask
|
|
171
|
+
`launchctl print system/<label>`, and `launchd_override(label, "system")` asks `launchctl print-disabled system`
|
|
172
|
+
(Node: the `domain: 'system'` option), both of which any account may read; the probe's `lifecycle.one-copy` reads the
|
|
173
|
+
domain from the descriptor's plist the same way.
|
|
174
|
+
- **The answer the installer waits for**: root reads the service's listener as another account's, so the installer takes
|
|
175
|
+
the job's pid from `launchctl print system/<label>` and reads the token-free well-known document only on a connection
|
|
176
|
+
that pid accepted, while the pid runs as the `UserName`'s uid (`ps`) and listens on exactly `127.0.0.1` (`lsof -p`);
|
|
177
|
+
an install refuses a `UserName` that is not a short name, no account of the Mac or root, and an origin other than
|
|
178
|
+
`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 (CT-261). A `ProgramArguments` wrapper that
|
|
180
|
+
starts the server as a child cannot pass either check, and the refusal says so: start the server with `exec`. An
|
|
181
|
+
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. For hosts and the probe the domain comes from the plist's folder alone, so a LaunchDaemon's plist must live
|
|
183
|
+
in `/Library/LaunchDaemons` even when the installer is given `domain="system"`: the installer neither moves nor checks
|
|
184
|
+
it. A descriptor field for the domain is the contract's CT-234, open.
|
|
185
|
+
- **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 contract's later RT-3 vouches only for the caller's own uid. The
|
|
187
|
+
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
|
+
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 CT-261 weaker vouch (the pid listens on the port; that it accepted the connection is not shown). The contract owner accepted this in
|
|
190
|
+
principle as DEC-0161 (RT-3 extended to launchd's system domain, the restricted-table vouch included; proposed, its
|
|
191
|
+
text after its readings), so it stays a departure from the pin until the next repin (FAA-288).
|
|
192
|
+
Every other client keeps HA-2 as written.
|
|
193
|
+
- **Hosts.** A host or broker that reads launchd only in `gui/<uid>` sees a LaunchDaemon as not loaded and must not load
|
|
194
|
+
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).
|
|
196
|
+
|
|
126
197
|
## One copy under a supervisor
|
|
127
198
|
|
|
128
199
|
Started by hand, a copy that finds `service.lock` held prints one sentence naming the
|