@passioncode-ai/fabric-agent-adapter 0.48.3 → 0.49.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 +69 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +10 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +8 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +9 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +21 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +13 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +15 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +17 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +15 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +14 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +159 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +13 -12
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +4 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +13 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +36 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.49.1 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The Python estate kit matches every membership as the contract does** (FAA-113, from the 0.49.0 seam reading).
|
|
8
|
+
The contract keeps machine ids, passkeys, publisher keys, grants and revocations in a `Set`, or tests them with
|
|
9
|
+
`includes`; both match NaN with NaN. Fifteen lookups in `fabric_estate.py` used `===`-style matching, so a record a
|
|
10
|
+
Python caller builds with a NaN id was read differently from the Node twin: a NaN id listed twice passed, and a
|
|
11
|
+
secret from a NaN machine was flagged as coming from outside the estate. They now use `set_has`. No JSON the kit
|
|
12
|
+
loads carries NaN (`parse_constant` refuses it), so a service reading records from JSON saw no difference.
|
|
13
|
+
|
|
14
|
+
### Tests
|
|
15
|
+
|
|
16
|
+
- FAC-SEM-137 on the verify path (FAA-112): six `verify` vectors vary the delta anchor on the contract's EdDSA
|
|
17
|
+
revision 2, re-signed with the vectors' test seed and answered by the contract's `verifyEstateRecord`.
|
|
18
|
+
- The board restart cases wait on their condition, not a fixed 20 ms (FAA-114).
|
|
19
|
+
|
|
20
|
+
## 0.49.0 (2026-10-24)
|
|
21
|
+
|
|
22
|
+
The operator's sweep (FAA-111): blind seam and product readings of 0.47.0 to 0.48.3, their findings fixed, documented
|
|
23
|
+
or filed ([dispositions](docs/evidence/reports/2026-10-24-faa-111-tiers/README.md)), and three open rows closed.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **The session start says less in a folder that does not use Fabric** (FAA-60). The plugin's `SessionStart` hook
|
|
28
|
+
printed the whole Fabric preamble (about 900 characters) in every session wherever `fabric` was registered, a project
|
|
29
|
+
Fabric never served included. It now prints the preamble only where `project_uses_fabric` finds a sign (a contract
|
|
30
|
+
file up to the checkout root, a project-scope registration, a service descriptor naming the checkout's origin), and
|
|
31
|
+
elsewhere one line that points at `fabric_whoami`, so a project Fabric serves without a contract file still finds it.
|
|
32
|
+
- **`watch-mail` delivers the turns the hooks spooled** (FAA-89). A look that finds spooled turns runs the worker's own
|
|
33
|
+
`deliver` (one rejoin that reports the turns and reads the unread count) instead of a second rejoin beside them, so
|
|
34
|
+
the once-a-minute budget no longer holds a turn back while an agent waits on the board. The count it reads must be a
|
|
35
|
+
whole number of zero or more, and a look that delivered the session's end says the session ended.
|
|
36
|
+
- **The trigger descriptions of three skills name their boundaries** (FAA-88): `building-fabric-services`,
|
|
37
|
+
`creating-fabric-agents` and `working-with-fabric`, after their first trigger evals against a real client
|
|
38
|
+
([report](docs/evidence/evals/2026-10-24-trigger-run.md): 26/26, 44/45, 38/39 with Claude Code 2.1.299 and haiku).
|
|
39
|
+
- **`fabric_kit.py build` refuses to rewrite a released version's digests**: when `kit.json`'s `"version"` already has
|
|
40
|
+
a release tag and the sources differ, it stops and says to move the version first (the mistake happened three times).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **A resumed board handler ran twice** (a regression of 0.48.2). FAA-86 kept a resumed request first in the queue
|
|
45
|
+
until its resume returned; a handler that finished inside the resume (a synchronous `spawn`, the kit's fixture among
|
|
46
|
+
them) settled, the settle resumed the queue again and found the same request still first, so it ran the handler and
|
|
47
|
+
sent `com.effect_begin` a second time. Both twins now take the request off before resuming it and put it back first
|
|
48
|
+
only when the resume raises and no effect has begun (a request whose effect began waits for reconcile), which keeps
|
|
49
|
+
FAA-86's behaviour.
|
|
50
|
+
- **The OpenRouter ceiling counts money only** (DEC-0110). `Client.spent_usd` / `spentUsd` and `totals()` counted a
|
|
51
|
+
`subscription` receipt's cost in a shared ledger as spent, while the usage report has counted money only since 0.48.0.
|
|
52
|
+
A `subscription` receipt is now never money; `totals()` answers `spentUsd: 0` when every call was on a subscription
|
|
53
|
+
or there was no call.
|
|
54
|
+
- **The network guards no longer pass a 403 that carries the well-known document.** `guard_verdict` passed any 403;
|
|
55
|
+
a 403 whose JSON body holds the document or a part of it now fails, judged by the same shape as the no-token check.
|
|
56
|
+
- **The no-token check reads the document by its shape** (D4 of FAA-111). A proxy's JSON error with a `service` string
|
|
57
|
+
of its own failed as "the well-known document"; only a `protocol` of the extension, a `service` object with an `id` or
|
|
58
|
+
a `process` object with a `pid` counts. `references/remote-placement.md` lists the two false FAILs that remain.
|
|
59
|
+
|
|
60
|
+
### Documentation
|
|
61
|
+
|
|
62
|
+
- `references/usage.md`: a service built with a kit older than 0.48.0 counts a subscription's estimate until it
|
|
63
|
+
re-vendors `fabric_usage`; a self-stated `spentUsd` on the twin of `budget` must match the receipts or the report is
|
|
64
|
+
refused; a counted `null` means unknown. `references/profiles.md` and `references/protocol.md`: a `subscription`
|
|
65
|
+
row's cost is an estimate, and `platform-metered` is a route.
|
|
66
|
+
- Probe order on a remote placement: `auth.token-file` now comes before `well-known.requires-token` (since 0.48.0); a
|
|
67
|
+
reader that indexes the results by position should read them by rule id.
|
|
68
|
+
- `test/validate.py` scans the recipes' README for em dashes. The FAA-107 and FAA-84 backlog rows are corrected.
|
|
69
|
+
- Filed from the seam reading: FAA-112 (FAC-SEM-137 has no verify-path vectors) and FAA-113 (the remaining Python
|
|
70
|
+
estate lookups that mirror a Node `Set`).
|
|
71
|
+
|
|
3
72
|
## 0.48.3 (2026-10-24)
|
|
4
73
|
|
|
5
74
|
### Fixed
|
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ python3 plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_s
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
In Claude Code the plugin adds four session hooks: SessionStart (at startup and `/clear`, never on compact or
|
|
37
|
-
resume) puts that preamble into the session when the MCP server `fabric` is registered
|
|
37
|
+
resume) puts that preamble into the session when the MCP server `fabric` is registered, Fabric runs and the project
|
|
38
|
+
uses Fabric (one line pointing at `fabric_whoami` in a folder with no sign of it); when Fabric
|
|
38
39
|
is down it adds one line (skip `fabric_whoami`, start Fabric through the broker if the task needs it), and only in a
|
|
39
40
|
project that uses Fabric; Stop records each turn's facts (HEAD, files changed, lines added and removed, never content)
|
|
40
41
|
and the board's unread count; UserPromptSubmit adds one line only when that count rose; SessionEnd releases what the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.1",
|
|
4
4
|
"description": "Create Fabric-compatible agents, run them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract. This package is the installer CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "python3 test/validate.py && python3 -m unittest discover -s test -q && node --test 'test/*.test.mjs'"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.49.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.49.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
|
11
11
|
---
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: building-fabric-services
|
|
3
3
|
description: >-
|
|
4
|
-
Use when running an agent as a Fabric service (fabric-service/0.1, its kits and probe): a local
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
(
|
|
12
|
-
Fabric Dashboards.
|
|
4
|
+
Use when running an agent as a Fabric service (fabric-service/0.1, its kits and probe): a local service (launchd,
|
|
5
|
+
systemd, Windows), an ONLINE agent or dashboard on https, its dashboard link and kit (narrow panel, themes), data
|
|
6
|
+
and cache paths, how callers reach and authenticate it, long jobs (fabric.job.*), spend report and limits, runner
|
|
7
|
+
routes and model profiles, the estate record and delegation tokens, answering a Project's board requests, or packing
|
|
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).
|
|
13
13
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
14
14
|
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
15
|
metadata:
|
|
16
16
|
author: PassionCode.ai
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "0.49.1"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
20
|
extension-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md
CHANGED
|
@@ -25,7 +25,7 @@ python3 $K/fabric_kit.py install <service-dir> --lang node # fabric-ser
|
|
|
25
25
|
python3 $K/fabric_kit.py install <service-dir> --module fabric_openrouter --module fabric_board
|
|
26
26
|
python3 $K/fabric_kit.py install <service-dir> --all --dry-run # every module, nothing written
|
|
27
27
|
python3 $K/fabric_kit.py status <service-dir> [--json] # current, older (the release), modified, missing
|
|
28
|
-
python3 $K/fabric_kit.py build [--check] # maintainers: refresh kit.json from the sources
|
|
28
|
+
python3 $K/fabric_kit.py build [--check] # maintainers: refresh kit.json from the sources (refuses a tagged version)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
- `install` copies the closure of the named modules and nothing else. It refuses to overwrite a file it does not
|
|
@@ -110,4 +110,6 @@ A probe result in an event is one of `PROBE_RESULTS`; a host never appends a run
|
|
|
110
110
|
## Usage by route
|
|
111
111
|
|
|
112
112
|
`make_usage_receipt(..., route="subscription"|"key")` records how the call was paid: the operator's own login in its
|
|
113
|
-
vendor's runner, or an issued key. The usage report keeps the rows of one model on two routes apart.
|
|
113
|
+
vendor's runner, or an issued key. The usage report keeps the rows of one model on two routes apart. A subscription
|
|
114
|
+
row's `costUsd` is an estimate at list price, never money spent: a limit's `spentUsd` leaves it out (DEC-0110,
|
|
115
|
+
[usage](usage.md)).
|
|
@@ -84,7 +84,7 @@ No auth; the Host/Origin guard applies; answered from memory in under 100 ms.
|
|
|
84
84
|
(a key not entered, a model not chosen). It is not an outage, and a watcher does not alert on it. The kits pass
|
|
85
85
|
entries through unchanged.
|
|
86
86
|
- `configuration` (DEC-0046) MAY say which runner and model profile the service resolved and from where
|
|
87
|
-
(`request`, `agent`, `binding`, `estate`); a usage receipt MAY carry the `route` (`subscription` or `
|
|
87
|
+
(`request`, `agent`, `binding`, `estate`); a usage receipt MAY carry the `route` (`subscription`, `key` or `platform-metered`; a `subscription` row's cost is an estimate, never money, DEC-0110). The kits
|
|
88
88
|
build both: `fabric_profiles.configuration` and `make_usage_receipt(route=)` ([profiles](profiles.md)).
|
|
89
89
|
- `build` needs `commit` or `digest` (`sha256:<64 hex>`).
|
|
90
90
|
- `summary`: at most six tiles; `attention: true` counts toward the host's badge.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -62,17 +62,20 @@ watch the gate fail.
|
|
|
62
62
|
(Heroku, Fly, Render, a CDN) a request that names a foreign `Host` never reaches the process: the
|
|
63
63
|
router answers it, usually `404` or `421`. The probe counts that as the refusal it is, as long as
|
|
64
64
|
the body is not the well-known document. The Origin and cross-site checks name the right `Host`,
|
|
65
|
-
reach the service, and still need its own `403`.
|
|
65
|
+
reach the service, and still need its own `403`. No refusal carries the document: a `403` whose JSON body
|
|
66
|
+
holds the document or a part of it (the shapes below) fails (FAA-111).
|
|
66
67
|
|
|
67
68
|
Without the token the well-known route answers `401` with none of the service's data (DEC-0109). The kit's server
|
|
68
69
|
answers it with an empty body. An identity proxy in front of the service (Cloudflare Access, for one) may answer that
|
|
69
70
|
`401` with its own sign-in page: `well-known.requires-token` passes it when the page neither parses as the well-known
|
|
70
71
|
document nor names the service's identity values (its id, `id.instance`, build, or pid, as whole words). The probe
|
|
71
72
|
takes the service's own host name out first, because a sign-in page names the address it guards, and does not look for
|
|
72
|
-
a value shorter than four characters, because a platform's pid is often `1`. A copy of the document
|
|
73
|
-
fails, however short the id
|
|
74
|
-
|
|
75
|
-
value it found
|
|
73
|
+
a value shorter than four characters, because a platform's pid is often `1`. A copy of the document, or of its parts,
|
|
74
|
+
anywhere inside a JSON body fails, however short the id: an object whose `protocol` is the extension's, a `service`
|
|
75
|
+
object with an `id`, a `process` object with a `pid`. A proxy's own JSON error with a `service` string passes. Two false
|
|
76
|
+
FAILs remain possible, and the evidence names the value it found: a proxy page that names the service's id or
|
|
77
|
+
`id.instance` (an application the proxy titles after the service), and, when the document was read, a page that happens
|
|
78
|
+
to contain the service's pid as a whole number (an inline script's constant, say).
|
|
76
79
|
|
|
77
80
|
## What a host does and will not do
|
|
78
81
|
|
|
@@ -82,8 +82,15 @@ ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budget
|
|
|
82
82
|
a `subscription` route's `costUsd` is an estimate at list price, never spent money, so a window
|
|
83
83
|
spent only on a subscription spends `0`, and money is `null` only while no call off a subscription
|
|
84
84
|
is priced. `usage_spend(rows)` / `usageSpend(rows)` is the contract's `usageSpend` over
|
|
85
|
-
`days[].byModel[]` rows: money and the subscription's estimate apart. A
|
|
86
|
-
|
|
85
|
+
`days[].byModel[]` rows: money and the subscription's estimate apart. A day's `costUsd` still sums every
|
|
86
|
+
route, so beside a subscription it is larger than the limit's `spentUsd`. A service built with a kit older than
|
|
87
|
+
0.48.0 keeps counting the estimate as spent until it re-vendors `fabric_usage`
|
|
88
|
+
(`fabric_kit.py install <dir> --module fabric_usage`, or `--lang node --module fabric-usage`). A per-order limit
|
|
89
|
+
(`per_job`, `approval`) gets `spentUsd: null`. Every other limit states its own spend. The machine twin of
|
|
90
|
+
`budget` may leave `spentUsd` out (the kit counts it) or state the same number the receipts give; a different
|
|
91
|
+
number makes the twins disagree, and the report is refused (`FAC-SEM-031`). A counted `spentUsd` of `null` means
|
|
92
|
+
unknown, never zero: no call off a subscription in the window is priced, or one receipt in it names a `route` the
|
|
93
|
+
contract does not know or a cost its schema refuses, and then the whole window is unknown rather than guessed.
|
|
87
94
|
- `scope` is `machine`, `project`, `pool` or `job`; a `project` or `pool` limit names its
|
|
88
95
|
`subject`. A limit has at most one window (`period`, `windowSeconds` or `since`), and it agrees
|
|
89
96
|
with the kind (`daily`: the day or 86400 s; `monthly`: the month or 28–31 days; `velocity`: a
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py
CHANGED
|
@@ -79,22 +79,39 @@ def priority_problems(plist: dict) -> list:
|
|
|
79
79
|
PLATFORM_HOST_REFUSALS = (400, 404, 421)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _body_carries_document(body: bytes) -> bool:
|
|
83
|
+
"""Whether a raw answer body parses as JSON that carries the well-known document or a part of it."""
|
|
84
|
+
try:
|
|
85
|
+
return _carries_document(json.loads((body or b"").decode("utf-8", "replace")))
|
|
86
|
+
except RecursionError:
|
|
87
|
+
return True
|
|
88
|
+
except ValueError:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
82
92
|
def guard_verdict(rule: str, remote: bool, status: int, body: bytes) -> Tuple[str, str]:
|
|
93
|
+
"""A guard refuses with the service's own 403, and no refusal carries the well-known document (a 403 whose body is
|
|
94
|
+
the document refused nothing). Elsewhere the substring `fabric-service/` alone already counts as disclosure."""
|
|
83
95
|
if status == 403:
|
|
96
|
+
if _body_carries_document(body):
|
|
97
|
+
return "FAIL", "HTTP 403, and the body carries the well-known document"
|
|
84
98
|
return "PASS", "HTTP 403"
|
|
85
|
-
disclosed = b"fabric-service/" in (body or b"")
|
|
99
|
+
disclosed = b"fabric-service/" in (body or b"") or _body_carries_document(body)
|
|
86
100
|
if rule == "network.host-check" and remote and status in PLATFORM_HOST_REFUSALS and not disclosed:
|
|
87
101
|
return "PASS", "HTTP %d from the platform router: a foreign Host never reaches the service" % status
|
|
88
102
|
return "FAIL", "HTTP %d%s" % (status, ", and the body is the well-known document" if disclosed else "")
|
|
89
103
|
|
|
90
104
|
|
|
91
105
|
def _carries_document(value: Any, depth: int = 0) -> bool:
|
|
92
|
-
"""Whether a parsed body is, or holds anywhere inside it, an object that reads as the well-known document
|
|
93
|
-
`protocol` is the extension's,
|
|
106
|
+
"""Whether a parsed body is, or holds anywhere inside it, an object that reads as the well-known document or a part
|
|
107
|
+
of it: its `protocol` is the extension's, its `service` is an object with an `id`, or its `process` an object with
|
|
108
|
+
a `pid` (D3 of FAA-109). A proxy's own error with a `service` string is none of these."""
|
|
94
109
|
if depth > 64:
|
|
95
110
|
return True # nothing honest nests that deep; refuse rather than recurse without bound
|
|
96
111
|
if isinstance(value, dict):
|
|
97
|
-
|
|
112
|
+
service, process = value.get("service"), value.get("process")
|
|
113
|
+
if value.get("protocol") == fs.PROTOCOL or (isinstance(service, dict) and "id" in service) \
|
|
114
|
+
or (isinstance(process, dict) and "pid" in process):
|
|
98
115
|
return True
|
|
99
116
|
return any(_carries_document(v, depth + 1) for v in value.values())
|
|
100
117
|
if isinstance(value, list):
|
|
@@ -318,9 +318,10 @@ export class Responder {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/**
|
|
321
|
-
* Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A
|
|
322
|
-
*
|
|
323
|
-
*
|
|
321
|
+
* Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A lost answer, of its
|
|
322
|
+
* probe (finding 35) or of the `com.complete` that sends a known outcome (FAA-86), puts the request back first in
|
|
323
|
+
* line, and the next tick sends the same keys again from the checkpoint. It leaves the queue before it resumes, so a
|
|
324
|
+
* handler that settles inside the resume (and reaches `replace`, which resumes the queue again) never runs it twice.
|
|
324
325
|
*/
|
|
325
326
|
async resumeNext() {
|
|
326
327
|
while (!this.current && this.toResume.length && !this.stopping) {
|
|
@@ -331,8 +332,15 @@ export class Responder {
|
|
|
331
332
|
}
|
|
332
333
|
const entry = JSON.parse(JSON.stringify(stored));
|
|
333
334
|
if (entry.probe) await this.repliedBefore(entry);
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
this.toResume.shift();
|
|
336
|
+
try {
|
|
337
|
+
await this.resume(entry);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
// Back first only while it can still be resumed: once its effect began it waits for reconcile.
|
|
340
|
+
const held = this.checkpoint.entries[entry.request];
|
|
341
|
+
if (held && !held.effect && held.step !== 'unknown' && !this.toResume.includes(entry.request)) this.toResume.unshift(entry.request);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
336
344
|
}
|
|
337
345
|
}
|
|
338
346
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs
CHANGED
|
@@ -142,6 +142,7 @@ export class Client {
|
|
|
142
142
|
let total = 0;
|
|
143
143
|
for (const r of this.ledger.receipts()) {
|
|
144
144
|
if (!isObject(r) || !this.inWindow(r.at, today)) continue;
|
|
145
|
+
if (r.route === 'subscription') continue; // an estimate at list price, never money spent (DEC-0110)
|
|
145
146
|
if (typeof r.costUsd === 'number' && price(r.costUsd) !== null) total += r.costUsd;
|
|
146
147
|
else total += this.unmeasuredWorst.get(`${r.at} ${r.model}`) ?? unmeasuredUsd; // known here, else unmeasuredUsd
|
|
147
148
|
}
|
|
@@ -322,14 +323,22 @@ export class Client {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
// Calls,
|
|
326
|
+
// Calls, money spent and unpriced calls over receipts. A subscription receipt's cost is an estimate, never money
|
|
327
|
+
// (DEC-0110): spentUsd is null only when calls off a subscription exist and none is priced, 0 when all were on one.
|
|
326
328
|
export function totals(receipts) {
|
|
327
329
|
let calls = 0;
|
|
328
330
|
let priced = 0;
|
|
331
|
+
let paid = 0;
|
|
332
|
+
let paidPriced = 0;
|
|
329
333
|
let spent = 0;
|
|
330
334
|
for (const r of receipts) {
|
|
331
335
|
calls += 1;
|
|
332
|
-
|
|
336
|
+
const onSubscription = r.route === 'subscription';
|
|
337
|
+
if (!onSubscription) paid += 1;
|
|
338
|
+
if (typeof r.costUsd === 'number' && price(r.costUsd) !== null) {
|
|
339
|
+
priced += 1;
|
|
340
|
+
if (!onSubscription) { paidPriced += 1; spent += r.costUsd; }
|
|
341
|
+
}
|
|
333
342
|
}
|
|
334
|
-
return { calls, spentUsd:
|
|
343
|
+
return { calls, spentUsd: paid && !paidPriced ? null : spent, unpricedCalls: calls - priced };
|
|
335
344
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py
CHANGED
|
@@ -377,19 +377,27 @@ class Responder:
|
|
|
377
377
|
self.checkpoint.put(entry)
|
|
378
378
|
|
|
379
379
|
def _resume_next(self) -> None:
|
|
380
|
-
"""Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
380
|
+
"""Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A lost answer,
|
|
381
|
+
of its probe (finding 35) or of the `com.complete` that sends a known outcome (FAA-86), puts the request back
|
|
382
|
+
first in line, and the next tick sends the same keys again from the checkpoint. It leaves the queue before it
|
|
383
|
+
resumes, so a handler that settles inside the resume (and reaches `_replace`, which resumes the queue again)
|
|
384
|
+
never runs it twice."""
|
|
384
385
|
while self.current is None and self._to_resume and not self.stopping:
|
|
385
386
|
entry = self.checkpoint.entries.get(self._to_resume[0])
|
|
386
387
|
if entry is not None and self.slots.get(entry["capability"], {}).get("state") == "held":
|
|
387
388
|
entry = json.loads(json.dumps(entry))
|
|
388
389
|
if entry.get("probe"):
|
|
389
390
|
self._replied_before(entry)
|
|
390
|
-
self.
|
|
391
|
-
|
|
392
|
-
self.
|
|
391
|
+
self._to_resume.pop(0)
|
|
392
|
+
try:
|
|
393
|
+
self._resume(entry)
|
|
394
|
+
except BaseException:
|
|
395
|
+
# Back first only while it can still be resumed: once its effect began it waits for reconcile.
|
|
396
|
+
held = self.checkpoint.entries.get(entry["request"])
|
|
397
|
+
if held is not None and not held.get("effect") and held.get("step") != "unknown" \
|
|
398
|
+
and entry["request"] not in self._to_resume:
|
|
399
|
+
self._to_resume.insert(0, entry["request"])
|
|
400
|
+
raise
|
|
393
401
|
else:
|
|
394
402
|
self._to_resume.pop(0)
|
|
395
403
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py
CHANGED
|
@@ -49,7 +49,6 @@ from fabric_crypto import ( # noqa: E402 - the core (FAA-65)
|
|
|
49
49
|
)
|
|
50
50
|
from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
|
|
51
51
|
MISSING as _MISSING,
|
|
52
|
-
contains as _contains,
|
|
53
52
|
get as _get,
|
|
54
53
|
get_string as _s,
|
|
55
54
|
indexed_objects as _indexed,
|
|
@@ -59,7 +58,7 @@ from fabric_jsvalue import ( # noqa: E402 - the core (FAA-65)
|
|
|
59
58
|
objects as _list,
|
|
60
59
|
safe_integer as _safe_integer,
|
|
61
60
|
same as _same,
|
|
62
|
-
set_has as _set_has,
|
|
61
|
+
set_has as _set_has, # every membership the contract keeps in a Set or tests with includes (FAA-113)
|
|
63
62
|
show as _show,
|
|
64
63
|
)
|
|
65
64
|
from fabric_time import ( # noqa: E402 - the core (FAA-65)
|
|
@@ -331,7 +330,7 @@ def verify_estate_record(record: Any, *, at: Any, parent: Any = _MISSING, author
|
|
|
331
330
|
if not matches:
|
|
332
331
|
return {"ok": False, "problem": "unknown-authority"}
|
|
333
332
|
authority = matches[0]
|
|
334
|
-
if
|
|
333
|
+
if _set_has(revoked, _get(authority, "credentialId")):
|
|
335
334
|
return {"ok": False, "problem": "revoked-authority"}
|
|
336
335
|
origins = _get(authority, "origins")
|
|
337
336
|
credential = {"credentialId": _s(authority, "credentialId"), "alg": _s(authority, "alg"),
|
|
@@ -385,7 +384,7 @@ def _scope_admits(record: Any, kid: Any, context: Any) -> bool:
|
|
|
385
384
|
"""DEC-0090: a key no `publisherScopes[]` entry names is trusted in any context; a partner's key, only in a context
|
|
386
385
|
one of its scopes lists."""
|
|
387
386
|
scopes = [s for s in _list(_get(record, "publisherScopes")) if _same(_get(s, "kid"), kid)]
|
|
388
|
-
return not scopes or any(isinstance(_get(s, "contexts"), list) and
|
|
387
|
+
return not scopes or any(isinstance(_get(s, "contexts"), list) and _set_has(s["contexts"], context) for s in scopes)
|
|
389
388
|
|
|
390
389
|
|
|
391
390
|
#: v4 §7, Freshness: an estate revision's window is at most 30 days (DEC-0055 item 2).
|
|
@@ -545,7 +544,7 @@ def _keys(value: Dict[str, Any], flag) -> None:
|
|
|
545
544
|
hub_keys: List[Any] = []
|
|
546
545
|
for machine, m in _indexed(_get(value, "machines")):
|
|
547
546
|
at = "/machines/%d" % m
|
|
548
|
-
if
|
|
547
|
+
if _set_has(ids, _get(machine, "id")):
|
|
549
548
|
flag("%s/id" % at, "machine %s is listed twice" % _show(_get(machine, "id")))
|
|
550
549
|
ids.append(_get(machine, "id"))
|
|
551
550
|
device_keys = _list(_get(machine, "deviceKeys"))
|
|
@@ -562,15 +561,15 @@ def _keys(value: Dict[str, Any], flag) -> None:
|
|
|
562
561
|
flag("%s/deviceKeys/%d/publicKey" % (at, k), "a device key's octets are written in canonical padded base64, the one form a key reference can name (DEC-0084)")
|
|
563
562
|
elif not _valid_device_key(key):
|
|
564
563
|
flag("%s/deviceKeys/%d/publicKey" % (at, k), "an Ed25519 key is never of small order (DEC-0045) and a P-256 key is a valid point (SEC 1 §3.2.2)")
|
|
565
|
-
if
|
|
564
|
+
if _set_has(seen, _get(key, "publicKey")):
|
|
566
565
|
flag("%s/deviceKeys/%d/publicKey" % (at, k), "one key is never used for the other role")
|
|
567
566
|
seen.append(_get(key, "publicKey"))
|
|
568
567
|
hub = _get(machine, "hubKey")
|
|
569
568
|
if isinstance(hub, dict):
|
|
570
|
-
if
|
|
569
|
+
if _set_has(hub_kids, _get(hub, "kid")):
|
|
571
570
|
flag("%s/hubKey/kid" % at, "hub key %s is certified for two machines: one kid per machine" % _show(_get(hub, "kid")))
|
|
572
571
|
hub_kids.append(_get(hub, "kid"))
|
|
573
|
-
if
|
|
572
|
+
if _set_has(hub_keys, _get(hub, "x")):
|
|
574
573
|
flag("%s/hubKey/x" % at, "one hub public key is certified for two machines under two kids: one key per machine")
|
|
575
574
|
hub_keys.append(_get(hub, "x"))
|
|
576
575
|
x = _get(hub, "x")
|
|
@@ -579,7 +578,7 @@ def _keys(value: Dict[str, Any], flag) -> None:
|
|
|
579
578
|
flag("%s/hubKey/x" % at, "a hub key of small order verifies forged tokens and is never certified (DEC-0045)")
|
|
580
579
|
credentials: List[Any] = []
|
|
581
580
|
for authority, index in _indexed(_get(value, "authorities")):
|
|
582
|
-
if
|
|
581
|
+
if _set_has(credentials, _get(authority, "credentialId")):
|
|
583
582
|
flag("/authorities/%d/credentialId" % index, "a passkey is listed once")
|
|
584
583
|
credentials.append(_get(authority, "credentialId"))
|
|
585
584
|
if "problem" in credential_key(_get(authority, "publicKey"), _show(_get(authority, "alg"))):
|
|
@@ -589,7 +588,7 @@ def _keys(value: Dict[str, Any], flag) -> None:
|
|
|
589
588
|
def _placements(value: Dict[str, Any], flag) -> None:
|
|
590
589
|
kids: List[Any] = []
|
|
591
590
|
for key, index in _indexed(_get(value, "publisherKeys")):
|
|
592
|
-
if
|
|
591
|
+
if _set_has(kids, _get(key, "kid")):
|
|
593
592
|
flag("/publisherKeys/%d/kid" % index, "publisher key %s is pinned twice" % _show(_get(key, "kid")))
|
|
594
593
|
kids.append(_get(key, "kid"))
|
|
595
594
|
for machine, m in _indexed(_get(value, "machines")):
|
|
@@ -601,7 +600,7 @@ def _placements(value: Dict[str, Any], flag) -> None:
|
|
|
601
600
|
flag("%s/agentRef" % at, "%s is placed twice on one machine in the context %s"
|
|
602
601
|
% (_show(_get(placement, "agentRef")), _show(_context_of(placement))))
|
|
603
602
|
agents.append(key)
|
|
604
|
-
if not
|
|
603
|
+
if not _set_has(kids, _get(placement, "publisherKid")):
|
|
605
604
|
flag("%s/publisherKid" % at, "publisher key %s is not in publisherKeys, so the machine pins no key to verify the package under" % _show(_get(placement, "publisherKid")))
|
|
606
605
|
|
|
607
606
|
|
|
@@ -611,10 +610,10 @@ def _partner_scopes(value: Dict[str, Any], flag) -> None:
|
|
|
611
610
|
pinned = [_get(k, "kid") for k in _list(_get(value, "publisherKeys"))]
|
|
612
611
|
scoped: List[Any] = []
|
|
613
612
|
for scope, index in _indexed(_get(value, "publisherScopes")):
|
|
614
|
-
if
|
|
613
|
+
if _set_has(scoped, _get(scope, "kid")):
|
|
615
614
|
flag("/publisherScopes/%d/kid" % index, "publisher key %s is scoped twice" % _show(_get(scope, "kid")))
|
|
616
615
|
scoped.append(_get(scope, "kid"))
|
|
617
|
-
if not
|
|
616
|
+
if not _set_has(pinned, _get(scope, "kid")):
|
|
618
617
|
flag("/publisherScopes/%d/kid" % index, "publisher key %s is not in publisherKeys, so the scope limits no key" % _show(_get(scope, "kid")))
|
|
619
618
|
for machine, m in _indexed(_get(value, "machines")):
|
|
620
619
|
for placement, p in _indexed(_get(machine, "packages")):
|
|
@@ -638,7 +637,7 @@ def _secret_references(value: Dict[str, Any], flag) -> None:
|
|
|
638
637
|
flag("%s/%s" % (at, field), "a secret placement names a slot, never a value: this looks like a credential")
|
|
639
638
|
if _key_of(_get(secret, "agentRef"), _context_of(secret)) not in placed:
|
|
640
639
|
flag("%s/agentRef" % at, "a secret goes only where a placed package declares it (v4 Path 2): this agent is not placed on this machine in the secret's context")
|
|
641
|
-
if not
|
|
640
|
+
if not _set_has(machine_ids, _get(secret, "from")):
|
|
642
641
|
flag("%s/from" % at, "the slot is filled from a machine of this estate")
|
|
643
642
|
elif not _same(_context_of(secret), PERSONAL_CONTEXT) and not _same(_get(secret, "from"), _get(machine, "id")):
|
|
644
643
|
# An envelope names no context (DEC-0082): an organization's instance fills its slot on its own machine.
|
|
@@ -693,7 +692,7 @@ def _machine_profiles(value: Dict[str, Any], flag) -> None:
|
|
|
693
692
|
flag("/machines/%d/secrets" % m, "a view machine holds none of the estate's secrets")
|
|
694
693
|
for machine, m in _indexed(_get(value, "machines")):
|
|
695
694
|
for secret, s in _indexed(_get(machine, "secrets")):
|
|
696
|
-
if _set_has(views, _get(secret, "from")):
|
|
695
|
+
if _set_has(views, _get(secret, "from")):
|
|
697
696
|
flag("/machines/%d/secrets/%d/from" % (m, s), "machine %s is a view machine: it is the source of no secret" % _show(_get(secret, "from")))
|
|
698
697
|
|
|
699
698
|
|
|
@@ -709,7 +708,7 @@ def _grants_and_revocations(value: Dict[str, Any], flag) -> None:
|
|
|
709
708
|
if _get(grant, "principalKind") != "external":
|
|
710
709
|
flag("/shareGrants/%d/principalKind" % index, "a share grant is to a principal outside the estate: principalKind external")
|
|
711
710
|
ident = _get(_get(grant, "meta"), "id") if isinstance(_get(grant, "meta"), dict) else _MISSING
|
|
712
|
-
if
|
|
711
|
+
if _set_has(grant_ids, _grant_key(ident)):
|
|
713
712
|
flag("/shareGrants/%d/meta/id" % index, "grant %s is listed twice" % _show(ident))
|
|
714
713
|
grant_ids.append(_grant_key(ident))
|
|
715
714
|
machines = _list(_get(value, "machines"))
|
|
@@ -735,7 +734,7 @@ def _grants_and_revocations(value: Dict[str, Any], flag) -> None:
|
|
|
735
734
|
if _get(revocation, "kind") == "device-key" and isinstance(raw_ref, str) and "@" in raw_ref and not KEY_REFERENCE.match(raw_ref):
|
|
736
735
|
flag("/revocations/%d/ref" % index, "a device-key revocation names a kid or a key reference <kid>@sha256:<64 lower-case hex digits> (DEC-0084)")
|
|
737
736
|
seen.append(key)
|
|
738
|
-
if
|
|
737
|
+
if _set_has(listed.get(kind, []), ref):
|
|
739
738
|
flag("/revocations/%d/ref" % index, "the record still lists the %s it revokes: revocation outranks addition" % kind)
|
|
740
739
|
|
|
741
740
|
|
|
@@ -906,7 +905,7 @@ def _installation(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
906
905
|
declared = [_get(slot, "name") for slot in _list(_get(pkg, "secrets"))]
|
|
907
906
|
flag075 = _finding(findings, "FAC-SEM-075")
|
|
908
907
|
for secret, index in _indexed(_get(machine, "secrets")):
|
|
909
|
-
if _same(_get(secret, "agentRef"), _get(pkg, "agentRef")) and _same(_context_of(secret), context) and not
|
|
908
|
+
if _same(_get(secret, "agentRef"), _get(pkg, "agentRef")) and _same(_context_of(secret), context) and not _set_has(declared, _get(secret, "slot")):
|
|
910
909
|
flag075("/record/machines/%d/secrets/%d/slot" % (machine_index, index),
|
|
911
910
|
"the package declares no slot %s: a secret goes only where a placed package declares it" % _show(_get(secret, "slot")))
|
|
912
911
|
if placement is not None and not _scope_admits(record, _get(placement, "publisherKid"), context):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py
CHANGED
|
@@ -158,7 +158,7 @@ def double(value: Any) -> float:
|
|
|
158
158
|
|
|
159
159
|
def same(a: Any, b: Any) -> bool:
|
|
160
160
|
"""JavaScript's === on parsed JSON: an object equals only itself. A Set's SameValueZero differs only for NaN, which
|
|
161
|
-
no JSON value is: use ``set_has`` where the contract keeps a Set (FAA-107)."""
|
|
161
|
+
no JSON value is: use ``set_has`` where the contract keeps a Set or tests with ``includes`` (FAA-107, FAA-113)."""
|
|
162
162
|
if a is MISSING or b is MISSING:
|
|
163
163
|
return a is b
|
|
164
164
|
if isinstance(a, (dict, list)) or isinstance(b, (dict, list)):
|
|
@@ -23,6 +23,7 @@ import json
|
|
|
23
23
|
import os
|
|
24
24
|
import re
|
|
25
25
|
import shutil
|
|
26
|
+
import subprocess
|
|
26
27
|
import sys
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
from typing import Dict, List
|
|
@@ -61,6 +62,16 @@ def load_manifest() -> Dict:
|
|
|
61
62
|
return json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
def _released(version: str) -> bool:
|
|
66
|
+
"""Whether the checkout the kit sits in carries the release tag `v<version>` (False outside a Git checkout)."""
|
|
67
|
+
try:
|
|
68
|
+
listed = subprocess.run(["git", "-C", str(HERE), "tag", "--list", "v%s" % version],
|
|
69
|
+
capture_output=True, text=True, timeout=10)
|
|
70
|
+
except (OSError, subprocess.SubprocessError):
|
|
71
|
+
return False
|
|
72
|
+
return listed.returncode == 0 and bool(listed.stdout.strip())
|
|
73
|
+
|
|
74
|
+
|
|
64
75
|
def build(check: bool = False) -> List[str]:
|
|
65
76
|
"""Refresh every module's imports and sha256 in kit.json, and record this release's digests in its history.
|
|
66
77
|
Returns what changed (with check=True nothing is written)."""
|
|
@@ -83,6 +94,10 @@ def build(check: bool = False) -> List[str]:
|
|
|
83
94
|
current = {e["file"]: e["sha256"] for e in manifest["python"] + manifest["node"]}
|
|
84
95
|
current.update(digests)
|
|
85
96
|
if manifest.setdefault("history", {}).get(manifest["version"]) != current:
|
|
97
|
+
if not check and manifest["version"] in manifest["history"] and _released(manifest["version"]):
|
|
98
|
+
# A build before the version moves on would rewrite a released version's digests (FAA-111).
|
|
99
|
+
raise SystemExit("kit.json's version %s is released (tag v%s): move its \"version\" to the next release, "
|
|
100
|
+
"then build" % (manifest["version"], manifest["version"]))
|
|
86
101
|
manifest["history"][manifest["version"]] = current
|
|
87
102
|
changed.append("kit.json history %s" % manifest["version"])
|
|
88
103
|
if changed and not check:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py
CHANGED
|
@@ -212,12 +212,15 @@ class Client:
|
|
|
212
212
|
return day == today if self.window == "day" else day[:7] == today[:7]
|
|
213
213
|
|
|
214
214
|
def spent_usd(self, *, unmeasured_usd: float = 0.0) -> float:
|
|
215
|
-
"""What the ledger records for the current window; a receipt without a cost counts as `unmeasured_usd`.
|
|
215
|
+
"""What the ledger records for the current window; a receipt without a cost counts as `unmeasured_usd`. A
|
|
216
|
+
`subscription` receipt in a shared ledger is an estimate at list price, never money spent (DEC-0110)."""
|
|
216
217
|
today = fabric_time.utc_day(fabric_time.now_ms(self._now))
|
|
217
218
|
total = 0.0
|
|
218
219
|
for receipt in self.ledger.receipts():
|
|
219
220
|
if not isinstance(receipt, dict) or not self._in_window(receipt.get("at"), today):
|
|
220
221
|
continue
|
|
222
|
+
if receipt.get("route") == "subscription":
|
|
223
|
+
continue
|
|
221
224
|
cost = receipt.get("costUsd")
|
|
222
225
|
measured = isinstance(cost, (int, float)) and _price(cost) is not None
|
|
223
226
|
if measured:
|
|
@@ -419,13 +422,18 @@ class Client:
|
|
|
419
422
|
|
|
420
423
|
|
|
421
424
|
def totals(receipts: Iterable[Mapping[str, Any]]) -> Dict[str, Any]:
|
|
422
|
-
"""Calls,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
+
"""Calls, money spent and unpriced calls over receipts (what a dashboard line shows). A `subscription` receipt's
|
|
426
|
+
cost is an estimate, never money (DEC-0110): `spentUsd` is None only when calls off a subscription exist and none of
|
|
427
|
+
them is priced, and 0.0 when every call was on a subscription."""
|
|
428
|
+
calls = priced = paid = paid_priced = 0
|
|
425
429
|
spent = 0.0
|
|
426
430
|
for r in receipts:
|
|
427
431
|
calls += 1
|
|
432
|
+
on_subscription = r.get("route") == "subscription"
|
|
433
|
+
paid += 0 if on_subscription else 1
|
|
428
434
|
if isinstance(r.get("costUsd"), (int, float)) and _price(r.get("costUsd")) is not None:
|
|
429
435
|
priced += 1
|
|
430
|
-
|
|
431
|
-
|
|
436
|
+
if not on_subscription:
|
|
437
|
+
paid_priced += 1
|
|
438
|
+
spent += float(r["costUsd"])
|
|
439
|
+
return {"calls": calls, "spentUsd": None if paid and not paid_priced else spent, "unpricedCalls": calls - priced}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-service-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.1",
|
|
4
4
|
"about": "The service kit's modules in dependency order: a module imports only modules listed before it. Written by fabric_kit.py build; edit the order and layers by hand, never the digests.",
|
|
5
5
|
"python": [
|
|
6
6
|
{
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"file": "fabric_jsvalue.py",
|
|
9
9
|
"layer": "L0",
|
|
10
10
|
"imports": [],
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "12888a2c934a885d35999ac9782eaedc8d04946d9775213344a235a8814d3d28"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"module": "fabric_time",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"fabric_secrets",
|
|
78
78
|
"fabric_time"
|
|
79
79
|
],
|
|
80
|
-
"sha256": "
|
|
80
|
+
"sha256": "a0ddc49d748c1efb1830de176998d6c8eac30f77d387c71eae12549c52aaab27"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"module": "fabric_delegation",
|
|
@@ -355,7 +355,7 @@
|
|
|
355
355
|
"fabric_hub",
|
|
356
356
|
"fabric_service"
|
|
357
357
|
],
|
|
358
|
-
"sha256": "
|
|
358
|
+
"sha256": "2845d7d758b06ac4588bdf825a471c201a46ce535a631805697b1e35495fead6"
|
|
359
359
|
},
|
|
360
360
|
{
|
|
361
361
|
"module": "fabric_board_fixture",
|
|
@@ -397,7 +397,7 @@
|
|
|
397
397
|
"fabric_time",
|
|
398
398
|
"fabric_usage"
|
|
399
399
|
],
|
|
400
|
-
"sha256": "
|
|
400
|
+
"sha256": "309f588f9c976b1061323f3161e35d86248afd5b59b16391afbfc4e1099ecd65"
|
|
401
401
|
}
|
|
402
402
|
],
|
|
403
403
|
"node": [
|
|
@@ -747,7 +747,7 @@
|
|
|
747
747
|
"fabric-hub",
|
|
748
748
|
"fabric-service"
|
|
749
749
|
],
|
|
750
|
-
"sha256": "
|
|
750
|
+
"sha256": "26c9b0b2818a4cedc5248b08ea6732e2ee4ee81e31f6fee2b572a019db813c93"
|
|
751
751
|
},
|
|
752
752
|
{
|
|
753
753
|
"module": "fabric-board-fixture",
|
|
@@ -788,7 +788,7 @@
|
|
|
788
788
|
"fabric-time",
|
|
789
789
|
"fabric-usage"
|
|
790
790
|
],
|
|
791
|
-
"sha256": "
|
|
791
|
+
"sha256": "ab6c4c7251d563980d895061ee19976bde2f364c21b6354efdd9a9027b1ea139"
|
|
792
792
|
}
|
|
793
793
|
],
|
|
794
794
|
"history": {
|
|
@@ -2875,6 +2875,158 @@
|
|
|
2875
2875
|
"fabric-openrouter.mjs": "f8f0168691f968b7cd85f36aa98e2167e88cc44f1104ff514fd0c7699b339253",
|
|
2876
2876
|
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
2877
2877
|
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2878
|
+
},
|
|
2879
|
+
"0.49.0": {
|
|
2880
|
+
"fabric_jsvalue.py": "ff1bbfae999fb5683e59e31189143f1c85fa9d7780040b5a6952e060a143fc18",
|
|
2881
|
+
"fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
|
|
2882
|
+
"fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
|
|
2883
|
+
"fabric_secrets.py": "68a0b5e9e90c468ec9ac67b2f0fd1848e08813d6ec1ec14a2a47337eb1a98911",
|
|
2884
|
+
"fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
|
|
2885
|
+
"fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
|
|
2886
|
+
"fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
|
|
2887
|
+
"fabric_paths.py": "6c92af34292baddf7e10d3134d0a5b0bb23387741812a9f79c409cf1628bad23",
|
|
2888
|
+
"fabric_estate.py": "f493cd424dae6380d1caad91ffdca2764e51a49a6b1051a77731e3d8755277cd",
|
|
2889
|
+
"fabric_delegation.py": "6638a6039befe3b43f59018bab292e050931af21b544df1044cd5059572ded36",
|
|
2890
|
+
"fabric_share.py": "78506fe1a60829c525dbf6266a0aa0749a2164290ed94da8249a20b4e9be5605",
|
|
2891
|
+
"fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
|
|
2892
|
+
"fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
|
|
2893
|
+
"fabric_package.py": "ff533f1b3f92b4ea0f532fcec50b68c610d47549930eef8ad5247692a64dfb3c",
|
|
2894
|
+
"fabric_protocols.py": "28e693282cddff8e4396ae39ce6fa935e875e8ddac7375bea2c189dd6fb04970",
|
|
2895
|
+
"fabric_connections.py": "77d3ccf863c70e9209d21ef45c46a6db9c06237019bc20d54f8d3946873f3f11",
|
|
2896
|
+
"fabric_lock.py": "ba2399d2137c7a7791b7133b9db9cca36693f4eb7e30955b25a75ade814e95ba",
|
|
2897
|
+
"fabric_usage.py": "b1f335cab4625171a616cf68cf3fd892b18bdadcf4baec3a3a0fdcf12785c170",
|
|
2898
|
+
"fabric_backups.py": "02dc5eb66ec956723d3ddba07b6dd15f6a9726d901f4d60edfd02eee57880ceb",
|
|
2899
|
+
"fabric_descriptor.py": "daf16f95b1810283b1386488e006e940a0cdfa5c3c8d09386ac33d29d58df78d",
|
|
2900
|
+
"fabric_auth.py": "55794587750f6c9f40935b74295d83d7096cb4b26a0b36ac292334cd87d8f853",
|
|
2901
|
+
"fabric_events.py": "0086886c2c29dbd5c9d152be9d375c5bdf4a29f89b8095baff8490c2bcd64e47",
|
|
2902
|
+
"fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
|
|
2903
|
+
"fabric_supervisors.py": "a2dbf15aa1c4eb9de42996b413103fa4814afd2e4e66d1ab7315f30e06ab6841",
|
|
2904
|
+
"fabric_service.py": "103ab4026f96e127c01cd91e4ad44f6b0ce2424ff33f078ed106b39e773a10e3",
|
|
2905
|
+
"fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
|
|
2906
|
+
"fabric_interop_rules.py": "b5f0ecb1791b4c50c5ac151f987f692ed7001bc74841a8a161125ec775145e09",
|
|
2907
|
+
"fabric_interop.py": "f92fa111328fe3811438f733d8f0eb5295e6941c774123a78e26318bdcb35fc3",
|
|
2908
|
+
"fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
|
|
2909
|
+
"fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
|
|
2910
|
+
"fabric_board_link.py": "b5bff994cb615c6a4bb0d88d020e3a59504166180e1fa1aea3be93224f2aeccb",
|
|
2911
|
+
"fabric_board_session.py": "2845d7d758b06ac4588bdf825a471c201a46ce535a631805697b1e35495fead6",
|
|
2912
|
+
"fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
|
|
2913
|
+
"fabric_board.py": "0f075894b3e352194fcaf3fc7d08dee4e9f1c215e859769ed85fa5010e4a1599",
|
|
2914
|
+
"fabric_http.py": "5d7f72af3a73fdbe8c1f487ecf60cef23a7c46289eba542b03aff4706b8218d4",
|
|
2915
|
+
"fabric_openrouter.py": "309f588f9c976b1061323f3161e35d86248afd5b59b16391afbfc4e1099ecd65",
|
|
2916
|
+
"fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
|
|
2917
|
+
"fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
|
|
2918
|
+
"fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
|
|
2919
|
+
"fabric-secrets.mjs": "6ad8ab18ac899ecc2384e8ec6a3fb689b92be3b9a5ce3a59b92a37d52da5d766",
|
|
2920
|
+
"fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
|
|
2921
|
+
"fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
|
|
2922
|
+
"fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
|
|
2923
|
+
"fabric-paths.mjs": "2201b4ed7126f260931b63e8386377b4317d8dcad419d109bf22c683e44b7f29",
|
|
2924
|
+
"fabric-estate.mjs": "b6610aaad9d00d8a7f42955627fb81725950841455bcedfc050abb0e079f0f93",
|
|
2925
|
+
"fabric-delegation.mjs": "dafde9ea327c88d92dc73d1400282b43dc6f84dd345a2d2b9b154304f65fc9d7",
|
|
2926
|
+
"fabric-share.mjs": "d40a7405d7bd4c3213ed691330b7549afe8e023db0753b582e249714e672010d",
|
|
2927
|
+
"fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
|
|
2928
|
+
"fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
|
|
2929
|
+
"fabric-package.mjs": "13913ea6d12056c3a31b8222b6c4aab8395941fe519913692e1a06d8672e1c88",
|
|
2930
|
+
"fabric-protocols.mjs": "dc039ec3612d0090c9ba6a3a949b0b7df54712bead300dcb6cc8b9bc305cd183",
|
|
2931
|
+
"fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
|
|
2932
|
+
"fabric-lock.mjs": "ac219ab0a326d5c43c7c4c255e0c3843d4794de795a60e3af661e7d5350ea052",
|
|
2933
|
+
"fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
|
|
2934
|
+
"fabric-backups.mjs": "849acdc6eba0fff648c8158e61f6b68cccd7bedf0880f7d5ec03a6a20f1e7005",
|
|
2935
|
+
"fabric-descriptor.mjs": "95c0b7ff36c29d7c748f1fa6fb1767d6fe8c97fca7f1ee404559930abbd3cc84",
|
|
2936
|
+
"fabric-auth.mjs": "3ea6796470fcd829bc72a772fb7e4c37532a09d1e37ff41592dcf1a1a3074ca2",
|
|
2937
|
+
"fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
|
|
2938
|
+
"fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
|
|
2939
|
+
"fabric-supervisors.mjs": "9d61c1a17538a5e2453e33e5ae347337336748041bc054478a96dd51f3d4fc51",
|
|
2940
|
+
"fabric-service.mjs": "1b4639a10c530c1241d317c5e9cf3a10cd96e5d9af8b29fc1b1844de5c63bfec",
|
|
2941
|
+
"fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
|
|
2942
|
+
"fabric-interop-rules.mjs": "57a8081307956e8c381119b361fa5232a7bf791815be8e3541eb5492820e2ea5",
|
|
2943
|
+
"fabric-interop.mjs": "581983ec3e197e5c2340ad25e8468320c69a2459c90097c7fcf2699766b4fb26",
|
|
2944
|
+
"fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
|
|
2945
|
+
"fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
|
|
2946
|
+
"fabric-board-link.mjs": "4cd6d4e21713a38c8e56f7d59db3148638b8c27c08e774d96a0f6283c6049e0a",
|
|
2947
|
+
"fabric-board-session.mjs": "26c9b0b2818a4cedc5248b08ea6732e2ee4ee81e31f6fee2b572a019db813c93",
|
|
2948
|
+
"fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
|
|
2949
|
+
"fabric-board.mjs": "9e7bfe4d412521982224ef12bebfc2ff255eeeaf8d96cf40234ac18de68e7a59",
|
|
2950
|
+
"fabric-http.mjs": "f886bcb49c4353d039387ac9568738dda26697a94dd07fc6a6204500f0bc27bf",
|
|
2951
|
+
"fabric-openrouter.mjs": "ab6c4c7251d563980d895061ee19976bde2f364c21b6354efdd9a9027b1ea139",
|
|
2952
|
+
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
2953
|
+
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2954
|
+
},
|
|
2955
|
+
"0.49.1": {
|
|
2956
|
+
"fabric_jsvalue.py": "12888a2c934a885d35999ac9782eaedc8d04946d9775213344a235a8814d3d28",
|
|
2957
|
+
"fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
|
|
2958
|
+
"fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
|
|
2959
|
+
"fabric_secrets.py": "68a0b5e9e90c468ec9ac67b2f0fd1848e08813d6ec1ec14a2a47337eb1a98911",
|
|
2960
|
+
"fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
|
|
2961
|
+
"fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
|
|
2962
|
+
"fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
|
|
2963
|
+
"fabric_paths.py": "6c92af34292baddf7e10d3134d0a5b0bb23387741812a9f79c409cf1628bad23",
|
|
2964
|
+
"fabric_estate.py": "a0ddc49d748c1efb1830de176998d6c8eac30f77d387c71eae12549c52aaab27",
|
|
2965
|
+
"fabric_delegation.py": "6638a6039befe3b43f59018bab292e050931af21b544df1044cd5059572ded36",
|
|
2966
|
+
"fabric_share.py": "78506fe1a60829c525dbf6266a0aa0749a2164290ed94da8249a20b4e9be5605",
|
|
2967
|
+
"fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
|
|
2968
|
+
"fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
|
|
2969
|
+
"fabric_package.py": "ff533f1b3f92b4ea0f532fcec50b68c610d47549930eef8ad5247692a64dfb3c",
|
|
2970
|
+
"fabric_protocols.py": "28e693282cddff8e4396ae39ce6fa935e875e8ddac7375bea2c189dd6fb04970",
|
|
2971
|
+
"fabric_connections.py": "77d3ccf863c70e9209d21ef45c46a6db9c06237019bc20d54f8d3946873f3f11",
|
|
2972
|
+
"fabric_lock.py": "ba2399d2137c7a7791b7133b9db9cca36693f4eb7e30955b25a75ade814e95ba",
|
|
2973
|
+
"fabric_usage.py": "b1f335cab4625171a616cf68cf3fd892b18bdadcf4baec3a3a0fdcf12785c170",
|
|
2974
|
+
"fabric_backups.py": "02dc5eb66ec956723d3ddba07b6dd15f6a9726d901f4d60edfd02eee57880ceb",
|
|
2975
|
+
"fabric_descriptor.py": "daf16f95b1810283b1386488e006e940a0cdfa5c3c8d09386ac33d29d58df78d",
|
|
2976
|
+
"fabric_auth.py": "55794587750f6c9f40935b74295d83d7096cb4b26a0b36ac292334cd87d8f853",
|
|
2977
|
+
"fabric_events.py": "0086886c2c29dbd5c9d152be9d375c5bdf4a29f89b8095baff8490c2bcd64e47",
|
|
2978
|
+
"fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
|
|
2979
|
+
"fabric_supervisors.py": "a2dbf15aa1c4eb9de42996b413103fa4814afd2e4e66d1ab7315f30e06ab6841",
|
|
2980
|
+
"fabric_service.py": "103ab4026f96e127c01cd91e4ad44f6b0ce2424ff33f078ed106b39e773a10e3",
|
|
2981
|
+
"fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
|
|
2982
|
+
"fabric_interop_rules.py": "b5f0ecb1791b4c50c5ac151f987f692ed7001bc74841a8a161125ec775145e09",
|
|
2983
|
+
"fabric_interop.py": "f92fa111328fe3811438f733d8f0eb5295e6941c774123a78e26318bdcb35fc3",
|
|
2984
|
+
"fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
|
|
2985
|
+
"fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
|
|
2986
|
+
"fabric_board_link.py": "b5bff994cb615c6a4bb0d88d020e3a59504166180e1fa1aea3be93224f2aeccb",
|
|
2987
|
+
"fabric_board_session.py": "2845d7d758b06ac4588bdf825a471c201a46ce535a631805697b1e35495fead6",
|
|
2988
|
+
"fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
|
|
2989
|
+
"fabric_board.py": "0f075894b3e352194fcaf3fc7d08dee4e9f1c215e859769ed85fa5010e4a1599",
|
|
2990
|
+
"fabric_http.py": "5d7f72af3a73fdbe8c1f487ecf60cef23a7c46289eba542b03aff4706b8218d4",
|
|
2991
|
+
"fabric_openrouter.py": "309f588f9c976b1061323f3161e35d86248afd5b59b16391afbfc4e1099ecd65",
|
|
2992
|
+
"fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
|
|
2993
|
+
"fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
|
|
2994
|
+
"fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
|
|
2995
|
+
"fabric-secrets.mjs": "6ad8ab18ac899ecc2384e8ec6a3fb689b92be3b9a5ce3a59b92a37d52da5d766",
|
|
2996
|
+
"fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
|
|
2997
|
+
"fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
|
|
2998
|
+
"fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
|
|
2999
|
+
"fabric-paths.mjs": "2201b4ed7126f260931b63e8386377b4317d8dcad419d109bf22c683e44b7f29",
|
|
3000
|
+
"fabric-estate.mjs": "b6610aaad9d00d8a7f42955627fb81725950841455bcedfc050abb0e079f0f93",
|
|
3001
|
+
"fabric-delegation.mjs": "dafde9ea327c88d92dc73d1400282b43dc6f84dd345a2d2b9b154304f65fc9d7",
|
|
3002
|
+
"fabric-share.mjs": "d40a7405d7bd4c3213ed691330b7549afe8e023db0753b582e249714e672010d",
|
|
3003
|
+
"fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
|
|
3004
|
+
"fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
|
|
3005
|
+
"fabric-package.mjs": "13913ea6d12056c3a31b8222b6c4aab8395941fe519913692e1a06d8672e1c88",
|
|
3006
|
+
"fabric-protocols.mjs": "dc039ec3612d0090c9ba6a3a949b0b7df54712bead300dcb6cc8b9bc305cd183",
|
|
3007
|
+
"fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
|
|
3008
|
+
"fabric-lock.mjs": "ac219ab0a326d5c43c7c4c255e0c3843d4794de795a60e3af661e7d5350ea052",
|
|
3009
|
+
"fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
|
|
3010
|
+
"fabric-backups.mjs": "849acdc6eba0fff648c8158e61f6b68cccd7bedf0880f7d5ec03a6a20f1e7005",
|
|
3011
|
+
"fabric-descriptor.mjs": "95c0b7ff36c29d7c748f1fa6fb1767d6fe8c97fca7f1ee404559930abbd3cc84",
|
|
3012
|
+
"fabric-auth.mjs": "3ea6796470fcd829bc72a772fb7e4c37532a09d1e37ff41592dcf1a1a3074ca2",
|
|
3013
|
+
"fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
|
|
3014
|
+
"fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
|
|
3015
|
+
"fabric-supervisors.mjs": "9d61c1a17538a5e2453e33e5ae347337336748041bc054478a96dd51f3d4fc51",
|
|
3016
|
+
"fabric-service.mjs": "1b4639a10c530c1241d317c5e9cf3a10cd96e5d9af8b29fc1b1844de5c63bfec",
|
|
3017
|
+
"fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
|
|
3018
|
+
"fabric-interop-rules.mjs": "57a8081307956e8c381119b361fa5232a7bf791815be8e3541eb5492820e2ea5",
|
|
3019
|
+
"fabric-interop.mjs": "581983ec3e197e5c2340ad25e8468320c69a2459c90097c7fcf2699766b4fb26",
|
|
3020
|
+
"fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
|
|
3021
|
+
"fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
|
|
3022
|
+
"fabric-board-link.mjs": "4cd6d4e21713a38c8e56f7d59db3148638b8c27c08e774d96a0f6283c6049e0a",
|
|
3023
|
+
"fabric-board-session.mjs": "26c9b0b2818a4cedc5248b08ea6732e2ee4ee81e31f6fee2b572a019db813c93",
|
|
3024
|
+
"fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
|
|
3025
|
+
"fabric-board.mjs": "9e7bfe4d412521982224ef12bebfc2ff255eeeaf8d96cf40234ac18de68e7a59",
|
|
3026
|
+
"fabric-http.mjs": "f886bcb49c4353d039387ac9568738dda26697a94dd07fc6a6204500f0bc27bf",
|
|
3027
|
+
"fabric-openrouter.mjs": "ab6c4c7251d563980d895061ee19976bde2f364c21b6354efdd9a9027b1ea139",
|
|
3028
|
+
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
3029
|
+
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2878
3030
|
}
|
|
2879
3031
|
},
|
|
2880
3032
|
"data": {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: creating-fabric-agents
|
|
3
|
-
description: Use when designing and building a NEW agent or provider that must be Fabric-compatible from its first commit, «создай агента, совместимого с фабрикой Passion Code», «новый агент под Fabric», "create a fabric-compatible agent", "build a new Fabric provider", "fabric-ready agent from scratch", «агент в облаке организации», "which models and OpenRouter key for a new Fabric agent". Runs the intake grill (capability, named consumer, workflow or agent, profile, effects, where it runs, models, who pays, how it learns), turns source projects' recorded failures into planted eval fixtures, scaffolds the pinned contract bundle and sets the canary expectation. NOT for adapting an existing project (adapting-projects-to-fabric), making an agent a local or online Fabric service with a dashboard (building-fabric-services), a coding session working through Fabric or its board (working-with-fabric), the Fabric host or orchestrator, or agent
|
|
3
|
+
description: Use when designing and building a NEW agent or provider that must be Fabric-compatible from its first commit, «создай агента, совместимого с фабрикой Passion Code», «новый агент под Fabric», "create a fabric-compatible agent", "build a new Fabric provider", "fabric-ready agent from scratch", «агент в облаке организации», "which models and OpenRouter key for a new Fabric agent". Runs the intake grill (capability, named consumer, workflow or agent, profile, effects, where it runs, models, who pays, how it learns), turns source projects' recorded failures into planted eval fixtures, scaffolds the pinned contract bundle and sets the canary expectation. NOT for adapting an existing project (adapting-projects-to-fabric), making an agent a local or online Fabric service with a dashboard (building-fabric-services), a coding session working through Fabric or its board (working-with-fabric), the Fabric host or orchestrator, or an agent, bot or A2A server that need not speak Fabric.
|
|
4
4
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.49.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
|
11
11
|
---
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
name: working-with-fabric
|
|
3
3
|
description: >-
|
|
4
4
|
Use when a coding agent (Claude Code, Codex, Kimi Code, Kilo, Hermes) works in a project through Fabric from a
|
|
5
|
-
terminal: finding Fabric, having the lifecycle broker start
|
|
6
|
-
fabric_whoami first, the project board (com.status, com.list, com.read_ack; asking another Project
|
|
7
|
-
com.submit; unread-mail lines, watch-mail), claiming and releasing work (fabric_work_claim,
|
|
8
|
-
reports, handoffs, refusals. Triggers: "connect to Fabric", "register fabric-mcp", "claim the
|
|
9
|
-
"check the project board", "ask the other project through Fabric", "Fabric is not running",
|
|
10
|
-
Fabric», «возьми задачу в Fabric», «проверь доску проекта»,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
terminal: finding Fabric, having the lifecycle broker start Fabric (not other apps), registering fabric-mcp once
|
|
6
|
+
with no secret, fabric_whoami first, the project board (com.status, com.list, com.read_ack; asking another Project
|
|
7
|
+
only through com.submit; unread-mail lines, watch-mail), claiming and releasing work (fabric_work_claim,
|
|
8
|
+
fabric_work_release), reports, handoffs, refusals. Triggers: "connect to Fabric", "register fabric-mcp", "claim the
|
|
9
|
+
task in Fabric", "check the project board", "ask the other project through Fabric", "Fabric is not running",
|
|
10
|
+
«подключись к Fabric», «возьми задачу в Fabric», «проверь доску проекта», «Fabric не запущен», "hit your limit,
|
|
11
|
+
resets at". Explains the plugin's session hooks. NOT for building or adapting an agent or a board responder
|
|
12
|
+
(creating-fabric-agents, adapting-projects-to-fabric, building-fabric-services), changing Fabric's own code, or
|
|
13
|
+
messaging people.
|
|
14
14
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
15
15
|
compatibility: >-
|
|
16
16
|
Needs the Fabric desktop app (0.4.0 or later for fabric-mcp and the fabric_work tools) and an MCP client.
|
|
@@ -19,7 +19,7 @@ compatibility: >-
|
|
|
19
19
|
this plugin; Codex/Kimi use command hooks; Hermes/OpenCode/OpenClaw/Pi use native context plugins after opt-in.
|
|
20
20
|
metadata:
|
|
21
21
|
author: PassionCode.ai
|
|
22
|
-
version: "0.
|
|
22
|
+
version: "0.49.1"
|
|
23
23
|
---
|
|
24
24
|
|
|
25
25
|
# Working with Fabric
|
|
@@ -227,8 +227,9 @@ and do the work without claiming Fabric saw it.
|
|
|
227
227
|
|
|
228
228
|
## The session hooks
|
|
229
229
|
|
|
230
|
-
In Claude Code this plugin ships four: at session start and `/clear` (not on compact or resume) the preamble above
|
|
231
|
-
|
|
230
|
+
In Claude Code this plugin ships four: at session start and `/clear` (not on compact or resume) the preamble above
|
|
231
|
+
in a project that uses Fabric, one line pointing at `fabric_whoami` in a folder with no sign of it, or, when Fabric is
|
|
232
|
+
down and the project uses Fabric, one line pointing at Step 1; after each turn the turn's facts (HEAD, files changed, lines added
|
|
232
233
|
and removed) and the board's unread count; at each prompt one line only when that count rose; at the end, the
|
|
233
234
|
release of what the session left claimed. The turn, prompt and end hooks spool and call Fabric only when the running
|
|
234
235
|
Fabric advertises `session-hooks/1`; the turn and end hooks always prune their own stale spool. All are silent in a session Fabric launched, never block the session and read no token. Read
|
|
@@ -19,7 +19,7 @@ need from Fabric. In Claude Code they live in the plugin's `hooks/hooks.json` an
|
|
|
19
19
|
|
|
20
20
|
| Event | When it acts | What it does |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| SessionStart (`startup`, `clear`; the plugin's matcher, and the script ignores `compact` and `resume` for every host) | Claude Code has an MCP server named `fabric` (local, project or user scope) | Fabric running: adds the preamble: `fabric_whoami` first; `com.status` and `com.list`, `com.read_ack` only what was read; `fabric_work_list`, then `fabric_work_claim`; `fabric_work_release` at the end; ask another Project only through `com.submit`. Fabric down, in a project that uses Fabric
|
|
22
|
+
| SessionStart (`startup`, `clear`; the plugin's matcher, and the script ignores `compact` and `resume` for every host) | Claude Code has an MCP server named `fabric` (local, project or user scope) | Fabric running, in a project that uses Fabric (below): adds the preamble: `fabric_whoami` first; `com.status` and `com.list`, `com.read_ack` only what was read; `fabric_work_list`, then `fabric_work_claim`; `fabric_work_release` at the end; ask another Project only through `com.submit`. Fabric running in a folder with no such sign (a user-scope registration reaches every folder): one line pointing at `fabric_whoami`, which names the project when Fabric serves the folder anyway (FAA-60). Fabric down, in a project that uses Fabric: one line, skip `fabric_whoami` and the board, start Fabric through the lifecycle broker only if the task needs it. Fabric down anywhere else, or not registered: nothing. |
|
|
23
23
|
| Stop (each turn) | as for SessionStart, and the running Fabric's `hub.json` lists `"session-hooks/1"` in `features` | measures the turn's facts (HEAD, files changed, lines added and removed: counts and ids only, git sharing 1.5 s), spools them in one entry and starts a detached worker that sends `fabric_session_observe {event: "turn", head, filesChanged, linesAdded, linesRemoved, …}` as stored, then reads `com.status` and keeps the unread count in the session's spool (`board.json`) |
|
|
24
24
|
| UserPromptSubmit | the session's spool holds a count | adds ONE line, `N unread on the project board: com.list`, only when the count rose since the last line; a fall or the same count says nothing. It reads two small files and never calls Fabric. |
|
|
25
25
|
| SessionEnd | as for Stop | spools the end; the worker sends `fabric_work_release {claims: "session", outcome: "let-go", …}` (with `observedAt`, the time the hook fired, where the tool's input schema names it: Fabric 0.3.9 then refuses an end from before the session enrolled), then `fabric_session_observe {event: "end", reason}`, and removes the session's spool. Reason `clear` ends nothing: `/clear` keeps the agent process and its fabric-mcp, so no claim is released and no end observed; turns already spooled still go out (since 0.42.0) |
|
|
@@ -74,7 +74,9 @@ starts it under a background watcher. It rejoins the session (`CLAUDE_CODE_SESSI
|
|
|
74
74
|
`fabric-mcp --rejoin-only` for each look, prints one line each time the unread count rises, and waits `interval`
|
|
75
75
|
± 20 % between looks (after the jitter never under 10 s and never over 15 minutes). It shares the session's
|
|
76
76
|
once-a-minute rejoin with the Stop worker: a look within a minute of the last rejoin (the worker's `rejoined` mark or
|
|
77
|
-
its own) waits instead, and its own rejoin renews that mark when the session's folder exists.
|
|
77
|
+
its own) waits instead, and its own rejoin renews that mark when the session's folder exists. Because its looks keep
|
|
78
|
+
that mark fresh, a Stop-hook turn usually waits in the spool while it runs: a look that finds spooled turns delivers
|
|
79
|
+
them through its own rejoin, as the worker would, and reads the board's count the worker writes (FAA-89). While Fabric is down or
|
|
78
80
|
the board unreadable it says so once and backs off, doubling to at most 15 minutes. It exits on its stdin's end (when started with a pipe), SIGTERM, SIGINT or SIGHUP,
|
|
79
81
|
or when its parent is gone. It refuses to start in a session Fabric launched, and without a `fabric` registration.
|
|
80
82
|
|
package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py
CHANGED
|
@@ -65,6 +65,16 @@ PREAMBLE = (
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
#: FAA-60: one line where Fabric runs and `fabric` is registered, but nothing says this folder works through it (a
|
|
69
|
+
#: user-scope registration reaches every folder of the machine). It hides nothing: `fabric_whoami` names the project
|
|
70
|
+
#: when Fabric serves the folder all the same.
|
|
71
|
+
POINTER = (
|
|
72
|
+
"[fabric-agent-adapter] Fabric is running and this {host} has `fabric` registered; this folder carries no sign that "
|
|
73
|
+
"it works through Fabric. If it does, call `fabric_whoami`: it names the project. The working-with-fabric skill has "
|
|
74
|
+
"the routine."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
68
78
|
#: One line, and only in a project that uses Fabric (project_uses_fabric): everywhere else a Fabric that is down is
|
|
69
79
|
#: nothing the session needs to hear.
|
|
70
80
|
DOWN_NOTICE = (
|
|
@@ -89,8 +99,8 @@ def read_hook_input() -> Dict[str, Any]:
|
|
|
89
99
|
def hook_session_start(payload: Dict[str, Any], env: Optional[Dict[str, str]] = None,
|
|
90
100
|
host: str = "claude") -> Optional[str]:
|
|
91
101
|
"""What to print, or None: Claude/Codex hook JSON, plain context for native plugins and Kimi. The preamble when the
|
|
92
|
-
host has `fabric` registered
|
|
93
|
-
(
|
|
102
|
+
host has `fabric` registered, Fabric runs and the project uses Fabric (project_uses_fabric); a one-line pointer
|
|
103
|
+
when the project shows no sign (FAA-60); when Fabric is down, one line, and only in a project that uses Fabric. Silent on a compacted or resumed session (`source`), which already holds what it was told,
|
|
94
104
|
and in a session Fabric launched. How to read the machine's Fabric services is the host instructions' to say."""
|
|
95
105
|
if host not in HOSTS or payload.get("source") in QUIET_SOURCES or fabric_launched(env):
|
|
96
106
|
return None
|
|
@@ -102,7 +112,7 @@ def hook_session_start(payload: Dict[str, Any], env: Optional[Dict[str, str]] =
|
|
|
102
112
|
if not registered:
|
|
103
113
|
return None
|
|
104
114
|
if locate_hub(fabric_root(env=env))["state"] == "running":
|
|
105
|
-
context = PREAMBLE.format(host=HOSTS[host])
|
|
115
|
+
context = (PREAMBLE if project_uses_fabric(cwd, env, registration) else POINTER).format(host=HOSTS[host])
|
|
106
116
|
elif project_uses_fabric(cwd, env, registration):
|
|
107
117
|
context = DOWN_NOTICE
|
|
108
118
|
else:
|
package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
agent waits on it, with backoff while Fabric is down. Standard library only."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
+
from typing import Any, Dict, Optional, Tuple
|
|
10
11
|
|
|
11
12
|
from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
|
|
12
13
|
BOARD_STATUS_TOOL,
|
|
@@ -28,6 +29,7 @@ from fabric_session_mcp import ( # noqa: E402 - the kit (FAA-65)
|
|
|
28
29
|
NoSessionError,
|
|
29
30
|
_wait_for_session_tools,
|
|
30
31
|
board_unread,
|
|
32
|
+
deliver,
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
|
|
@@ -43,7 +45,8 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
43
45
|
answers but the board cannot be read, ("down", why) when Fabric cannot be reached, ("wait", seconds) when the
|
|
44
46
|
session rejoined within the last minute (the Stop worker's mark, or this one's): the rejoin budget is shared, so
|
|
45
47
|
no fabric-mcp starts then (FAA-68). No fabric-mcp starts unless the running Fabric advertises session-hooks/1
|
|
46
|
-
(the rejoin). A successful rejoin renews the mark in the session's folder when the hooks made one.
|
|
48
|
+
(the rejoin). A successful rejoin renews the mark in the session's folder when the hooks made one. Turns the hooks
|
|
49
|
+
spooled while the mark held them back are delivered by this look, through the same rejoin (FAA-89)."""
|
|
47
50
|
env = dict(os.environ if env is None else env)
|
|
48
51
|
directory = session_dir(session_id, env)
|
|
49
52
|
left = rejoin_wait(directory)
|
|
@@ -57,6 +60,8 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
57
60
|
registration = claude_registration(cwd, env)
|
|
58
61
|
if not registration:
|
|
59
62
|
return "down", "Claude Code has no MCP server named fabric"
|
|
63
|
+
if directory.is_dir() and any(directory.glob("*-*.json")):
|
|
64
|
+
return _deliver_and_count(directory, env, deadline_s)
|
|
60
65
|
client: Optional[McpStdio] = None
|
|
61
66
|
try:
|
|
62
67
|
deadline = time.monotonic() + deadline_s
|
|
@@ -86,6 +91,35 @@ def poll_unread(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None
|
|
|
86
91
|
client.close()
|
|
87
92
|
|
|
88
93
|
|
|
94
|
+
def _deliver_and_count(directory: Path, env: Dict[str, str], deadline_s: float) -> Tuple[str, Any]:
|
|
95
|
+
"""FAA-89: the hooks' worker drains the spool through one rejoin and writes the board's unread count beside it;
|
|
96
|
+
this look reads that count instead of rejoining a second time within the minute."""
|
|
97
|
+
started = time.time()
|
|
98
|
+
deliver(directory, env, deadline_s)
|
|
99
|
+
if not directory.is_dir(): # the look delivered the session's end, and the worker removed its folder
|
|
100
|
+
return "down", "the session ended: its spooled turns were delivered"
|
|
101
|
+
board = directory / "board.json"
|
|
102
|
+
try:
|
|
103
|
+
fresh = board.stat().st_mtime >= started - 1
|
|
104
|
+
except OSError:
|
|
105
|
+
fresh = False
|
|
106
|
+
unread = None
|
|
107
|
+
if fresh:
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(board.read_text(encoding="utf-8"))
|
|
110
|
+
unread = data.get("unread") if isinstance(data, dict) else None
|
|
111
|
+
except (OSError, ValueError):
|
|
112
|
+
unread = None
|
|
113
|
+
if isinstance(unread, int) and not isinstance(unread, bool) and unread >= 0:
|
|
114
|
+
return "ok", unread
|
|
115
|
+
if fresh:
|
|
116
|
+
return "unavailable", "the project board cannot be read"
|
|
117
|
+
left = rejoin_wait(directory)
|
|
118
|
+
if left > 0:
|
|
119
|
+
return "wait", left
|
|
120
|
+
return "down", "the spooled turns could not be delivered now"
|
|
121
|
+
|
|
122
|
+
|
|
89
123
|
def watch_mail(session_id: str, cwd: Path, env: Optional[Dict[str, str]] = None, interval: float = 60, *,
|
|
90
124
|
poll: Optional[Callable[[], Tuple[str, Any]]] = None, out: Callable[[str], None] = print,
|
|
91
125
|
wait: Callable[[float], bool], stop: Callable[[], bool], rng: Callable[[], float]) -> int:
|