@passioncode-ai/passioncode 0.2.17 → 0.2.18

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 (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +13 -13
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +1 -1
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -4
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +41 -41
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -1
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +12 -12
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +4 -4
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +1 -1
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -4
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +13 -11
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +10 -5
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +6 -6
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +8 -6
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +26 -26
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -1
  26. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +3 -2
  27. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +10 -10
  28. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +13 -13
  29. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +4 -4
  30. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +3 -3
  31. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -5
  32. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +5 -4
  33. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +20 -45
  34. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +1 -1
  35. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -1
  36. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -1
  37. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +1 -1
  38. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +28 -8
  39. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -1
  40. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +1 -1
  41. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +1 -1
  42. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +34 -14
  43. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +1 -1
  44. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +33 -33
  45. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +1 -1
  46. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +27 -27
  47. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +31 -31
  48. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +9 -9
  49. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +8 -8
  50. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -6
  51. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
@@ -1,4 +1,4 @@
1
- # fabric-service/0.1 — wire reference
1
+ # fabric-service/0.1: wire reference
2
2
 
3
3
  Pinned to `fabric-agent-contract` commit `c55f2d5a11c9a0197e958e87212fca7ae1154769`
4
4
  (`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
@@ -14,13 +14,13 @@ this page is the working summary. Extension key:
14
14
 
15
15
  ## Contents
16
16
 
17
- - Descriptor — written by the installer
17
+ - Descriptor: written by the installer
18
18
  - Well-known document
19
19
  - Events page
20
20
  - Login code and the operator session
21
21
  - Semantic rules a host applies
22
22
 
23
- ## Descriptor — written by the installer
23
+ ## Descriptor: written by the installer
24
24
 
25
25
  Directory: macOS `~/Library/Application Support/ai.passioncode.fabric/services/`,
26
26
  Linux `${XDG_DATA_HOME:-~/.local/share}/passioncode-fabric/services/`, Windows
@@ -33,13 +33,13 @@ File `<id>.<instance>.json`, mode 0600 (on Windows a protected ACL), atomic.
33
33
  | `id`, `instance` | `^[a-z][a-z0-9-]{1,62}$`, `^[a-z][a-z0-9-]{0,31}$` (default `default`); the pair is unique per machine |
34
34
  | `name`, `summary` | ≤ 80 and ≤ 200 characters |
35
35
  | `origin` | `http://127.0.0.1:<port>`; the port is a machine-wide claim. Optional for an app's descriptor only (`manager: "person"`, DEC-0069): its live origin and process id are in the app's own liveness file, and a descriptor without an origin claims no port (`FAC-SEM-010`) |
36
- | `auth` | `tokenFile` (0600; on Windows a protected ACL — owner and every granting entry the current user, SYSTEM or Administrators: service.md *Windows token files*, DEC-0032, DEC-0033); `header` default `Authorization` with `scheme` `Bearer`; any other header carries the raw token with `scheme: "none"` |
36
+ | `auth` | `tokenFile` (0600; on Windows a protected ACL: owner and every granting entry the current user, SYSTEM or Administrators: service.md *Windows token files*, DEC-0032, DEC-0033); `header` default `Authorization` with `scheme` `Bearer`; any other header carries the raw token with `scheme: "none"` |
37
37
  | `lifecycle` | `manager` `launchd` (then `label` and `plist`), `systemd` (then `unit`, a `systemctl --user` unit), `task-scheduler` (then `task`, a per-user Scheduled Task), `node` (Project Observatory's node supervises it as its child, with no supervisor file, DEC-0061), `person` (an app a person installs and starts, then `app`, the installed app, DEC-0069) or `none`; `unit` and `task` are refused beside any other manager (DEC-0032), `label` and `plist` beside any but `launchd` (DEC-0057), `app` beside any but `person`. Both kits and the probe accept all six; both kits write the unit and the task (`systemd_unit`, `task_xml` / `systemdUnit`, `taskXml`). An app's descriptor says it is installed, never that it runs: the probe checks `app` is still there (`descriptor.app-installed`) and reads no well-known document; a reader that finds the app gone treats the descriptor as stale (`app_descriptor_stale` / `appDescriptorStale`), and an app that writes its own descriptor rewrites it only when `app_descriptor_action` / `appDescriptorAction` says `write` |
38
38
  | `paths` | `data` (required), `logs[]`, optional `config`, `cache` |
39
- | `commands` | only `doctor` and `update`, argument arrays whose first item is an absolute or `~/` path (Windows: also `C:\…`, `C:/…` or `~\…`, never a network share — DEC-0032) |
39
+ | `commands` | only `doctor` and `update`, argument arrays whose first item is an absolute or `~/` path (Windows: also `C:\…`, `C:/…` or `~\…`, never a network share; DEC-0032) |
40
40
  | `source.repository`, `fabricManifest` | optional |
41
- | `installedAt`, `installedBy` | when, and who installed it: the installer and its version as a string, or `{by, role?, tool}` (DEC-0061) — `by` `person` (with `role` `operator` or `administrator`), `node` or `organization`; `role` for a person only; `tool` the installer and its version. It names a role, never a person. An app's descriptor uses the object form with `by: "person"`. `register_remote` / `registerRemote` and the sample write the object form |
42
- | `runner` | optional `{kind?, credentialKind}` (DEC-0061): `credentialKind` `subscription`, `key`, `platform-metered` or `none` — a kind, never a credential (`FAC-SEM-086` refuses a credential-shaped value here or in `installedBy`); a subscription names its vendor's runner (`claude-code`, `kimi-code`, `codex`) |
41
+ | `installedAt`, `installedBy` | when, and who installed it: the installer and its version as a string, or `{by, role?, tool}` (DEC-0061): `by` `person` (with `role` `operator` or `administrator`), `node` or `organization`; `role` for a person only; `tool` the installer and its version. It names a role, never a person. An app's descriptor uses the object form with `by: "person"`. `register_remote` / `registerRemote` and the sample write the object form |
42
+ | `runner` | optional `{kind?, credentialKind}` (DEC-0061): `credentialKind` `subscription`, `key`, `platform-metered` or `none`: a kind, never a credential (`FAC-SEM-086` refuses a credential-shaped value here or in `installedBy`); a subscription names its vendor's runner (`claude-code`, `kimi-code`, `codex`) |
43
43
  | `peer` | optional `{endpoint, custody, residency, maxDataClass}` (DEC-0061): the service is offered to others. `endpoint` `https://<dns-name>[:<port>][/<path>]`, never a reserved name (`FAC-SEM-085`); `custody` `organization-managed` or `personal`; `residency` 1–32 ISO 3166-1 alpha-2 codes, `EU` admitted, user-assigned codes refused; `maxDataClass` `public` < `internal` < `confidential` < `restricted`. An offer needs `runner` and never runs on a subscription. A host sends a request from anyone but the operator only when `peer_refusal` / `peerRefusal` answers null (`FAC-SEM-088`); usage rows may take `route: "platform-metered"` |
44
44
 
45
45
  ## Identifier equality (DEC-0076)
@@ -53,7 +53,7 @@ and a value that is not a string is compared as it is. The kits compare by it in
53
53
  events), FAC-SEM-076 (estate grants and their revocations) and FAC-SEM-014 (a provider entry's `providerId`), and they
54
54
  write the identifiers they scaffold in normal form.
55
55
 
56
- ## Well-known document — `GET /.well-known/fabric-service`
56
+ ## Well-known document: `GET /.well-known/fabric-service`
57
57
 
58
58
  No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
59
59
 
@@ -83,8 +83,8 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
83
83
  - `build` needs `commit` or `digest` (`sha256:<64 hex>`).
84
84
  - `summary`: at most six tiles; `attention: true` counts toward the host's badge.
85
85
  - `surfaces.events` is required; `dashboard` and `mcp` when the service has them; `usage`
86
- (DEC-0021) when the service reports its spend — see [the usage reference](usage.md);
87
- `operatorChannel.path` (DEC-0034) when the service has an operator channel — its status
86
+ (DEC-0021) when the service reports its spend (see [the usage reference](usage.md));
87
+ `operatorChannel.path` (DEC-0034) when the service has an operator channel; its status
88
88
  document follows the contract's `operator-channel-status.schema.json` and is token-protected
89
89
  like the events feed. The kits do not serve it yet.
90
90
  - `extensions` MAY carry the embedded dashboard declaration under
@@ -98,7 +98,7 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
98
98
  token is not an MCP credential: a host and the probe never call `/mcp` with it as a caller.
99
99
  The kits verify them with `fabric_delegation` / `fabric-delegation.mjs` ([delegation](delegation.md)).
100
100
 
101
- ## Events page — `GET /fabric/v1/events?after=<cursor>&limit=<n>`
101
+ ## Events page: `GET /fabric/v1/events?after=<cursor>&limit=<n>`
102
102
 
103
103
  Token required. `limit` default 50, maximum 200. Without `after`: the newest `limit`,
104
104
  ascending. `cursor` is the last id returned, or the `after` you sent when the page is
@@ -134,7 +134,7 @@ reading it further (HTTP 400, or an MCP `isError` result's `structuredContent`);
134
134
  own fault `no-accepted-revisions`. `PROTOCOL_REVISIONS` is the contract's register; `check_service.py` reads
135
135
  `well-known.protocols`.
136
136
 
137
- ## Login code and the operator session — `POST /fabric/v1/login-code`
137
+ ## Login code and the operator session: `POST /fabric/v1/login-code`
138
138
 
139
139
  Token required → `{ "url": "/fabric/v1/login?code=<16–256 url-safe chars>", "expiresAt": "…" }`,
140
140
  single use, at most 120 s, recorded as used before it is honoured. `GET` that URL →
@@ -151,4 +151,4 @@ ended session answers `401` on the dashboard page.
151
151
  | `FAC-SEM-010` | no two descriptors claim one port or one `id.instance` |
152
152
  | `FAC-SEM-011` | `ready` carries no degraded source |
153
153
  | `FAC-SEM-012` | commands start with an absolute or `~/` executable |
154
- | `FAC-SEM-025` | a usage report adds up; an all-unpriced row costs `null`, not `0` (DEC-0021) |
154
+ | `FAC-SEM-025` | a usage report adds up; an all-unpriced row costs `null`, not `0` (DEC-0021); unmeasured calls are within calls and a day's are its models' sum (DEC-0091) |
@@ -1,4 +1,4 @@
1
- # The remote placement — online agents and dashboards
1
+ # The remote placement: online agents and dashboards
2
2
 
3
3
  Normative source: `fabric-agent-contract` `docs/specification/service.md` → *Remote placement*,
4
4
  DEC-0019, at the commit this skill pins. This page is the how-to.
@@ -7,7 +7,7 @@ DEC-0019, at the commit this skill pins. This page is the how-to.
7
7
 
8
8
  It is, when it runs outside the operator's computer and the operator wants it in Fabric beside
9
9
  the local services: a hosted dashboard, an agent on a server, a bot's web console. It keeps the
10
- same objects — well-known document, events feed, login code — at the same paths.
10
+ same objects (well-known document, events feed, login code) at the same paths.
11
11
 
12
12
  ## The descriptor
13
13
 
@@ -47,7 +47,7 @@ watch the gate fail.
47
47
  the same value is written on the operator's computer by `registerRemote`. Compare in constant
48
48
  time (`tokenMatches`).
49
49
  - **Session key**: 32 random bytes, a separate secret; rotating it signs every operator out.
50
- - **Codes and sessions**: `MemoryCodeStore` — a restart forgets every outstanding code, so none
50
+ - **Codes and sessions**: `MemoryCodeStore`: a restart forgets every outstanding code, so none
51
51
  can be replayed, and every session record, so a deploy signs the operator out (DEC-0039: the
52
52
  server keeps each session's last use and logout). The host signs in again on the page's `401`.
53
53
  To keep sessions across a deploy, pass a durable store (any object with `load()` and `save()`).
@@ -68,7 +68,7 @@ reach the service, and still need its own `403`.
68
68
 
69
69
  | Situation | Host |
70
70
  |---|---|
71
- | `401` on the well-known document | `down` — the service refused the token; never `foreign` |
71
+ | `401` on the well-known document | `down`: the service refused the token; never `foreign` |
72
72
  | answer names another `id.instance` | `foreign`; the token is not sent again until the descriptor changes |
73
73
  | certificate invalid or for another name | `down`, the reason names TLS |
74
74
  | redirect | not followed; `down` |
@@ -1,8 +1,8 @@
1
- # Settings backups — what the operator decided survives the data directory (DEC-0025)
1
+ # Settings backups: what the operator decided survives the data directory (DEC-0025)
2
2
 
3
3
  Fabric Agent Contract DEC-0025, `docs/specification/service.md` → *Settings backup*, schema
4
4
  `schemas/settings-backup.schema.json`, rule `FAC-SEM-035`. A service whose store holds what the operator
5
- decided — which projects it serves, how each is bound, which secret slot each binding uses, their overrides —
5
+ decided (which projects it serves, how each is bound, which secret slot each binding uses, their overrides)
6
6
  SHOULD keep settings backups, so a lost, purged or wrongly migrated data directory costs the operator nothing.
7
7
 
8
8
  ## Where
@@ -61,7 +61,7 @@ plan = fs.restore_plan(doc, {"projects": {"columns": [...], "key": ["id"], "requ
61
61
  # plan["tables"]["projects"]: insert, present, droppedColumns, defaultedColumns, missingRequired, refused
62
62
  ```
63
63
 
64
- - **Automatic restore** only on a *fresh* database — no domain records **and** the service has never started —
64
+ - **Automatic restore** only on a *fresh* database (no domain records **and** the service has never started),
65
65
  so a restart never undoes a deletion. Insert `plan[...]["insert"]` and add every row the database refuses to
66
66
  `refused` with its key and reason.
67
67
  - **Manual restore** shows the plan first; applying it is the operator's act, never an agent's.
@@ -16,13 +16,13 @@
16
16
  the estate's delegation tokens; declare `auth.delegation {issuer, jwks}` in the well-known
17
17
  document. The probe treats it like `own` and checks the declaration
18
18
  (`auth.delegation-declared`). The kits verify the tokens: put `delegation_gate` /
19
- `delegationGate` in front of the tools — see [the delegation reference](delegation.md).
19
+ `delegationGate` in front of the tools (see [the delegation reference](delegation.md)).
20
20
  - Register it into a client config with the service's own command
21
21
  (`<tool> mcp-register`), which writes `{"type":"http","url":"http://127.0.0.1:<port>/mcp","headers":{...}}`
22
22
  atomically, aborts if the file changed meanwhile, and keeps one 0600 backup. The token
23
- goes in `headers`, never in the URL — every log that records a URL prints its query
23
+ goes in `headers`, never in the URL: every log that records a URL prints its query
24
24
  string. An HTTP registration starts nothing per session: every client session talks to the
25
- one resident service (LC-19). A stdio server, by contrast, is a process per session — keep
25
+ one resident service (LC-19). A stdio server, by contrast, is a process per session; keep
26
26
  one only at project scope.
27
27
  - Ship server `instructions` that tell a caller where to start and that every answer
28
28
  carries `degraded`.
@@ -39,7 +39,7 @@ a sign-in link only when stdout is a terminal.
39
39
  ## A2A for remote agents
40
40
 
41
41
  Remote reach goes through A2A `1.0` over HTTPS or an authenticated gateway in front of
42
- MCP — never by binding a LAN address. A tunnel (Tailscale Serve and the like) is an
42
+ MCP, never by binding a LAN address. A tunnel (Tailscale Serve and the like) is an
43
43
  outward surface with its own token lifetime; watch it like a service, because nothing
44
44
  else will notice it expire.
45
45
 
@@ -50,7 +50,7 @@ else will notice it expire.
50
50
  - Compared in constant time (`token_matches`).
51
51
  - Rotation is an operator act: write a new file, restart, re-run `mcp-register`.
52
52
  - Same-user processes can read it. The protocol defends against web pages and
53
- mistakes, not against hostile code running as the operator — say so in SECURITY.md.
53
+ mistakes, not against hostile code running as the operator; say so in SECURITY.md.
54
54
 
55
55
  ## Dashboard session
56
56
 
@@ -1,4 +1,4 @@
1
- # Usage report — what the agent spent (DEC-0021)
1
+ # Usage report: what the agent spent (DEC-0021)
2
2
 
3
3
  Fabric Agent Contract DEC-0021, `docs/specification/service.md` → *Usage report*, schema
4
4
  `schemas/service-usage.schema.json`. Optional. A service that calls paid models or tools should
@@ -39,7 +39,8 @@ ledger.record(k.makeUsageReceipt('openrouter', model, { inputTokens, outputToken
39
39
  - **Unknown tokens are named too** (DEC-0091). When the provider gave no token count (a stream cut off, a local
40
40
  model without a counter), pass `tokens_measured=False` / `tokensMeasured: false` with the tokens you have (often 0).
41
41
  The receipt carries `tokensMeasured: false`, and the report counts the call in `unmeasuredCalls` on its day and model
42
- row, never more than `calls`; a host then shows the token totals as a lower bound.
42
+ row, never more than `calls`; a host then shows the token totals as a lower bound. A job whose usage you send as
43
+ `null` or `partial` ([interop](interop.md)) is such a call.
43
44
  - **No content.** A receipt carries no prompt, output, caller or user id. The kits accept only the
44
45
  fields above.
45
46
  - If the service already keeps a ledger (a jobs table with token counts), build the receipts from
@@ -63,7 +64,7 @@ left out.
63
64
  it, and the host only shows it.
64
65
 
65
66
  **Every limit (DEC-0027).** Pass `budgets` to list every spending limit the service applies,
66
- enforced or not, so the spend view shows all of them — the operator asked for that on 2026-10-07:
67
+ enforced or not, so the spend view shows all of them; the operator asked for that on 2026-10-07:
67
68
 
68
69
  ```python
69
70
  ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budgets=[
@@ -78,7 +79,7 @@ ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budget
78
79
  way `budget` is, so `budget` and its twin in `budgets` never disagree. A per-order limit
79
80
  (`per_job`, `approval`) gets `spentUsd: null`. Every other limit states its own spend.
80
81
  - `scope` is `machine`, `project`, `pool` or `job`; a `project` or `pool` limit names its
81
- `subject`. A limit has at most one window — `period`, `windowSeconds` or `since` — and it agrees
82
+ `subject`. A limit has at most one window (`period`, `windowSeconds` or `since`), and it agrees
82
83
  with the kind (`daily`: the day or 86400 s; `monthly`: the month or 28–31 days; `velocity`: a
83
84
  rolling window). `tripped: true` (work stopped by it now) needs `enforced: true`. An unknown
84
85
  kind is shown generically, never refused.
@@ -90,12 +90,12 @@ def marker_problem(plist: dict) -> Optional[str]:
90
90
  fix = ("a copy that finds the lock held exits 75 at once and KeepAlive respawns it every ThrottleInterval; "
91
91
  "regenerate the plist with launchd_plist / launchdPlist and reinstall")
92
92
  if marker["kind"] == "conflict":
93
- return "%s — %s" % (fs.marker_conflict_message(marker), fix)
93
+ return "%s: %s" % (fs.marker_conflict_message(marker), fix)
94
94
  if marker["kind"] == "supervised":
95
- return "the marker names %s, not launchd — %s" % (marker["manager"], fix)
95
+ return "the marker names %s, not launchd: %s" % (marker["manager"], fix)
96
96
  if marker.get("unreadable") is not None:
97
- return "the marker %r names no supervisor — %s" % (marker["unreadable"], fix)
98
- return "no %s=launchd — %s" % (fs.SUPERVISOR_ENV, fix)
97
+ return "the marker %r names no supervisor: %s" % (marker["unreadable"], fix)
98
+ return "no %s=launchd: %s" % (fs.SUPERVISOR_ENV, fix)
99
99
 
100
100
 
101
101
  def priority_problems(plist: dict) -> list:
@@ -729,7 +729,7 @@ class Probe:
729
729
  marker or "%s: %s=launchd, so a copy that finds the lock held backs off" % (plist_path.name, fs.SUPERVISOR_ENV))
730
730
  slow = priority_problems(plist)
731
731
  self.add("lifecycle.priority", "FAIL" if slow else "PASS",
732
- "; ".join(slow) + " — macOS may starve the service under load and hosts then see an outage; use ProcessType Standard"
732
+ "; ".join(slow) + ". macOS may starve the service under load and hosts then see an outage; use ProcessType Standard"
733
733
  if slow else "%s: scheduled as a standard process" % plist_path.name)
734
734
  if sys.platform != "darwin" or not shutil.which("launchctl"):
735
735
  self.add("lifecycle.one-copy", "NOT_RUN", "launchctl is not available")
@@ -920,51 +920,26 @@ def usage_problems(report: Any, service_id: str, instance: str) -> List[str]:
920
920
  days = report.get("days")
921
921
  if not isinstance(days, list) or len(days) > fs.USAGE_DAYS:
922
922
  return out + ["days is not a list of at most 31"]
923
- fields = ("calls", "unpricedCalls", "unmeasuredCalls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens")
924
-
925
- def priced(row: Dict[str, Any], at: str) -> None:
926
- calls, unpriced, cost = row.get("calls", 0), row.get("unpricedCalls", 0), row.get("costUsd")
927
- if unpriced > calls:
928
- out.append("%s: more unpriced calls than calls" % at)
929
- if row.get("unmeasuredCalls", 0) > calls:
930
- out.append("%s: more calls with unknown tokens than calls (DEC-0091)" % at)
931
- if calls and unpriced == calls and cost is not None:
932
- out.append("%s: every call is unpriced, so the cost must be null, not %r" % (at, cost))
933
- if unpriced < calls and cost is None:
934
- out.append("%s: priced calls carry a cost" % at)
935
- if cost is not None and (not isinstance(cost, (int, float)) or cost < 0):
936
- out.append("%s: costUsd is not a non-negative number" % at)
937
-
938
- previous = ""
923
+ # The arithmetic of FAC-SEM-025 is the kit's `usage_findings`, the contract's `usageRules` held to its vectors: one
924
+ # implementation, so the probe never disagrees with the kit and never throws on a count that is not a number.
939
925
  for i, day in enumerate(days):
940
- at = "days[%d]" % i
941
- date = str(day.get("date", "")) if isinstance(day, dict) else ""
942
- if not re.match(r"^\d{4}-\d{2}-\d{2}$", date) or date <= previous:
943
- out.append("%s: dates must run forward" % at)
944
- previous = date
945
- priced(day, at)
946
- models = day.get("byModel") if isinstance(day, dict) else None
947
- if not isinstance(models, list):
948
- out.append("%s: byModel missing" % at)
926
+ if not isinstance(day, dict):
927
+ out.append("days[%d]: not an object" % i)
949
928
  continue
950
- for j, m in enumerate(models):
951
- priced(m, "%s.byModel[%d]" % (at, j))
952
- for f in fields:
953
- if day.get(f, 0) != sum(m.get(f, 0) for m in models):
954
- out.append("%s: %s is not the sum of its models" % (at, f))
955
- known = sum(m["costUsd"] for m in models if isinstance(m.get("costUsd"), (int, float)))
956
- if isinstance(day.get("costUsd"), (int, float)) and abs(day["costUsd"] - known) > 1e-6:
957
- out.append("%s: costUsd is not the sum of its models' known costs" % at)
958
- for j, m in enumerate(models):
929
+ if not isinstance(day.get("byModel"), list):
930
+ out.append("days[%d]: byModel missing" % i)
931
+ continue
932
+ for row, at in [(day, "days[%d]" % i)] + [(m, "days[%d].byModel[%d]" % (i, j)) for j, m in enumerate(day["byModel"]) if isinstance(m, dict)]:
933
+ cost = row.get("costUsd")
934
+ if cost is not None and (isinstance(cost, bool) or not isinstance(cost, (int, float)) or cost < 0):
935
+ out.append("%s: costUsd is not a non-negative number" % at)
936
+ for j, m in enumerate(day["byModel"]):
959
937
  # DEC-0046, DEC-0061: a row's route is a login in its vendor's runner, an issued key, or the platform's
960
938
  # own metered models (a listing in the cloud).
961
- if "route" in m and m["route"] not in fs.USAGE_ROUTES:
962
- out.append("%s.byModel[%d]: route %r is not subscription, key or platform-metered" % (at, j, m["route"]))
963
- # DEC-0065: the window and the project attribution, as the contract's usageRules reads them (FAC-SEM-025).
939
+ if isinstance(m, dict) and "route" in m and m["route"] not in fs.USAGE_ROUTES:
940
+ out.append("days[%d].byModel[%d]: route %r is not subscription, key or platform-metered" % (i, j, m["route"]))
964
941
  for finding in fs.usage_findings(report):
965
- path, message = finding["instancePath"], finding["message"]
966
- if path.startswith(("/byProject", "/generatedAt")) or "generatedAt" in message or "exists in its month" in message:
967
- out.append("%s: %s" % (path or "/", message))
942
+ out.append("%s: %s" % (finding["instancePath"] or "/", finding["message"]))
968
943
  return out
969
944
 
970
945
 
@@ -408,7 +408,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
408
408
  for c in report["copies"]:
409
409
  print(" %-8s %s (%s)" % (c["state"], c["path"], c["version"] or "no version marker"))
410
410
  if not report["copies"]:
411
- print(" no kit copy" + (" — services %s declare a dashboard" % ", ".join(report["services"])
411
+ print(" no kit copy" + (" (services %s declare a dashboard)" % ", ".join(report["services"])
412
412
  if report["dashboard"] and report["services"] else ""))
413
413
  if report["services"]:
414
414
  print(" services from this repository: %s" % ", ".join(report["services"]))
@@ -362,7 +362,7 @@ export class BoardSession {
362
362
  try {
363
363
  return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined });
364
364
  } catch (e) {
365
- if (e instanceof HubError) throw new BoardError('unreachable', 'Fabric\'s hub did not answer; a write may have been stored — send it again with the same idempotency key.', true);
365
+ if (e instanceof HubError) throw new BoardError('unreachable', 'Fabric\'s hub did not answer; a write may have been stored, so send it again with the same idempotency key.', true);
366
366
  throw e;
367
367
  }
368
368
  }
@@ -178,11 +178,12 @@ function authentic(token, jwks, typ) {
178
178
  return { compact, kid, x: found.x };
179
179
  }
180
180
 
181
- /** Depth of an actor chain; Infinity for a chain that is not one (a missing or non-string sub at any level). */
182
181
  const ORG_CONTEXT = /^org:[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
183
182
  /** DEC-0090: a partner actor is exactly {partner: org:<id>}, never with a sub, a session or an act. */
184
183
  export const isPartnerActor = (node) => isObject(node) && typeof node.partner === 'string' && ORG_CONTEXT.test(node.partner) && Object.keys(node).length === 1;
185
184
 
185
+ /** Depth of an actor chain; Infinity for a chain that is not one (a missing or non-string sub at any level). A partner
186
+ * actor ends the chain and counts, never begins it (DEC-0090). */
186
187
  export function chainDepth(act) {
187
188
  let depth = 0;
188
189
  let node = act;
@@ -420,7 +420,7 @@ export function readFolder(folder) {
420
420
  }
421
421
  };
422
422
  walk(folder, '');
423
- if (refused.length) throw new PackageError(`Not packed: remove these or pack a clean copy — ${refused.sort().join('; ')}`);
423
+ if (refused.length) throw new PackageError(`Not packed: remove these or pack a clean copy: ${refused.sort().join('; ')}`);
424
424
  return files;
425
425
  }
426
426
 
@@ -17,7 +17,7 @@ export const EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/exten
17
17
  // The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
18
18
  // document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
19
19
  export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
20
- export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.0';
20
+ export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.1';
21
21
  export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
22
22
  export const EXIT_ALREADY_RUNNING = 75;
23
23
  // Lifecycle rules: references/lifecycle.md (the rule for this kit's users; it implements the public
@@ -1119,6 +1119,25 @@ function pricedRow(row, at) {
1119
1119
  * date exists, not after `generatedAt`'s UTC date, at most 30 days before it) and the project attribution (each project
1120
1120
  * once by its normal form, each task run once; calls, priced calls and known costs bounded). Never throws.
1121
1121
  */
1122
+ /** The contract's `toNumber` (src/findings.ts): `Number()` without its throw on an object such as {"toString": 1}. */
1123
+ function toNumber(value) {
1124
+ try {
1125
+ return Number(value);
1126
+ } catch {
1127
+ return Number.NaN;
1128
+ }
1129
+ }
1130
+
1131
+ /** The contract's `show`: `String()`, or the value's JSON where `String()` would throw. */
1132
+ function show(value) {
1133
+ if (typeof value === 'string') return value;
1134
+ try {
1135
+ return String(value);
1136
+ } catch {
1137
+ return JSON.stringify(value) ?? typeof value;
1138
+ }
1139
+ }
1140
+
1122
1141
  export function usageFindings(report) {
1123
1142
  const value = typeof report === 'object' && report !== null && !Array.isArray(report) ? report : {};
1124
1143
  const findings = [];
@@ -1127,23 +1146,24 @@ export function usageFindings(report) {
1127
1146
  let previous = '';
1128
1147
  days.forEach((day, i) => {
1129
1148
  const at = `/days/${i}`;
1130
- const date = String(day.date ?? '');
1149
+ const date = show(day.date ?? '');
1131
1150
  if (date <= previous) flag(`${at}/date`, `days must run forward without a repeat (${date} after ${previous})`);
1132
1151
  previous = date;
1133
1152
  findings.push(...pricedRow(day, at));
1134
1153
  const models = usageList(day.byModel);
1135
1154
  models.forEach((m, j) => findings.push(...pricedRow(m, `${at}/byModel/${j}`)));
1136
1155
  if (!models.length) {
1137
- const spent = ['calls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'].some((f) => Number(day[f] ?? 0) > 0) || Number(day.costUsd ?? 0) > 0;
1156
+ const spent = ['calls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'].some((f) => toNumber(day[f] ?? 0) > 0) || toNumber(day.costUsd ?? 0) > 0;
1138
1157
  if (spent) flag(`${at}/byModel`, 'a day with calls, tokens or cost names the models they belong to');
1139
1158
  return;
1140
1159
  }
1141
1160
  for (const field of ['calls', 'unpricedCalls', 'unmeasuredCalls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']) {
1142
- const sum = models.reduce((n, m) => n + Number(m[field] ?? 0), 0);
1143
- if (Number(day[field] ?? 0) !== sum) flag(`${at}/${field}`, `the day's ${field} is not the sum of its models (${String(day[field] ?? 0)} ≠ ${sum})`);
1161
+ const sum = models.reduce((n, m) => n + toNumber(m[field] ?? 0), 0);
1162
+ if (toNumber(day[field] ?? 0) !== sum) flag(`${at}/${field}`, `the day's ${field} is not the sum of its models (${show(day[field] ?? 0)} ≠ ${sum})`);
1144
1163
  }
1145
1164
  const known = models.map((m) => (typeof m.costUsd === 'number' ? m.costUsd : null)).filter((x) => x !== null).reduce((n, x) => n + x, 0);
1146
- if (typeof day.costUsd === 'number' && Math.abs(day.costUsd - known) > 0.000001 + 1e-9 * Math.max(Math.abs(day.costUsd), Math.abs(known))) flag(`${at}/costUsd`, "the day's cost is not the sum of its models' known costs");
1165
+ // The contract's !close(day, known): Infinity or NaN is never close, so it is a finding.
1166
+ if (typeof day.costUsd === 'number' && !(Math.abs(day.costUsd - known) <= 0.000001 + 1e-9 * Math.max(Math.abs(day.costUsd), Math.abs(known)))) flag(`${at}/costUsd`, "the day's cost is not the sum of its models' known costs");
1147
1167
  });
1148
1168
  if (days.length) {
1149
1169
  const generated = strictTime(value.generatedAt);
@@ -1167,8 +1187,8 @@ export function usageFindings(report) {
1167
1187
  const at = `/byProject/${p}`;
1168
1188
  const normal = normalIdentifier(project.projectId);
1169
1189
  if (normal !== undefined) {
1170
- if (seen.has(normal)) flag(`${at}/projectId`, `each project is one row: ${String(project.projectId)} names the project ${seen.get(normal)} already names`);
1171
- else seen.set(normal, String(project.projectId));
1190
+ if (seen.has(normal)) flag(`${at}/projectId`, `each project is one row: ${show(project.projectId)} names the project ${seen.get(normal)} already names`);
1191
+ else seen.set(normal, show(project.projectId));
1172
1192
  }
1173
1193
  projectCalls += usageCount(project.calls);
1174
1194
  projectCost += usageCost(project.costUsd);
@@ -483,7 +483,7 @@ class BoardSession:
483
483
  return self.post(self.grant.origin, body, self.grant.bearer(), session=self.mcp_session, timeout=self.timeout,
484
484
  method=method, protocol_version=self.protocol_version if self.mcp_session else None)
485
485
  except _hub.HubError as exc:
486
- raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored — send it again with "
486
+ raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored, so send it again with "
487
487
  "the same idempotency key.", True) from exc
488
488
 
489
489
  def open(self) -> None:
@@ -364,7 +364,7 @@ def _chain_of(act: Dict[str, Any]) -> List[str]:
364
364
  node: Any = act
365
365
  while isinstance(node, dict):
366
366
  chain.append(node["partner"] if "partner" in node else node["sub"]) # the contract's chainOf, partner first
367
- node = node.get("act") if "act" in node else None
367
+ node = node.get("act")
368
368
  return chain
369
369
 
370
370
 
@@ -648,7 +648,7 @@ def read_folder(folder: Any) -> Dict[str, bytes]:
648
648
  continue
649
649
  files[rel] = full.read_bytes()
650
650
  if refused:
651
- raise PackageError("Not packed: remove these or pack a clean copy — " + "; ".join(sorted(refused)))
651
+ raise PackageError("Not packed: remove these or pack a clean copy: " + "; ".join(sorted(refused)))
652
652
  return files
653
653
 
654
654
 
@@ -18,6 +18,7 @@ import hashlib
18
18
  import hmac
19
19
  import http.server
20
20
  import json
21
+ import math
21
22
  import ntpath
22
23
  import os
23
24
  from pathlib import Path, PurePath
@@ -46,7 +47,7 @@ EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/service
46
47
  # The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
47
48
  # document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
48
49
  DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
49
- DASHBOARD_KIT = "fabric-dashboard-kit@1.1.0"
50
+ DASHBOARD_KIT = "fabric-dashboard-kit@1.1.1"
50
51
  REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
51
52
  EXIT_ALREADY_RUNNING = 75
52
53
  # Lifecycle rules: references/lifecycle.md, the rule for this kit's users, which implements the public
@@ -1734,11 +1735,21 @@ _DAY_MS = 86_400_000
1734
1735
 
1735
1736
 
1736
1737
  def _usage_count(value: Any) -> int:
1737
- return value if isinstance(value, int) and not isinstance(value, bool) and 0 <= value <= 2 ** 53 - 1 else 0
1738
+ """The contract's `count`: Number.isSafeInteger and not negative, so 5.0 (which JavaScript reads as 5) counts."""
1739
+ import fabric_estate # noqa: E402
1740
+ return int(value) if not isinstance(value, bool) and fabric_estate._safe_integer(value) and value >= 0 else 0
1741
+
1742
+
1743
+ def _usage_money(value: Any) -> Optional[float]:
1744
+ """A JSON number as JavaScript holds it (an integer beyond a double is Infinity), or None for anything else."""
1745
+ import fabric_estate # noqa: E402
1746
+ return fabric_estate._double(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
1738
1747
 
1739
1748
 
1740
1749
  def _usage_cost(value: Any) -> float:
1741
- return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) and value == value and value >= 0 and value != float("inf") else 0.0
1750
+ """The contract's `cost`: a known, finite, non-negative cost, or 0."""
1751
+ money = _usage_money(value)
1752
+ return money if money is not None and math.isfinite(money) and money >= 0 else 0.0
1742
1753
 
1743
1754
 
1744
1755
  def _exceeds(a: float, b: float) -> bool:
@@ -1797,8 +1808,7 @@ def usage_findings(report: Any) -> List[Dict[str, str]]:
1797
1808
  previous = ""
1798
1809
  for i, day in enumerate(days):
1799
1810
  at = "/days/%d" % i
1800
- date = day.get("date")
1801
- date = "" if date is None else (date if isinstance(date, str) else json.dumps(date))
1811
+ date = fabric_estate._show("" if day.get("date") is None else day["date"]) # the contract's show(day.date ?? "")
1802
1812
  if date <= previous:
1803
1813
  flag(at + "/date", "days must run forward without a repeat (%s after %s)" % (date, previous))
1804
1814
  previous = date
@@ -1806,19 +1816,29 @@ def usage_findings(report: Any) -> List[Dict[str, str]]:
1806
1816
  models = _usage_list(day.get("byModel"))
1807
1817
  for j, m in enumerate(models):
1808
1818
  findings.extend(_priced_row(m, "%s/byModel/%d" % (at, j)))
1819
+ # The contract reads every count as `toNumber(x ?? 0)` and writes it with `show`: a string count is read as a
1820
+ # number, and what is no number (an object, "abc") is NaN, which equals no sum, so it is a finding.
1821
+ number = lambda row, f: fabric_estate._js_to_number(row.get(f)) # noqa: E731 - None and absent read as 0
1809
1822
  if not models:
1810
- spent = any((day.get(f) or 0) > 0 for f in ("calls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens")
1811
- if isinstance(day.get(f) or 0, (int, float))) or (isinstance(day.get("costUsd"), (int, float)) and day["costUsd"] > 0)
1823
+ spent = any(number(day, f) > 0 for f in ("calls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens")) \
1824
+ or number(day, "costUsd") > 0
1812
1825
  if spent:
1813
1826
  flag(at + "/byModel", "a day with calls, tokens or cost names the models they belong to")
1814
1827
  continue
1815
1828
  for field in ("calls", "unpricedCalls", "unmeasuredCalls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"):
1816
- total = sum(m.get(field) or 0 for m in models if isinstance(m.get(field) or 0, (int, float)))
1817
- if (day.get(field) or 0) != total:
1818
- flag("%s/%s" % (at, field), "the day's %s is not the sum of its models (%s ≠ %s)" % (field, day.get(field) or 0, total))
1819
- known = sum(m["costUsd"] for m in models if isinstance(m.get("costUsd"), (int, float)) and not isinstance(m.get("costUsd"), bool))
1820
- day_cost = day.get("costUsd")
1821
- if isinstance(day_cost, (int, float)) and not isinstance(day_cost, bool) and abs(day_cost - known) > 0.000001 + 1e-9 * max(abs(day_cost), abs(known)):
1829
+ total = 0.0
1830
+ for m in models:
1831
+ total += number(m, field) # in order, in doubles, as JavaScript adds
1832
+ if number(day, field) != total:
1833
+ shown = fabric_estate._show(0 if day.get(field) is None else day[field])
1834
+ flag("%s/%s" % (at, field), "the day's %s is not the sum of its models (%s ≠ %s)" % (field, shown, fabric_estate._js_string(total)))
1835
+ known = 0.0
1836
+ for m in models:
1837
+ money = _usage_money(m.get("costUsd"))
1838
+ known += money if money is not None else 0.0
1839
+ day_cost = _usage_money(day.get("costUsd"))
1840
+ # The contract's !close(day, known): Infinity or NaN is never close, so it is a finding.
1841
+ if day_cost is not None and not abs(day_cost - known) <= 0.000001 + 1e-9 * max(abs(day_cost), abs(known)):
1822
1842
  flag(at + "/costUsd", "the day's cost is not the sum of its models' known costs")
1823
1843
  # The window.
1824
1844
  if days:
@@ -1865,7 +1885,7 @@ def usage_findings(report: Any) -> List[Dict[str, str]]:
1865
1885
  run_priced += max(0, _usage_count(run.get("calls")) - _usage_count(run.get("unpricedCalls")))
1866
1886
  findings.extend(_priced_row(run, "%s/taskRuns/%d" % (at, r)))
1867
1887
  project_priced = max(0, _usage_count(project.get("calls")) - _usage_count(project.get("unpricedCalls")))
1868
- known_cost = isinstance(project.get("costUsd"), (int, float)) and not isinstance(project.get("costUsd"), bool) and _usage_cost(project.get("costUsd")) == project.get("costUsd")
1888
+ known_cost = _usage_cost(project.get("costUsd")) == _usage_money(project.get("costUsd")) # as JavaScript holds it
1869
1889
  if run_calls > _usage_count(project.get("calls")):
1870
1890
  flag(at + "/taskRuns", "a project's task runs hold more calls than the project (%d > %d)" % (run_calls, _usage_count(project.get("calls"))))
1871
1891
  elif run_priced > project_priced:
@@ -258,7 +258,7 @@ def make_handler(svc: Service):
258
258
  if url.path == "/":
259
259
  if not self._session_ok():
260
260
  return self._send(401, "Open this dashboard from Fabric Dashboards.", content_type="text/plain")
261
- rows = "".join("<li>%s — %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
261
+ rows = "".join("<li>%s: %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
262
262
  for e in reversed(svc.log.fetch(None, 20)))
263
263
  return self._send(200, PAGE.format(name=html.escape(svc.name), agent=html.escape("%s.%s" % (svc.id, svc.instance)), status="ready", rows=rows),
264
264
  content_type="text/html; charset=utf-8")