@passioncode-ai/fabric-agent-adapter 0.47.1 → 0.48.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 +38 -0
- package/README.md +1 -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 +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +31 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +42 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +32 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +51 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +37 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +95 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +161 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.48.1 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The Python usage kit rounds a cost as the Node twin does** (FAA-110, a risk of 0.48.0's blind reading). Python's
|
|
8
|
+
`round(x, 6)` rounds half to even on the exact binary value; Node's `Math.round(x * 1e6) / 1e6` rounds half up on the
|
|
9
|
+
double product, so the twins' `costUsd` and `spentUsd` could differ by a micro-dollar (927 of 3000 sampled values).
|
|
10
|
+
The Python kit now rounds as Node does (`_round_usd`), held to 3014 cases Node answers
|
|
11
|
+
(`test/fixtures/kit-rounding-cases.json`).
|
|
12
|
+
- **`usage_spend` adds call counts as doubles**, as the contract and the Node twin do, so a total past 2^53 agrees (a
|
|
13
|
+
new vector: three rows of 2^53 - 1 calls).
|
|
14
|
+
|
|
15
|
+
## 0.48.0 (2026-10-23)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Pinned to contract `2f020a6`**, its first tagged release `v0.1.0+2026-10-23` (FAA-109; `87d384f` stays
|
|
20
|
+
supported): DEC-0107, DEC-0109, DEC-0110 and DEC-0111. Every vendored vector is regenerated from the contract's own
|
|
21
|
+
functions at the pin.
|
|
22
|
+
- **`check_service.py` `well-known.requires-token` follows DEC-0109 (CT-114).** A remote service's no-token `401` may
|
|
23
|
+
carry an identity proxy's page: it passes when the page neither parses as the well-known document nor names the
|
|
24
|
+
service's id, `id.instance`, build or pid. The service's host name is taken out first, and a value shorter than four
|
|
25
|
+
characters is not looked for; a copy of the document anywhere inside a JSON body fails. The rule is recorded on every
|
|
26
|
+
path, a later request's timeout included. Fabric Inbox's remote placement on this machine now passes 34 of 34 rules (it failed
|
|
27
|
+
this one before).
|
|
28
|
+
- **A spending limit counts money only (DEC-0110, CT-115)**, both twins: `budget.spentUsd` and a machine limit's
|
|
29
|
+
`spentUsd` leave out a `subscription` route's `costUsd`, an estimate at list price; a window spent only on a
|
|
30
|
+
subscription spends `0`. New: `usage_spend` / `usageSpend`, the contract's `usageSpend`, held to 42 cases its own
|
|
31
|
+
function answers (`test/fixtures/contract/usage-spend-vectors.json`). A cost or a count past the double range answers
|
|
32
|
+
`None` in Python, as the contract answers `undefined`. The day's `costUsd` still sums every route.
|
|
33
|
+
- **Both estate readers read `deltaAnchor` (DEC-0107)**: `FAC-SEM-137` in the revision rules and the verdict, and
|
|
34
|
+
`delta_anchor_of` / `deltaAnchorOf`, held to the contract's fixtures and hostile variants. `FAC-SEM-075`'s message
|
|
35
|
+
says "a secret envelope", as the contract's.
|
|
36
|
+
- **The protocol register gains `call-record/1`, `credential-decision/1`, `credential-request/1`,
|
|
37
|
+
`credential-revocation/1` and `fabric-credential-envelope/1`** (DEC-0111), both twins. Credentials on request are a
|
|
38
|
+
node's and Project Observatory's duty, not the kit's.
|
|
39
|
+
- `references/remote-placement.md`, `usage.md`, `estate-record.md` and `protocol.md` say so.
|
|
40
|
+
|
|
3
41
|
## 0.47.1 (2026-10-23)
|
|
4
42
|
|
|
5
43
|
### Fixed
|
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ Scaffolding is non-destructive by default. It writes only the paths declared in
|
|
|
274
274
|
The default pin for new bundles is [`fabric-contract.lock.json`](fabric-contract.lock.json):
|
|
275
275
|
|
|
276
276
|
- version: `0.1.0`
|
|
277
|
-
- commit: `
|
|
277
|
+
- commit: `2f020a696453ae299b123517f14957f6a1354379`
|
|
278
278
|
|
|
279
279
|
Every other mention of the default contract revision (this section, the skills' metadata, the
|
|
280
280
|
skill card, `adapt_project.py`) must equal it; `python3 test/validate.py` fails a tree
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/fabric-agent-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.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.48.1",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,9 +5,9 @@ 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.48.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Adapting projects to Fabric
|
|
@@ -76,7 +76,7 @@ Use exactly:
|
|
|
76
76
|
|
|
77
77
|
- contract version `0.1.0`;
|
|
78
78
|
- repository `https://github.com/passioncode-ai/fabric-agent-contract`;
|
|
79
|
-
- commit `
|
|
79
|
+
- commit `2f020a696453ae299b123517f14957f6a1354379`.
|
|
80
80
|
|
|
81
81
|
This is the default for new bundles. When checking an issued legacy bundle, preserve its
|
|
82
82
|
explicit supported lock and use that exact clean checkout. Never silently upgrade or
|
package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py
CHANGED
|
@@ -22,10 +22,10 @@ import fabric_secrets as _secrets # noqa: E402 - the contract's credential dete
|
|
|
22
22
|
|
|
23
23
|
CONTRACT_VERSION = "0.1.0"
|
|
24
24
|
CONTRACT_REPOSITORY = "https://github.com/passioncode-ai/fabric-agent-contract"
|
|
25
|
-
CONTRACT_COMMIT = "
|
|
25
|
+
CONTRACT_COMMIT = "2f020a696453ae299b123517f14957f6a1354379"
|
|
26
26
|
# #region supported-contract-revisions — docs: README.md#contract-pin
|
|
27
27
|
# Only this declaration may name a legacy revision in live files.
|
|
28
|
-
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
28
|
+
SUPPORTED_CONTRACT_COMMITS = (CONTRACT_COMMIT, "87d384ffce2757081b7ab227e5dad3dddd08328f", "0a4ce773c4b9e6bec40c6387eb3f988af7152c0e", "e742016dc396aa31e5a13a893489e43f15786c69", "bfa9a67aa3c6cb808c093e7d83cd59799f1ee7d0", "c55f2d5a11c9a0197e958e87212fca7ae1154769", "07042181bfae2a481aa0da1a5651f24c76669c48", "d8146cfb0a1c74fc6432284c4356bb0c864b37b5", "82e163665e156a08aa3b92e4711015582c06162c", "1636fa97262f4a26e2086572d540c2ee701e0010", "f00a7685e3861c27f7899c033fb850a145a1708f", "a046f57cf46ea9e3da57ff8ce6a3ed4d26d8774b", "fe55fc67cba1a9e34a736da7d148d4fe2a8fbfa0", "b291bf09e0a9f9fd3c16cf540832cd021203b7d1", "0cacd08b0e95b6abf131eae1206b60792138bc3c", "2eb91c00e852d9c31d804310d38fad91589fcbac", "8e49fbd5fbbf230e32c1969cd24ac3c22422f0a5", "a83bec6b47776af688402c18efec507143061f68", "52da526d7cc1e063d74c5b520dbc03d0ace15246", "78b2da018d2c4bd9ac1bde1dc3b7f1a110a175f5", "623bf61358c339cb10297807b3f024b5d9f1f327", "23f9fda4c05f8a3852246ee98d4f2adf74ed0875", "9091d3d6606b0c4b5591a38c3674356a01fd7f55", "df55c8c54a23251342a7ee57ba95642b7eb39e61", "2ce392291c6668598d12cd38327e24696b5ca15c")
|
|
29
29
|
# #endregion supported-contract-revisions
|
|
30
30
|
INTEROP_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
31
31
|
MCP_REVISION = "2026-07-28"
|
|
@@ -14,10 +14,10 @@ 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.48.1"
|
|
18
18
|
contract-version: "0.1.0"
|
|
19
19
|
extension: "fabric-service/0.1"
|
|
20
|
-
extension-commit: "
|
|
20
|
+
extension-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
|
21
21
|
---
|
|
22
22
|
|
|
23
23
|
# Building Fabric services
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Fabric Agent Contract DEC-0055, `docs/specification/estate-record.md`, schema `schemas/estate-record.schema.json`,
|
|
4
4
|
rules `FAC-SEM-072…076`, with contexts (DEC-0082, `docs/specification/contexts.md`, `FAC-SEM-110`, `FAC-SEM-111`) and a
|
|
5
5
|
partner's scoped publisher key (DEC-0090, `docs/specification/partner-channel.md`, `FAC-SEM-122`) and each machine's
|
|
6
|
-
profile (DEC-0106, `docs/specification/machine-calls.md`, `FAC-SEM-134`)
|
|
6
|
+
profile (DEC-0106, `docs/specification/machine-calls.md`, `FAC-SEM-134`), and the revocation delta's head a
|
|
7
|
+
revision vouches for (DEC-0107, `deltaAnchor`, `FAC-SEM-137`).
|
|
7
8
|
One revision of one person's estate lists:
|
|
8
9
|
- the passkeys that sign it;
|
|
9
10
|
- the machines, with their device keys, a certificate of each machine's hub key and, optionally, a profile;
|
|
@@ -21,6 +22,7 @@ derived from its own digest. Python: [`scripts/fabric_estate.py`](../scripts/fab
|
|
|
21
22
|
- Trusting a delegation key
|
|
22
23
|
- Contexts: one agent for you and for an organization
|
|
23
24
|
- Machine profiles: full, member, view
|
|
25
|
+
- The delta anchor
|
|
24
26
|
- The estate ceremonies
|
|
25
27
|
- Share grants into an organization
|
|
26
28
|
- Checking a record you produce
|
|
@@ -59,7 +61,8 @@ It checks, in the order of the ER table:
|
|
|
59
61
|
7. **The window.** At most 30 days. A revision may be issued up to 60 s ahead of the verifier's clock (DEC-0066,
|
|
60
62
|
`CLOCK_SKEW_SECONDS`); earlier than that it is `not-yet-valid`.
|
|
61
63
|
8. **The record's own rules.** A violation is `rule-violation` with its `codes`, `FAC-SEM-072…076`, `FAC-SEM-110`,
|
|
62
|
-
`FAC-SEM-111`, `FAC-SEM-122`, `FAC-SEM-134` and `FAC-SEM-092`
|
|
64
|
+
`FAC-SEM-111`, `FAC-SEM-122`, `FAC-SEM-134` and `FAC-SEM-092`, and against the parent `FAC-SEM-114` and
|
|
65
|
+
`FAC-SEM-137`
|
|
63
66
|
(each passkey the record lists is a signing credential, its origins within its RP ID).
|
|
64
67
|
|
|
65
68
|
Dates are strict RFC 3339: a `T`, an offset, and a day that exists. `:60` and a space are refused. Passkey JWK `x`
|
|
@@ -160,6 +163,16 @@ own profile vectors (`fixtures/vectors/machine-call.json`) and hostile inputs (`
|
|
|
160
163
|
service is one yet (FAA-106). A delegation verifier still names the refusal: `profile-forbids` is a DL-19 code
|
|
161
164
|
([delegation](delegation.md)).
|
|
162
165
|
|
|
166
|
+
## The delta anchor
|
|
167
|
+
|
|
168
|
+
DEC-0107 lets a revision name the revocation delta's head it vouches for: `deltaAnchor {sequence, sha256}`. Every
|
|
169
|
+
revocation of the delta's entries up to it is in the record. `FAC-SEM-137` keeps it from moving back: once a parent
|
|
170
|
+
anchors the delta, its next revision anchors it too, at the parent's sequence or later, with the parent's hash at the
|
|
171
|
+
same sequence. `delta_anchor_of(record)` / `deltaAnchorOf(record)` answers the anchor, or `None` / `undefined` when the
|
|
172
|
+
record names none or one of another shape. Applying the delta against the anchor (`anchorDelta`) is a machine's duty
|
|
173
|
+
when it applies revisions; the kit verifies the revision and does not hold a delta. Fabric writes `deltaAnchor` only
|
|
174
|
+
once every reader is pinned at or after DEC-0107: update a copied kit first, as for `profile`.
|
|
175
|
+
|
|
163
176
|
## The estate ceremonies
|
|
164
177
|
|
|
165
178
|
Contract DEC-0084: create a network, add a machine nearby (a six-digit comparison code) or headless (an enroll token),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fabric-service/0.1: wire reference
|
|
2
2
|
|
|
3
|
-
Pinned to `fabric-agent-contract` commit `
|
|
3
|
+
Pinned to `fabric-agent-contract` commit `2f020a696453ae299b123517f14957f6a1354379`
|
|
4
4
|
(`docs/specification/service.md`, DEC-0015; the usage report DEC-0021; Windows and Linux supervision
|
|
5
5
|
DEC-0032 and DEC-0033; the operator channel DEC-0034 and DEC-0035; the embedded dashboard DEC-0036;
|
|
6
6
|
delegation and the operator session's lifetime DEC-0039; the meter's call window DEC-0041; session handoff DEC-0042;
|
|
@@ -12,7 +12,9 @@ by normal form DEC-0076; the project board's directory page DEC-0077 and a repla
|
|
|
12
12
|
DEC-0096; a schema pattern never uses a bare dot DEC-0097; the board as Fabric 0.3.9 serves it DEC-0098 and DEC-0102, a
|
|
13
13
|
`processor-hosted` peer DEC-0099, three device protocols in the register DEC-0101; a call queue's `queue-full` and
|
|
14
14
|
the `agent-directory`, `fabric-load` and `service-load` protocols DEC-0104; a machine's `profile` in the estate record,
|
|
15
|
-
`profile-forbids` and the `fabric-agent-call` protocol DEC-0106
|
|
15
|
+
`profile-forbids` and the `fabric-agent-call` protocol DEC-0106; the delta anchor DEC-0107; a remote service's
|
|
16
|
+
no-token `401` DEC-0109; money apart from a subscription's estimate DEC-0110; the five credential and call-record
|
|
17
|
+
protocols of DEC-0111 in the register). The contract's schemas are normative;
|
|
16
18
|
this page is the working summary. Extension key:
|
|
17
19
|
`https://fabric.passioncode.ai/agent-contract/extensions/service/0.1`.
|
|
18
20
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md
CHANGED
|
@@ -64,6 +64,16 @@ router answers it, usually `404` or `421`. The probe counts that as the refusal
|
|
|
64
64
|
the body is not the well-known document. The Origin and cross-site checks name the right `Host`,
|
|
65
65
|
reach the service, and still need its own `403`.
|
|
66
66
|
|
|
67
|
+
Without the token the well-known route answers `401` with none of the service's data (DEC-0109). The kit's server
|
|
68
|
+
answers it with an empty body. An identity proxy in front of the service (Cloudflare Access, for one) may answer that
|
|
69
|
+
`401` with its own sign-in page: `well-known.requires-token` passes it when the page neither parses as the well-known
|
|
70
|
+
document nor names the service's identity values (its id, `id.instance`, build, or pid, as whole words). The probe
|
|
71
|
+
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 anywhere inside a JSON body
|
|
73
|
+
fails, however short the id. One false FAIL remains possible: when the document was read, a proxy page that happens to
|
|
74
|
+
contain the service's pid as a whole number (an inline script's constant, say) fails the rule; the evidence names the
|
|
75
|
+
value it found.
|
|
76
|
+
|
|
67
77
|
## What a host does and will not do
|
|
68
78
|
|
|
69
79
|
| Situation | Host |
|
|
@@ -78,7 +78,11 @@ ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budget
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
- A machine limit with a `period` and no `spentUsd` is counted by the kit from the receipts, the
|
|
81
|
-
way `budget` is, so `budget` and its twin in `budgets` never disagree.
|
|
81
|
+
way `budget` is, so `budget` and its twin in `budgets` never disagree. Both count money only (DEC-0110):
|
|
82
|
+
a `subscription` route's `costUsd` is an estimate at list price, never spent money, so a window
|
|
83
|
+
spent only on a subscription spends `0`, and money is `null` only while no call off a subscription
|
|
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 per-order limit
|
|
82
86
|
(`per_job`, `approval`) gets `spentUsd: null`. Every other limit states its own spend.
|
|
83
87
|
- `scope` is `machine`, `project`, `pool` or `job`; a `project` or `pool` limit names its
|
|
84
88
|
`subject`. A limit has at most one window (`period`, `windowSeconds` or `since`), and it agrees
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
A remote placement (DEC-0019 — an online agent or dashboard at an https origin) is probed over
|
|
11
11
|
TLS with the certificate verified: the well-known document must refuse a request without the
|
|
12
|
-
token (401
|
|
12
|
+
token (401 with none of the service's data: an empty body, or an identity proxy's page, DEC-0109), the guards must refuse a foreign Host/Origin and cross-site requests,
|
|
13
13
|
and the session cookie must be __Host- and Secure. Loopback, launchd, lock and state rules do
|
|
14
14
|
not apply to it and are reported NOT_RUN with that reason.
|
|
15
15
|
|
|
@@ -62,7 +62,7 @@ from check_service_rules import PLATFORM_HOST_REFUSALS # noqa: E402,F401 - move
|
|
|
62
62
|
from check_service_rules import plist_problems # noqa: E402,F401 - moved (FAA-65)
|
|
63
63
|
from check_service_rules import marker_problem # noqa: E402,F401 - moved (FAA-65)
|
|
64
64
|
from check_service_rules import priority_problems # noqa: E402,F401 - moved (FAA-65)
|
|
65
|
-
from check_service_rules import guard_verdict # noqa: E402,F401 - moved (FAA-65)
|
|
65
|
+
from check_service_rules import guard_verdict, no_token_verdict # noqa: E402,F401 - moved (FAA-65)
|
|
66
66
|
from check_service_rules import board_responder_verdict # noqa: E402,F401 - moved (FAA-65)
|
|
67
67
|
from check_service_rules import session_lifetime_problem # noqa: E402,F401 - moved (FAA-65)
|
|
68
68
|
from check_service_rules import _semver # noqa: E402,F401 - moved (FAA-65)
|
|
@@ -190,10 +190,10 @@ class Probe:
|
|
|
190
190
|
except (OSError, ssl.SSLError) as exc:
|
|
191
191
|
self.add("well-known.answers", "FAIL", "no TLS answer on %s: %s" % (self.d.get("origin"), exc))
|
|
192
192
|
return
|
|
193
|
-
|
|
194
|
-
"HTTP %d without a token, %d byte(s)" % (status, len(body)))
|
|
193
|
+
no_token = (status, body) # judged once the document names the identity values (DEC-0109)
|
|
195
194
|
self.read_token()
|
|
196
195
|
if not self.token:
|
|
196
|
+
self.add_no_token(no_token, None)
|
|
197
197
|
self.add("well-known.answers", "NOT_RUN", "no readable token to ask with")
|
|
198
198
|
return
|
|
199
199
|
protected = self.auth_headers()
|
|
@@ -204,16 +204,24 @@ class Probe:
|
|
|
204
204
|
status, headers, body = self.request("GET", "/.well-known/fabric-service", protected)
|
|
205
205
|
timings.append((time.perf_counter() - started) * 1000)
|
|
206
206
|
except OSError as exc:
|
|
207
|
+
if self.remote:
|
|
208
|
+
self.add_no_token(no_token, None)
|
|
207
209
|
self.add("well-known.answers", "FAIL", "no answer on %s: %s" % (self.d.get("origin"), exc))
|
|
208
210
|
return
|
|
209
211
|
if status != 200:
|
|
212
|
+
if self.remote:
|
|
213
|
+
self.add_no_token(no_token, None)
|
|
210
214
|
self.add("well-known.answers", "FAIL", "HTTP %d" % status)
|
|
211
215
|
return
|
|
212
216
|
try:
|
|
213
217
|
self.wk = json.loads(body)
|
|
214
218
|
except ValueError:
|
|
219
|
+
if self.remote:
|
|
220
|
+
self.add_no_token(no_token, None)
|
|
215
221
|
self.add("well-known.answers", "FAIL", "body is not JSON")
|
|
216
222
|
return
|
|
223
|
+
if self.remote:
|
|
224
|
+
self.add_no_token(no_token, self.wk)
|
|
217
225
|
self.add("well-known.answers", "PASS", "HTTP 200")
|
|
218
226
|
median = sorted(timings)[1]
|
|
219
227
|
if self.remote:
|
|
@@ -257,6 +265,25 @@ class Probe:
|
|
|
257
265
|
self.add("well-known.ready-means-healthy", "FAIL" if ready_bad else "PASS",
|
|
258
266
|
"status %s, %d degraded" % (wk.get("status"), len(wk.get("degraded") or [])))
|
|
259
267
|
|
|
268
|
+
def add_no_token(self, answer: Tuple[int, bytes], wk: Any) -> None:
|
|
269
|
+
"""`well-known.requires-token` (DEC-0109): the no-token answer, against the identity values the descriptor
|
|
270
|
+
names and, when the document was read, its build and pid."""
|
|
271
|
+
status, body = answer
|
|
272
|
+
sid, instance = str(self.d.get("id") or ""), str(self.d.get("instance") or "default")
|
|
273
|
+
values = [sid, "%s.%s" % (sid, instance)]
|
|
274
|
+
if isinstance(wk, dict):
|
|
275
|
+
svc = wk.get("service") if isinstance(wk.get("service"), dict) else {}
|
|
276
|
+
build = svc.get("build") if isinstance(svc.get("build"), dict) else {}
|
|
277
|
+
process = wk.get("process") if isinstance(wk.get("process"), dict) else {}
|
|
278
|
+
if svc.get("id"):
|
|
279
|
+
values.append(str(svc["id"]))
|
|
280
|
+
values.append("%s.%s" % (svc["id"], svc.get("instance") or instance))
|
|
281
|
+
values += [str(build[k]) for k in ("commit", "digest") if build.get(k)]
|
|
282
|
+
if isinstance(process.get("pid"), int) and not isinstance(process.get("pid"), bool):
|
|
283
|
+
values.append(str(process["pid"]))
|
|
284
|
+
verdict, evidence = no_token_verdict(status, body, list(dict.fromkeys(values)), self.host_name)
|
|
285
|
+
self.add("well-known.requires-token", verdict, evidence)
|
|
286
|
+
|
|
260
287
|
# network ------------------------------------------------------------------
|
|
261
288
|
def network_rules(self) -> None:
|
|
262
289
|
for rule, headers in (
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py
CHANGED
|
@@ -88,6 +88,48 @@ def guard_verdict(rule: str, remote: bool, status: int, body: bytes) -> Tuple[st
|
|
|
88
88
|
return "FAIL", "HTTP %d%s" % (status, ", and the body is the well-known document" if disclosed else "")
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
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: its
|
|
93
|
+
`protocol` is the extension's, or it carries `service` or `process` (D3 of FAA-109)."""
|
|
94
|
+
if depth > 64:
|
|
95
|
+
return True # nothing honest nests that deep; refuse rather than recurse without bound
|
|
96
|
+
if isinstance(value, dict):
|
|
97
|
+
if value.get("protocol") == fs.PROTOCOL or "service" in value or "process" in value:
|
|
98
|
+
return True
|
|
99
|
+
return any(_carries_document(v, depth + 1) for v in value.values())
|
|
100
|
+
if isinstance(value, list):
|
|
101
|
+
return any(_carries_document(v, depth + 1) for v in value)
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def no_token_verdict(status: int, body: bytes, identities: List[str], host: str = "") -> Tuple[str, str]:
|
|
106
|
+
"""DEC-0109 (`well-known.requires-token` on a remote placement): the no-token answer is `401` and carries none of
|
|
107
|
+
the service's data. An empty body passes; so does an identity proxy's page in front of the service, as long as it
|
|
108
|
+
neither parses as the well-known document nor names one of the service's own identity values (its id, its
|
|
109
|
+
`id.instance`, its build or its pid, as whole words). The service's own host name is taken out first: a sign-in page
|
|
110
|
+
names the address it guards, and the address is not the document's data. A value shorter than four characters
|
|
111
|
+
names nothing on its own (a platform's pid is often 1) and is not looked for."""
|
|
112
|
+
if status != 401:
|
|
113
|
+
return "FAIL", "HTTP %d without a token" % status
|
|
114
|
+
if not body:
|
|
115
|
+
return "PASS", "HTTP 401 without a token, an empty body"
|
|
116
|
+
text = body.decode("utf-8", "replace")
|
|
117
|
+
try:
|
|
118
|
+
doc = json.loads(text)
|
|
119
|
+
except RecursionError:
|
|
120
|
+
return "FAIL", "HTTP 401, and the body nests deeper than any page does"
|
|
121
|
+
except ValueError:
|
|
122
|
+
doc = None
|
|
123
|
+
if _carries_document(doc):
|
|
124
|
+
return "FAIL", "HTTP 401, and the body carries the well-known document"
|
|
125
|
+
if host:
|
|
126
|
+
text = re.sub(re.escape(host), " ", text, flags=re.IGNORECASE)
|
|
127
|
+
for value in identities:
|
|
128
|
+
if len(value) >= 4 and re.search(r"(?<![A-Za-z0-9_.-])%s(?![A-Za-z0-9_-])" % re.escape(value), text):
|
|
129
|
+
return "FAIL", "HTTP 401, and the %d-byte body names the service's identity value %r" % (len(body), value)
|
|
130
|
+
return "PASS", "HTTP 401 without a token, a %d-byte page that is not the service's (an identity proxy's, DEC-0109)" % len(body)
|
|
131
|
+
|
|
132
|
+
|
|
91
133
|
def board_responder_verdict(wk: Any, kit: Optional[str] = None) -> Tuple[str, str]:
|
|
92
134
|
"""board.responder: NOT_RUN when the well-known document declares no fabric-project-comms revision; FAIL when it
|
|
93
135
|
declares one this kit does not read or the kit's fixture conversation finds a problem; PASS otherwise. `kit` is the
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// it has checked the revision against estate-record.schema.json (the verifier does not check the schema);
|
|
7
7
|
// - estateRecordRules(kind, value) — FAC-SEM-072…076, with FAC-SEM-134 on each machine's profile (DEC-0106);
|
|
8
8
|
// - machineProfile(record, machineId) — a machine's profile (DEC-0106): `full`, `member`, `view`, or undefined;
|
|
9
|
+
// - deltaAnchorOf(record) — the revocation delta's head a revision vouches for (DEC-0107), or undefined; a revision
|
|
10
|
+
// never moves it back (FAC-SEM-137, in the revision rules and the verdict);
|
|
9
11
|
// - hubKeyCertified(record, kid, x, at) — DL-14: a delegation verifier that reads the estate record trusts a token's
|
|
10
12
|
// `kid` only while a current revision certifies it for a machine (fabric-delegation.mjs calls it).
|
|
11
13
|
//
|
|
@@ -228,7 +230,7 @@ export function verifyEstateRecord(record, trust) {
|
|
|
228
230
|
if (at < issuedAt - CLOCK_SKEW_SECONDS * 1000) return { ok: false, problem: 'not-yet-valid' };
|
|
229
231
|
if (at > validUntil) return { ok: false, problem: 'expired' };
|
|
230
232
|
// With a parent, what the revision drops it revokes (FAC-SEM-114, DEC-0084): a removed machine, a replaced key.
|
|
231
|
-
const dropped = isObject(trust.parent) ? droppedFindings(trust.parent, record) : [];
|
|
233
|
+
const dropped = isObject(trust.parent) ? [...droppedFindings(trust.parent, record), ...anchorFindings(trust.parent, record)] : [];
|
|
232
234
|
const codes = [...new Set([...recordFindings(record), ...dropped].map((finding) => finding.code))];
|
|
233
235
|
if (codes.length > 0) return { ok: false, problem: 'rule-violation', codes };
|
|
234
236
|
return { ok: true, digest, revision: record.revision, signedBy: credential.credentialId, signCount: assertion.signCount };
|
|
@@ -460,7 +462,7 @@ function secretReferences(value, flag) {
|
|
|
460
462
|
if (!placed.has(keyOf(secret.agentRef, contextOf(secret)))) flag(`${at}/agentRef`, "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");
|
|
461
463
|
if (!machineIds.has(secret.from)) flag(`${at}/from`, 'the slot is filled from a machine of this estate');
|
|
462
464
|
// An envelope names no context (DEC-0082), so an organization's instance fills its slot on its own machine.
|
|
463
|
-
else if (contextOf(secret) !== PERSONAL_CONTEXT && secret.from !== machine.id) flag(`${at}/from`, "an organization's instance fills its slot from its own machine's vault, never by
|
|
465
|
+
else if (contextOf(secret) !== PERSONAL_CONTEXT && secret.from !== machine.id) flag(`${at}/from`, "an organization's instance fills its slot from its own machine's vault, never by a secret envelope from another machine");
|
|
464
466
|
const key = keyOf(secret.agentRef, secret.slot, contextOf(secret));
|
|
465
467
|
if (slots.has(key)) flag(`${at}/slot`, `slot ${show(secret.slot)} is placed twice for one agent in one context on one machine`);
|
|
466
468
|
slots.add(key);
|
|
@@ -642,6 +644,29 @@ function installation(value) {
|
|
|
642
644
|
return findings;
|
|
643
645
|
}
|
|
644
646
|
|
|
647
|
+
/** The anchor a record vouches for (DEC-0107), or undefined when it names none or its shape is not the schema's. */
|
|
648
|
+
export function deltaAnchorOf(record) {
|
|
649
|
+
if (!isObject(record) || !isObject(record.deltaAnchor)) return undefined;
|
|
650
|
+
const { sequence, sha256 } = record.deltaAnchor;
|
|
651
|
+
return Number.isSafeInteger(sequence) && sequence >= 1 && isString(sha256) && /^[a-f0-9]{64}$/.test(sha256) ? { sequence, sha256 } : undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* FAC-SEM-137 across two revisions (DEC-0107): the delta anchor never moves back. A revision whose parent anchors the
|
|
656
|
+
* delta anchors it too, at the parent's sequence or later, with the parent's hash at the same sequence.
|
|
657
|
+
*/
|
|
658
|
+
function anchorFindings(parent, next) {
|
|
659
|
+
const findings = [];
|
|
660
|
+
const flag = flagger(findings, 'FAC-SEM-137');
|
|
661
|
+
const before = deltaAnchorOf(parent);
|
|
662
|
+
if (before === undefined) return findings;
|
|
663
|
+
const after = deltaAnchorOf(next);
|
|
664
|
+
if (after === undefined) flag('/next/deltaAnchor', `the parent anchors the delta at ${before.sequence}: a revision keeps an anchor once one is set`);
|
|
665
|
+
else if (after.sequence < before.sequence) flag('/next/deltaAnchor/sequence', `the anchor moves back from ${before.sequence} to ${after.sequence}`);
|
|
666
|
+
else if (after.sequence === before.sequence && after.sha256 !== before.sha256) flag('/next/deltaAnchor/sha256', `another hash at sequence ${after.sequence}: two delta histories`);
|
|
667
|
+
return findings;
|
|
668
|
+
}
|
|
669
|
+
|
|
645
670
|
/**
|
|
646
671
|
* FAC-SEM-114 across two revisions (DEC-0084): what a revision drops, it revokes in the same revision. A removed machine
|
|
647
672
|
* as `machine <id>` and its hub key as `hub-key <kid>`; a replaced or dropped device key of a kept machine as `device-key`
|
|
@@ -680,7 +705,11 @@ function droppedFindings(parent, next) {
|
|
|
680
705
|
export function estateRecordRules(kind, value) {
|
|
681
706
|
const doc = isObject(value) ? value : {};
|
|
682
707
|
if (kind === 'estate-record') return recordFindings(doc);
|
|
683
|
-
if (kind === 'estate-record-revision')
|
|
708
|
+
if (kind === 'estate-record-revision') {
|
|
709
|
+
const parent = isObject(doc.parent) ? doc.parent : {};
|
|
710
|
+
const next = isObject(doc.next) ? doc.next : {};
|
|
711
|
+
return [...revisionChain(doc), ...droppedFindings(parent, next), ...anchorFindings(parent, next)];
|
|
712
|
+
}
|
|
684
713
|
if (kind === 'estate-placement') return installation(doc);
|
|
685
714
|
return undefined;
|
|
686
715
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs
CHANGED
|
@@ -7,8 +7,8 @@ import { credentialName, secretArgument, secretUrl, secretValue } from './fabric
|
|
|
7
7
|
|
|
8
8
|
/** Each contract protocol's published revisions, oldest first (the contract's `PROTOCOL_REVISIONS`); a test holds it equal to the contract's. */
|
|
9
9
|
export const PROTOCOL_REVISIONS = new Map(Object.entries({
|
|
10
|
-
'access-log': ['1'], approval: ['1'], 'activity-summary': ['1'], 'agent-directory': ['1'], 'coding-agents': ['1'], 'enroll-token': ['1'], entitlement: ['1'], 'entitlement-key-set': ['1'], 'estate-record': ['1'], 'execution-packet': ['1'],
|
|
11
|
-
'fabric-activity': ['0.1'], 'fabric-agent-call': ['1'], 'fabric-connections': ['0.1'], 'fabric-load': ['0.1'], 'fabric-dashboard': ['0.1'], 'fabric-delegation': ['0.1'], 'fabric-device': ['0.1'],
|
|
10
|
+
'access-log': ['1'], approval: ['1'], 'activity-summary': ['1'], 'agent-directory': ['1'], 'call-record': ['1'], 'coding-agents': ['1'], 'credential-decision': ['1'], 'credential-request': ['1'], 'credential-revocation': ['1'], 'enroll-token': ['1'], entitlement: ['1'], 'entitlement-key-set': ['1'], 'estate-record': ['1'], 'execution-packet': ['1'],
|
|
11
|
+
'fabric-activity': ['0.1'], 'fabric-agent-call': ['1'], 'fabric-connections': ['0.1'], 'fabric-credential-envelope': ['1'], 'fabric-load': ['0.1'], 'fabric-dashboard': ['0.1'], 'fabric-delegation': ['0.1'], 'fabric-device': ['0.1'],
|
|
12
12
|
'fabric-interop': ['0.1', '0.2'], 'fabric-local-channel': ['0.1'], 'fabric-local-runner': ['0.1'], 'fabric-operator-channel': ['0.1'],
|
|
13
13
|
'fabric-package': ['1'], 'fabric-project-comms': ['0.1'], 'fabric-proposal': ['1'], 'fabric-provider': ['0.1'], 'fabric-relay': ['0.1'],
|
|
14
14
|
'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], heartbeat: ['1'], 'learning-note': ['1'],
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs
CHANGED
|
@@ -310,21 +310,65 @@ export function usageReport(receipts, { id, instance = 'default', now = Date.now
|
|
|
310
310
|
return report;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* What a host shows for usage rows (DEC-0110, the contract's usageSpend): money spent apart from a subscription's
|
|
315
|
+
* estimate. `rows` are `days[].byModel[]` rows. Money is the `costUsd` of every row whose `route` is `key`,
|
|
316
|
+
* `platform-metered` or absent; a `subscription` row's `costUsd` is an estimate at list price, summed apart. A window
|
|
317
|
+
* whose calls were all on a subscription spent 0; money is null only when it has calls off a subscription and none of
|
|
318
|
+
* them is priced, and a lower bound while some are priced and some are not. An unknown `route`, or counts or a cost the
|
|
319
|
+
* schema refuses, answers undefined. It never throws on a JSON value.
|
|
320
|
+
*/
|
|
321
|
+
export function usageSpend(rows) {
|
|
322
|
+
if (!Array.isArray(rows)) return undefined;
|
|
323
|
+
const known = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
324
|
+
const money = [], estimate = [];
|
|
325
|
+
let calls = 0, subscriptionCalls = 0;
|
|
326
|
+
const paid = { calls: 0, priced: 0, unpriced: false }, sub = { calls: 0, priced: 0, unpriced: false };
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
if (!isPlainObject(row)) return undefined;
|
|
329
|
+
const { route } = row;
|
|
330
|
+
if (route !== undefined && !(typeof route === 'string' && USAGE_ROUTES.includes(route))) return undefined;
|
|
331
|
+
const rowCalls = row.calls, unpriced = row.unpricedCalls === undefined ? 0 : row.unpricedCalls, rowCost = row.costUsd;
|
|
332
|
+
if (!Number.isSafeInteger(rowCalls) || rowCalls < 0 || !Number.isSafeInteger(unpriced) || unpriced < 0 || unpriced > rowCalls) return undefined;
|
|
333
|
+
if (rowCost !== null && !known(rowCost)) return undefined;
|
|
334
|
+
if (rowCalls > 0 && (rowCost === null) !== (unpriced === rowCalls)) return undefined; // FAC-SEM-025's own pairing
|
|
335
|
+
calls += rowCalls;
|
|
336
|
+
const side = route === 'subscription' ? sub : paid;
|
|
337
|
+
if (route === 'subscription') subscriptionCalls += rowCalls;
|
|
338
|
+
side.calls += rowCalls;
|
|
339
|
+
side.priced += rowCalls - unpriced;
|
|
340
|
+
if (unpriced > 0) side.unpriced = true;
|
|
341
|
+
if (known(rowCost)) (route === 'subscription' ? estimate : money).push(rowCost);
|
|
342
|
+
}
|
|
343
|
+
const total = (side, costs) => (side.calls > 0 && side.priced === 0 ? null : costs.sort((a, b) => a - b).reduce((n, x) => n + x, 0));
|
|
344
|
+
return {
|
|
345
|
+
calls,
|
|
346
|
+
moneyUsd: total(paid, money),
|
|
347
|
+
moneyLowerBound: paid.priced > 0 && paid.unpriced,
|
|
348
|
+
estimateUsd: total(sub, estimate),
|
|
349
|
+
estimateLowerBound: sub.priced > 0 && sub.unpriced,
|
|
350
|
+
subscriptionCalls,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** CT-115: a limit counts money only, never a subscription's estimate (DEC-0110). */
|
|
355
|
+
function money(spend) {
|
|
356
|
+
return spend === undefined || spend.moneyUsd === null ? null : Math.round(spend.moneyUsd * 1e6) / 1e6;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Money spent in a rolling window, from the receipts' own times; null when no call off a subscription in it is priced. */
|
|
314
360
|
function spentSince(receipts, start, end) {
|
|
315
361
|
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
316
362
|
const lo = iso(start), hi = iso(end);
|
|
317
363
|
const inside = [...receipts].filter((r) => String(r.at ?? '') > lo && String(r.at ?? '') <= hi);
|
|
318
|
-
|
|
319
|
-
return
|
|
364
|
+
const priced = (r) => r.costUsd !== null && r.costUsd !== undefined;
|
|
365
|
+
return money(usageSpend(inside.map((r) => ({ ...(r.route ? { route: r.route } : {}), calls: 1, unpricedCalls: priced(r) ? 0 : 1, costUsd: priced(r) ? r.costUsd : null }))));
|
|
320
366
|
}
|
|
321
367
|
|
|
322
|
-
/**
|
|
368
|
+
/** Money spent in the current UTC day or month, over the window's model rows; null when no call off a subscription in it is priced. */
|
|
323
369
|
function spentIn(outDays, period, last) {
|
|
324
370
|
const prefix = period === 'day' ? last : last.slice(0, 7);
|
|
325
|
-
|
|
326
|
-
const unknown = window.length > 0 && window.every((d) => d.costUsd === null);
|
|
327
|
-
return unknown ? null : Math.round(window.reduce((n, d) => n + (d.costUsd ?? 0), 0) * 1e6) / 1e6;
|
|
371
|
+
return money(usageSpend(outDays.filter((d) => d.date.startsWith(prefix)).flatMap((d) => d.byModel)));
|
|
328
372
|
}
|
|
329
373
|
|
|
330
374
|
export const LIMIT_SCOPES = Object.freeze(['machine', 'project', 'pool', 'job']);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py
CHANGED
|
@@ -12,6 +12,8 @@ at the pin, ported answer for answer and held to the contract's own vectors (`te
|
|
|
12
12
|
- ``estate_record_rules(kind, value)`` — FAC-SEM-072…076 for `estate-record`, `estate-record-revision` and
|
|
13
13
|
`estate-placement`, with FAC-SEM-134 on each machine's profile (DEC-0106);
|
|
14
14
|
- ``machine_profile(record, machine_id)`` — a machine's profile (DEC-0106): `full`, `member`, `view`, or None;
|
|
15
|
+
- ``delta_anchor_of(record)`` — the revocation delta's head a revision vouches for (DEC-0107), or None; a revision
|
|
16
|
+
never moves it back (FAC-SEM-137, in the revision rules and the verdict);
|
|
15
17
|
- ``hub_key_certified(record, kid, x, at)`` — DL-14: a delegation verifier that reads the estate record trusts a token's
|
|
16
18
|
`kid` only while a current revision certifies it for a machine (``fabric_delegation`` calls it).
|
|
17
19
|
|
|
@@ -356,7 +358,7 @@ def verify_estate_record(record: Any, *, at: Any, parent: Any = _MISSING, author
|
|
|
356
358
|
return {"ok": False, "problem": "expired"}
|
|
357
359
|
codes: List[str] = []
|
|
358
360
|
# With a parent, what the revision drops it revokes (FAC-SEM-114, DEC-0084): a removed machine, a replaced key.
|
|
359
|
-
dropped = _dropped_findings(parent, record) if isinstance(parent, dict) else []
|
|
361
|
+
dropped = _dropped_findings(parent, record) + _anchor_findings(parent, record) if isinstance(parent, dict) else []
|
|
360
362
|
for finding in _record_findings(record) + dropped:
|
|
361
363
|
if finding["code"] not in codes:
|
|
362
364
|
codes.append(finding["code"])
|
|
@@ -640,7 +642,7 @@ def _secret_references(value: Dict[str, Any], flag) -> None:
|
|
|
640
642
|
flag("%s/from" % at, "the slot is filled from a machine of this estate")
|
|
641
643
|
elif not _same(_context_of(secret), PERSONAL_CONTEXT) and not _same(_get(secret, "from"), _get(machine, "id")):
|
|
642
644
|
# An envelope names no context (DEC-0082): an organization's instance fills its slot on its own machine.
|
|
643
|
-
flag("%s/from" % at, "an organization's instance fills its slot from its own machine's vault, never by
|
|
645
|
+
flag("%s/from" % at, "an organization's instance fills its slot from its own machine's vault, never by a secret envelope from another machine")
|
|
644
646
|
key = _key_of(_get(secret, "agentRef"), _get(secret, "slot"), _context_of(secret))
|
|
645
647
|
if key in slots:
|
|
646
648
|
flag("%s/slot" % at, "slot %s is placed twice for one agent in one context on one machine" % _show(_get(secret, "slot")))
|
|
@@ -782,6 +784,38 @@ def _placement_grants(value: Dict[str, Any], flag) -> None:
|
|
|
782
784
|
flag("/machines/%d/packages/%d%s" % (m, p, suffix), message)
|
|
783
785
|
|
|
784
786
|
|
|
787
|
+
_ANCHOR_HASH = re.compile(r"[a-f0-9]{64}")
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def delta_anchor_of(record: Any) -> Optional[Dict[str, Any]]:
|
|
791
|
+
"""The anchor a record vouches for (DEC-0107), or None when it names none or its shape is not the schema's. The
|
|
792
|
+
contract's ``deltaAnchorOf``."""
|
|
793
|
+
if not isinstance(record, dict) or not isinstance(_get(record, "deltaAnchor"), dict):
|
|
794
|
+
return None
|
|
795
|
+
sequence, sha = _get(record["deltaAnchor"], "sequence"), _get(record["deltaAnchor"], "sha256")
|
|
796
|
+
if _safe_integer(sequence) and sequence >= 1 and isinstance(sha, str) and _ANCHOR_HASH.fullmatch(sha):
|
|
797
|
+
return {"sequence": sequence, "sha256": sha}
|
|
798
|
+
return None
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _anchor_findings(parent: Dict[str, Any], nxt: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
802
|
+
"""FAC-SEM-137 across two revisions (DEC-0107): the delta anchor never moves back. A revision whose parent anchors
|
|
803
|
+
the delta anchors it too, at the parent's sequence or later, with the parent's hash at the same sequence."""
|
|
804
|
+
findings: List[Dict[str, str]] = []
|
|
805
|
+
flag = _finding(findings, "FAC-SEM-137")
|
|
806
|
+
before = delta_anchor_of(parent)
|
|
807
|
+
if before is None:
|
|
808
|
+
return findings
|
|
809
|
+
after = delta_anchor_of(nxt)
|
|
810
|
+
if after is None:
|
|
811
|
+
flag("/next/deltaAnchor", "the parent anchors the delta at %s: a revision keeps an anchor once one is set" % _js_string(before["sequence"]))
|
|
812
|
+
elif after["sequence"] < before["sequence"]:
|
|
813
|
+
flag("/next/deltaAnchor/sequence", "the anchor moves back from %s to %s" % (_js_string(before["sequence"]), _js_string(after["sequence"])))
|
|
814
|
+
elif after["sequence"] == before["sequence"] and after["sha256"] != before["sha256"]:
|
|
815
|
+
flag("/next/deltaAnchor/sha256", "another hash at sequence %s: two delta histories" % _js_string(after["sequence"]))
|
|
816
|
+
return findings
|
|
817
|
+
|
|
818
|
+
|
|
785
819
|
def _dropped_findings(parent: Dict[str, Any], nxt: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
786
820
|
"""FAC-SEM-114 across two revisions (DEC-0084): what a revision drops, it revokes in the same revision. A removed
|
|
787
821
|
machine as ``machine <id>`` and its hub key as ``hub-key <kid>``; a replaced or dropped device key of a kept machine
|
|
@@ -895,7 +929,7 @@ def estate_record_rules(kind: str, value: Any) -> Optional[List[Dict[str, str]]]
|
|
|
895
929
|
if kind == "estate-record-revision":
|
|
896
930
|
parent = value["parent"] if isinstance(_get(value, "parent"), dict) else {}
|
|
897
931
|
nxt = value["next"] if isinstance(_get(value, "next"), dict) else {}
|
|
898
|
-
return _revision_chain(value) + _dropped_findings(parent, nxt)
|
|
932
|
+
return _revision_chain(value) + _dropped_findings(parent, nxt) + _anchor_findings(parent, nxt)
|
|
899
933
|
if kind == "estate-placement":
|
|
900
934
|
return _installation(value)
|
|
901
935
|
return None
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py
CHANGED
|
@@ -13,9 +13,9 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
13
13
|
#: Each contract protocol's published revisions, oldest first (the contract's `PROTOCOL_REVISIONS`, DEC-0067); a test
|
|
14
14
|
#: holds it equal to the contract's register at the pin.
|
|
15
15
|
PROTOCOL_REVISIONS: Dict[str, Tuple[str, ...]] = {
|
|
16
|
-
"access-log": ("1",), "approval": ("1",), "activity-summary": ("1",), "agent-directory": ("1",), "coding-agents": ("1",), "enroll-token": ("1",), "entitlement": ("1",),
|
|
16
|
+
"access-log": ("1",), "approval": ("1",), "activity-summary": ("1",), "agent-directory": ("1",), "call-record": ("1",), "coding-agents": ("1",), "credential-decision": ("1",), "credential-request": ("1",), "credential-revocation": ("1",), "enroll-token": ("1",), "entitlement": ("1",),
|
|
17
17
|
"entitlement-key-set": ("1",), "estate-record": ("1",), "execution-packet": ("1",),
|
|
18
|
-
"fabric-activity": ("0.1",), "fabric-agent-call": ("1",), "fabric-connections": ("0.1",), "fabric-load": ("0.1",), "fabric-dashboard": ("0.1",), "fabric-delegation": ("0.1",), "fabric-device": ("0.1",),
|
|
18
|
+
"fabric-activity": ("0.1",), "fabric-agent-call": ("1",), "fabric-connections": ("0.1",), "fabric-credential-envelope": ("1",), "fabric-load": ("0.1",), "fabric-dashboard": ("0.1",), "fabric-delegation": ("0.1",), "fabric-device": ("0.1",),
|
|
19
19
|
"fabric-interop": ("0.1", "0.2"), "fabric-local-channel": ("0.1",), "fabric-local-runner": ("0.1",),
|
|
20
20
|
"fabric-operator-channel": ("0.1",), "fabric-package": ("1",), "fabric-project-comms": ("0.1",), "fabric-proposal": ("1",),
|
|
21
21
|
"fabric-provider": ("0.1",), "fabric-relay": ("0.1",), "fabric-secret-envelope": ("1",), "fabric-service": ("0.1",),
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py
CHANGED
|
@@ -157,9 +157,19 @@ def _add_totals(row: Dict[str, Any], receipt: Dict[str, Any]) -> None:
|
|
|
157
157
|
row["_cost"] += float(receipt["costUsd"])
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def _round_usd(value: float) -> float:
|
|
161
|
+
"""Math.round(value * 1e6) / 1e6, as the Node twin rounds: half up on the double product, where Python's round()
|
|
162
|
+
is half to even on the exact binary value, so the twins differed by 1e-6 (FAA-110)."""
|
|
163
|
+
product = value * 1e6
|
|
164
|
+
whole = math.floor(product)
|
|
165
|
+
if product - whole >= 0.5:
|
|
166
|
+
whole += 1
|
|
167
|
+
return whole / 1e6
|
|
168
|
+
|
|
169
|
+
|
|
160
170
|
def _close_totals(row: Dict[str, Any]) -> Dict[str, Any]:
|
|
161
171
|
cost = row.pop("_cost")
|
|
162
|
-
row["costUsd"] = None if row["calls"] and row["unpricedCalls"] == row["calls"] else
|
|
172
|
+
row["costUsd"] = None if row["calls"] and row["unpricedCalls"] == row["calls"] else _round_usd(cost)
|
|
163
173
|
return row
|
|
164
174
|
|
|
165
175
|
|
|
@@ -187,7 +197,7 @@ def _exceeds(a: float, b: float) -> bool:
|
|
|
187
197
|
|
|
188
198
|
|
|
189
199
|
def _usd(value: float) -> str:
|
|
190
|
-
rounded =
|
|
200
|
+
rounded = _round_usd(value)
|
|
191
201
|
return str(int(rounded)) if rounded == int(rounded) else repr(rounded)
|
|
192
202
|
|
|
193
203
|
|
|
@@ -494,21 +504,95 @@ def usage_report(receipts: Iterable[Dict[str, Any]], *, service_id: str, instanc
|
|
|
494
504
|
return report
|
|
495
505
|
|
|
496
506
|
|
|
507
|
+
def usage_spend(rows: Any) -> Optional[Dict[str, Any]]:
|
|
508
|
+
"""What a host shows for usage rows (DEC-0110, the contract's ``usageSpend``): money spent apart from a
|
|
509
|
+
subscription's estimate. ``rows`` are ``days[].byModel[]`` rows. Money is the ``costUsd`` of every row whose
|
|
510
|
+
``route`` is ``key``, ``platform-metered`` or absent; a ``subscription`` row's ``costUsd`` is an estimate at list
|
|
511
|
+
price, summed apart. A window whose calls were all on a subscription spent 0; money is None only when it has calls
|
|
512
|
+
off a subscription and none of them is priced, and a lower bound while some are priced and some are not. An unknown
|
|
513
|
+
``route``, or counts or a cost the schema refuses, answers None. It never raises on a JSON value."""
|
|
514
|
+
if not isinstance(rows, list):
|
|
515
|
+
return None
|
|
516
|
+
money: List[float] = []
|
|
517
|
+
estimate: List[float] = []
|
|
518
|
+
calls = subscription_calls = 0
|
|
519
|
+
paid = {"calls": 0, "priced": 0, "unpriced": False}
|
|
520
|
+
sub = {"calls": 0, "priced": 0, "unpriced": False}
|
|
521
|
+
for row in rows:
|
|
522
|
+
if not isinstance(row, dict):
|
|
523
|
+
return None
|
|
524
|
+
route = row.get("route", _jsv.MISSING)
|
|
525
|
+
if route is not _jsv.MISSING and not (isinstance(route, str) and route in USAGE_ROUTES):
|
|
526
|
+
return None
|
|
527
|
+
row_calls, cost = row.get("calls", _jsv.MISSING), row.get("costUsd", _jsv.MISSING)
|
|
528
|
+
unpriced = row.get("unpricedCalls", 0)
|
|
529
|
+
if not _whole(row_calls) or not _whole(unpriced) or unpriced > row_calls:
|
|
530
|
+
return None
|
|
531
|
+
if cost is not None and not _known_cost(cost):
|
|
532
|
+
return None
|
|
533
|
+
if row_calls > 0 and (cost is None) != (unpriced == row_calls): # FAC-SEM-025's own pairing
|
|
534
|
+
return None
|
|
535
|
+
calls += _jsv.double(row_calls) # a double, as the contract adds: past 2**53 the sums round alike (FAA-110)
|
|
536
|
+
side = sub if route == "subscription" else paid
|
|
537
|
+
if route == "subscription":
|
|
538
|
+
subscription_calls += _jsv.double(row_calls)
|
|
539
|
+
side["calls"] += int(row_calls)
|
|
540
|
+
side["priced"] += int(row_calls) - int(unpriced)
|
|
541
|
+
if unpriced > 0:
|
|
542
|
+
side["unpriced"] = True
|
|
543
|
+
if _known_cost(cost):
|
|
544
|
+
(estimate if route == "subscription" else money).append(_jsv.double(cost))
|
|
545
|
+
|
|
546
|
+
def total(side: Dict[str, Any], costs: List[float]) -> Optional[float]:
|
|
547
|
+
if side["calls"] > 0 and side["priced"] == 0:
|
|
548
|
+
return None
|
|
549
|
+
n: Any = 0
|
|
550
|
+
for x in sorted(costs): # in a fixed order, one addition at a time, as JavaScript's reduce adds
|
|
551
|
+
n = n + x
|
|
552
|
+
return n
|
|
553
|
+
|
|
554
|
+
return {"calls": int(calls), "moneyUsd": total(paid, money), "moneyLowerBound": paid["priced"] > 0 and paid["unpriced"],
|
|
555
|
+
"estimateUsd": total(sub, estimate), "estimateLowerBound": sub["priced"] > 0 and sub["unpriced"],
|
|
556
|
+
"subscriptionCalls": int(subscription_calls)}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _whole(value: Any) -> bool:
|
|
560
|
+
"""Number.isSafeInteger(value) and not negative."""
|
|
561
|
+
return _jsv.safe_integer(value) and value >= 0
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _known_cost(value: Any) -> bool:
|
|
565
|
+
"""A finite cost of at least 0, the number read as JavaScript holds it (an integer past the double range is
|
|
566
|
+
Infinity, never an OverflowError)."""
|
|
567
|
+
return _number(value) and math.isfinite(_jsv.double(value)) and value >= 0
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _money(spend: Optional[Dict[str, Any]]) -> Optional[float]:
|
|
571
|
+
"""CT-115: a limit counts money only, never a subscription's estimate (DEC-0110)."""
|
|
572
|
+
if spend is None or spend["moneyUsd"] is None:
|
|
573
|
+
return None
|
|
574
|
+
return _round_usd(float(spend["moneyUsd"]))
|
|
575
|
+
|
|
576
|
+
|
|
497
577
|
def _spent_since(receipts: List[Dict[str, Any]], start: _dt.datetime, end: _dt.datetime) -> Optional[float]:
|
|
498
|
-
"""
|
|
578
|
+
"""Money spent in a rolling window, from the receipts' own times; None when no call off a subscription in it is
|
|
579
|
+
priced. A subscription's calls cost no money (DEC-0110, CT-115)."""
|
|
499
580
|
lo, hi = start.strftime("%Y-%m-%dT%H:%M:%SZ"), end.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
581
|
+
rows = []
|
|
582
|
+
for r in receipts:
|
|
583
|
+
if lo < str(r.get("at", "")) <= hi:
|
|
584
|
+
row = {"calls": 1, "unpricedCalls": 1 if r.get("costUsd") is None else 0, "costUsd": r.get("costUsd")}
|
|
585
|
+
if r.get("route"):
|
|
586
|
+
row["route"] = r["route"]
|
|
587
|
+
rows.append(row)
|
|
588
|
+
return _money(usage_spend(rows))
|
|
504
589
|
|
|
505
590
|
|
|
506
591
|
def _spent_in(out_days: List[Dict[str, Any]], period: str, last: str) -> Optional[float]:
|
|
507
|
-
"""
|
|
592
|
+
"""Money spent in the current UTC day or month, over the window's model rows; None when no call off a subscription
|
|
593
|
+
in it is priced. A subscription's calls cost no money (DEC-0110, CT-115)."""
|
|
508
594
|
prefix = last if period == "day" else last[:7]
|
|
509
|
-
|
|
510
|
-
unknown = bool(window) and all(d["costUsd"] is None for d in window)
|
|
511
|
-
return None if unknown else round(sum(d["costUsd"] or 0.0 for d in window), 6)
|
|
595
|
+
return _money(usage_spend([m for d in out_days if d["date"].startswith(prefix) for m in d["byModel"]]))
|
|
512
596
|
|
|
513
597
|
|
|
514
598
|
LIMIT_SCOPES = ("machine", "project", "pool", "job")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabric-service-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.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
|
{
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"fabric_secrets",
|
|
78
78
|
"fabric_time"
|
|
79
79
|
],
|
|
80
|
-
"sha256": "
|
|
80
|
+
"sha256": "f493cd424dae6380d1caad91ffdca2764e51a49a6b1051a77731e3d8755277cd"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"module": "fabric_delegation",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"imports": [
|
|
151
151
|
"fabric_secrets"
|
|
152
152
|
],
|
|
153
|
-
"sha256": "
|
|
153
|
+
"sha256": "28e693282cddff8e4396ae39ce6fa935e875e8ddac7375bea2c189dd6fb04970"
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
"module": "fabric_connections",
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
"fabric_paths",
|
|
185
185
|
"fabric_time"
|
|
186
186
|
],
|
|
187
|
-
"sha256": "
|
|
187
|
+
"sha256": "b1f335cab4625171a616cf68cf3fd892b18bdadcf4baec3a3a0fdcf12785c170"
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
"module": "fabric_backups",
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
"fabric_time",
|
|
278
278
|
"fabric_usage"
|
|
279
279
|
],
|
|
280
|
-
"sha256": "
|
|
280
|
+
"sha256": "103ab4026f96e127c01cd91e4ad44f6b0ce2424ff33f078ed106b39e773a10e3"
|
|
281
281
|
},
|
|
282
282
|
{
|
|
283
283
|
"module": "fabric_profiles",
|
|
@@ -473,7 +473,7 @@
|
|
|
473
473
|
"fabric-secrets",
|
|
474
474
|
"fabric-time"
|
|
475
475
|
],
|
|
476
|
-
"sha256": "
|
|
476
|
+
"sha256": "b6610aaad9d00d8a7f42955627fb81725950841455bcedfc050abb0e079f0f93"
|
|
477
477
|
},
|
|
478
478
|
{
|
|
479
479
|
"module": "fabric-delegation",
|
|
@@ -544,7 +544,7 @@
|
|
|
544
544
|
"imports": [
|
|
545
545
|
"fabric-secrets"
|
|
546
546
|
],
|
|
547
|
-
"sha256": "
|
|
547
|
+
"sha256": "dc039ec3612d0090c9ba6a3a949b0b7df54712bead300dcb6cc8b9bc305cd183"
|
|
548
548
|
},
|
|
549
549
|
{
|
|
550
550
|
"module": "fabric-connections",
|
|
@@ -578,7 +578,7 @@
|
|
|
578
578
|
"fabric-paths",
|
|
579
579
|
"fabric-time"
|
|
580
580
|
],
|
|
581
|
-
"sha256": "
|
|
581
|
+
"sha256": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7"
|
|
582
582
|
},
|
|
583
583
|
{
|
|
584
584
|
"module": "fabric-backups",
|
|
@@ -671,7 +671,7 @@
|
|
|
671
671
|
"fabric-supervisors",
|
|
672
672
|
"fabric-usage"
|
|
673
673
|
],
|
|
674
|
-
"sha256": "
|
|
674
|
+
"sha256": "1b4639a10c530c1241d317c5e9cf3a10cd96e5d9af8b29fc1b1844de5c63bfec"
|
|
675
675
|
},
|
|
676
676
|
{
|
|
677
677
|
"module": "fabric-profiles",
|
|
@@ -2571,6 +2571,158 @@
|
|
|
2571
2571
|
"fabric-openrouter.mjs": "f8f0168691f968b7cd85f36aa98e2167e88cc44f1104ff514fd0c7699b339253",
|
|
2572
2572
|
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
2573
2573
|
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2574
|
+
},
|
|
2575
|
+
"0.48.0": {
|
|
2576
|
+
"fabric_jsvalue.py": "ff1bbfae999fb5683e59e31189143f1c85fa9d7780040b5a6952e060a143fc18",
|
|
2577
|
+
"fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
|
|
2578
|
+
"fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
|
|
2579
|
+
"fabric_secrets.py": "68a0b5e9e90c468ec9ac67b2f0fd1848e08813d6ec1ec14a2a47337eb1a98911",
|
|
2580
|
+
"fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
|
|
2581
|
+
"fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
|
|
2582
|
+
"fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
|
|
2583
|
+
"fabric_paths.py": "6c92af34292baddf7e10d3134d0a5b0bb23387741812a9f79c409cf1628bad23",
|
|
2584
|
+
"fabric_estate.py": "f493cd424dae6380d1caad91ffdca2764e51a49a6b1051a77731e3d8755277cd",
|
|
2585
|
+
"fabric_delegation.py": "6638a6039befe3b43f59018bab292e050931af21b544df1044cd5059572ded36",
|
|
2586
|
+
"fabric_share.py": "78506fe1a60829c525dbf6266a0aa0749a2164290ed94da8249a20b4e9be5605",
|
|
2587
|
+
"fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
|
|
2588
|
+
"fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
|
|
2589
|
+
"fabric_package.py": "ff533f1b3f92b4ea0f532fcec50b68c610d47549930eef8ad5247692a64dfb3c",
|
|
2590
|
+
"fabric_protocols.py": "28e693282cddff8e4396ae39ce6fa935e875e8ddac7375bea2c189dd6fb04970",
|
|
2591
|
+
"fabric_connections.py": "77d3ccf863c70e9209d21ef45c46a6db9c06237019bc20d54f8d3946873f3f11",
|
|
2592
|
+
"fabric_lock.py": "ba2399d2137c7a7791b7133b9db9cca36693f4eb7e30955b25a75ade814e95ba",
|
|
2593
|
+
"fabric_usage.py": "70e984ec3ff9f4ee31539df8d503f01ef8e6dd870e65e9546c0a2cb4aa040a88",
|
|
2594
|
+
"fabric_backups.py": "02dc5eb66ec956723d3ddba07b6dd15f6a9726d901f4d60edfd02eee57880ceb",
|
|
2595
|
+
"fabric_descriptor.py": "daf16f95b1810283b1386488e006e940a0cdfa5c3c8d09386ac33d29d58df78d",
|
|
2596
|
+
"fabric_auth.py": "55794587750f6c9f40935b74295d83d7096cb4b26a0b36ac292334cd87d8f853",
|
|
2597
|
+
"fabric_events.py": "0086886c2c29dbd5c9d152be9d375c5bdf4a29f89b8095baff8490c2bcd64e47",
|
|
2598
|
+
"fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
|
|
2599
|
+
"fabric_supervisors.py": "a2dbf15aa1c4eb9de42996b413103fa4814afd2e4e66d1ab7315f30e06ab6841",
|
|
2600
|
+
"fabric_service.py": "93368b68c4375dd43997e3b61b82d96743870d5481335461dc0f2d7af7ff3a67",
|
|
2601
|
+
"fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
|
|
2602
|
+
"fabric_interop_rules.py": "b5f0ecb1791b4c50c5ac151f987f692ed7001bc74841a8a161125ec775145e09",
|
|
2603
|
+
"fabric_interop.py": "f92fa111328fe3811438f733d8f0eb5295e6941c774123a78e26318bdcb35fc3",
|
|
2604
|
+
"fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
|
|
2605
|
+
"fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
|
|
2606
|
+
"fabric_board_link.py": "b5bff994cb615c6a4bb0d88d020e3a59504166180e1fa1aea3be93224f2aeccb",
|
|
2607
|
+
"fabric_board_session.py": "2271834c36c23f6bfa50395cd147e6af99f294ef09325fd15f0759ca8e18d941",
|
|
2608
|
+
"fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
|
|
2609
|
+
"fabric_board.py": "0f075894b3e352194fcaf3fc7d08dee4e9f1c215e859769ed85fa5010e4a1599",
|
|
2610
|
+
"fabric_http.py": "5d7f72af3a73fdbe8c1f487ecf60cef23a7c46289eba542b03aff4706b8218d4",
|
|
2611
|
+
"fabric_openrouter.py": "2017d898c17001d7275f58267de653be7ecf6acd17ad8d9b24ecb7a43e5617d4",
|
|
2612
|
+
"fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
|
|
2613
|
+
"fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
|
|
2614
|
+
"fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
|
|
2615
|
+
"fabric-secrets.mjs": "6ad8ab18ac899ecc2384e8ec6a3fb689b92be3b9a5ce3a59b92a37d52da5d766",
|
|
2616
|
+
"fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
|
|
2617
|
+
"fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
|
|
2618
|
+
"fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
|
|
2619
|
+
"fabric-paths.mjs": "2201b4ed7126f260931b63e8386377b4317d8dcad419d109bf22c683e44b7f29",
|
|
2620
|
+
"fabric-estate.mjs": "b6610aaad9d00d8a7f42955627fb81725950841455bcedfc050abb0e079f0f93",
|
|
2621
|
+
"fabric-delegation.mjs": "dafde9ea327c88d92dc73d1400282b43dc6f84dd345a2d2b9b154304f65fc9d7",
|
|
2622
|
+
"fabric-share.mjs": "d40a7405d7bd4c3213ed691330b7549afe8e023db0753b582e249714e672010d",
|
|
2623
|
+
"fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
|
|
2624
|
+
"fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
|
|
2625
|
+
"fabric-package.mjs": "49b757eefcc50e4c91585d5a5694bf3c83d0b03d112c1837f1c9f01466797ab5",
|
|
2626
|
+
"fabric-protocols.mjs": "dc039ec3612d0090c9ba6a3a949b0b7df54712bead300dcb6cc8b9bc305cd183",
|
|
2627
|
+
"fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
|
|
2628
|
+
"fabric-lock.mjs": "ac219ab0a326d5c43c7c4c255e0c3843d4794de795a60e3af661e7d5350ea052",
|
|
2629
|
+
"fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
|
|
2630
|
+
"fabric-backups.mjs": "849acdc6eba0fff648c8158e61f6b68cccd7bedf0880f7d5ec03a6a20f1e7005",
|
|
2631
|
+
"fabric-descriptor.mjs": "95c0b7ff36c29d7c748f1fa6fb1767d6fe8c97fca7f1ee404559930abbd3cc84",
|
|
2632
|
+
"fabric-auth.mjs": "3ea6796470fcd829bc72a772fb7e4c37532a09d1e37ff41592dcf1a1a3074ca2",
|
|
2633
|
+
"fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
|
|
2634
|
+
"fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
|
|
2635
|
+
"fabric-supervisors.mjs": "9d61c1a17538a5e2453e33e5ae347337336748041bc054478a96dd51f3d4fc51",
|
|
2636
|
+
"fabric-service.mjs": "1b4639a10c530c1241d317c5e9cf3a10cd96e5d9af8b29fc1b1844de5c63bfec",
|
|
2637
|
+
"fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
|
|
2638
|
+
"fabric-interop-rules.mjs": "57a8081307956e8c381119b361fa5232a7bf791815be8e3541eb5492820e2ea5",
|
|
2639
|
+
"fabric-interop.mjs": "581983ec3e197e5c2340ad25e8468320c69a2459c90097c7fcf2699766b4fb26",
|
|
2640
|
+
"fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
|
|
2641
|
+
"fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
|
|
2642
|
+
"fabric-board-link.mjs": "4cd6d4e21713a38c8e56f7d59db3148638b8c27c08e774d96a0f6283c6049e0a",
|
|
2643
|
+
"fabric-board-session.mjs": "e9eefb5d34dc66f6d6b528a9c9e85960fd884e6ea5e179c26cbfe3c054ccf7bc",
|
|
2644
|
+
"fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
|
|
2645
|
+
"fabric-board.mjs": "9e7bfe4d412521982224ef12bebfc2ff255eeeaf8d96cf40234ac18de68e7a59",
|
|
2646
|
+
"fabric-http.mjs": "f886bcb49c4353d039387ac9568738dda26697a94dd07fc6a6204500f0bc27bf",
|
|
2647
|
+
"fabric-openrouter.mjs": "f8f0168691f968b7cd85f36aa98e2167e88cc44f1104ff514fd0c7699b339253",
|
|
2648
|
+
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
2649
|
+
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2650
|
+
},
|
|
2651
|
+
"0.48.1": {
|
|
2652
|
+
"fabric_jsvalue.py": "ff1bbfae999fb5683e59e31189143f1c85fa9d7780040b5a6952e060a143fc18",
|
|
2653
|
+
"fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
|
|
2654
|
+
"fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
|
|
2655
|
+
"fabric_secrets.py": "68a0b5e9e90c468ec9ac67b2f0fd1848e08813d6ec1ec14a2a47337eb1a98911",
|
|
2656
|
+
"fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
|
|
2657
|
+
"fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
|
|
2658
|
+
"fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
|
|
2659
|
+
"fabric_paths.py": "6c92af34292baddf7e10d3134d0a5b0bb23387741812a9f79c409cf1628bad23",
|
|
2660
|
+
"fabric_estate.py": "f493cd424dae6380d1caad91ffdca2764e51a49a6b1051a77731e3d8755277cd",
|
|
2661
|
+
"fabric_delegation.py": "6638a6039befe3b43f59018bab292e050931af21b544df1044cd5059572ded36",
|
|
2662
|
+
"fabric_share.py": "78506fe1a60829c525dbf6266a0aa0749a2164290ed94da8249a20b4e9be5605",
|
|
2663
|
+
"fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
|
|
2664
|
+
"fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
|
|
2665
|
+
"fabric_package.py": "ff533f1b3f92b4ea0f532fcec50b68c610d47549930eef8ad5247692a64dfb3c",
|
|
2666
|
+
"fabric_protocols.py": "28e693282cddff8e4396ae39ce6fa935e875e8ddac7375bea2c189dd6fb04970",
|
|
2667
|
+
"fabric_connections.py": "77d3ccf863c70e9209d21ef45c46a6db9c06237019bc20d54f8d3946873f3f11",
|
|
2668
|
+
"fabric_lock.py": "ba2399d2137c7a7791b7133b9db9cca36693f4eb7e30955b25a75ade814e95ba",
|
|
2669
|
+
"fabric_usage.py": "b1f335cab4625171a616cf68cf3fd892b18bdadcf4baec3a3a0fdcf12785c170",
|
|
2670
|
+
"fabric_backups.py": "02dc5eb66ec956723d3ddba07b6dd15f6a9726d901f4d60edfd02eee57880ceb",
|
|
2671
|
+
"fabric_descriptor.py": "daf16f95b1810283b1386488e006e940a0cdfa5c3c8d09386ac33d29d58df78d",
|
|
2672
|
+
"fabric_auth.py": "55794587750f6c9f40935b74295d83d7096cb4b26a0b36ac292334cd87d8f853",
|
|
2673
|
+
"fabric_events.py": "0086886c2c29dbd5c9d152be9d375c5bdf4a29f89b8095baff8490c2bcd64e47",
|
|
2674
|
+
"fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
|
|
2675
|
+
"fabric_supervisors.py": "a2dbf15aa1c4eb9de42996b413103fa4814afd2e4e66d1ab7315f30e06ab6841",
|
|
2676
|
+
"fabric_service.py": "103ab4026f96e127c01cd91e4ad44f6b0ce2424ff33f078ed106b39e773a10e3",
|
|
2677
|
+
"fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
|
|
2678
|
+
"fabric_interop_rules.py": "b5f0ecb1791b4c50c5ac151f987f692ed7001bc74841a8a161125ec775145e09",
|
|
2679
|
+
"fabric_interop.py": "f92fa111328fe3811438f733d8f0eb5295e6941c774123a78e26318bdcb35fc3",
|
|
2680
|
+
"fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
|
|
2681
|
+
"fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
|
|
2682
|
+
"fabric_board_link.py": "b5bff994cb615c6a4bb0d88d020e3a59504166180e1fa1aea3be93224f2aeccb",
|
|
2683
|
+
"fabric_board_session.py": "2271834c36c23f6bfa50395cd147e6af99f294ef09325fd15f0759ca8e18d941",
|
|
2684
|
+
"fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
|
|
2685
|
+
"fabric_board.py": "0f075894b3e352194fcaf3fc7d08dee4e9f1c215e859769ed85fa5010e4a1599",
|
|
2686
|
+
"fabric_http.py": "5d7f72af3a73fdbe8c1f487ecf60cef23a7c46289eba542b03aff4706b8218d4",
|
|
2687
|
+
"fabric_openrouter.py": "2017d898c17001d7275f58267de653be7ecf6acd17ad8d9b24ecb7a43e5617d4",
|
|
2688
|
+
"fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
|
|
2689
|
+
"fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
|
|
2690
|
+
"fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
|
|
2691
|
+
"fabric-secrets.mjs": "6ad8ab18ac899ecc2384e8ec6a3fb689b92be3b9a5ce3a59b92a37d52da5d766",
|
|
2692
|
+
"fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
|
|
2693
|
+
"fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
|
|
2694
|
+
"fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
|
|
2695
|
+
"fabric-paths.mjs": "2201b4ed7126f260931b63e8386377b4317d8dcad419d109bf22c683e44b7f29",
|
|
2696
|
+
"fabric-estate.mjs": "b6610aaad9d00d8a7f42955627fb81725950841455bcedfc050abb0e079f0f93",
|
|
2697
|
+
"fabric-delegation.mjs": "dafde9ea327c88d92dc73d1400282b43dc6f84dd345a2d2b9b154304f65fc9d7",
|
|
2698
|
+
"fabric-share.mjs": "d40a7405d7bd4c3213ed691330b7549afe8e023db0753b582e249714e672010d",
|
|
2699
|
+
"fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
|
|
2700
|
+
"fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
|
|
2701
|
+
"fabric-package.mjs": "49b757eefcc50e4c91585d5a5694bf3c83d0b03d112c1837f1c9f01466797ab5",
|
|
2702
|
+
"fabric-protocols.mjs": "dc039ec3612d0090c9ba6a3a949b0b7df54712bead300dcb6cc8b9bc305cd183",
|
|
2703
|
+
"fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
|
|
2704
|
+
"fabric-lock.mjs": "ac219ab0a326d5c43c7c4c255e0c3843d4794de795a60e3af661e7d5350ea052",
|
|
2705
|
+
"fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
|
|
2706
|
+
"fabric-backups.mjs": "849acdc6eba0fff648c8158e61f6b68cccd7bedf0880f7d5ec03a6a20f1e7005",
|
|
2707
|
+
"fabric-descriptor.mjs": "95c0b7ff36c29d7c748f1fa6fb1767d6fe8c97fca7f1ee404559930abbd3cc84",
|
|
2708
|
+
"fabric-auth.mjs": "3ea6796470fcd829bc72a772fb7e4c37532a09d1e37ff41592dcf1a1a3074ca2",
|
|
2709
|
+
"fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
|
|
2710
|
+
"fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
|
|
2711
|
+
"fabric-supervisors.mjs": "9d61c1a17538a5e2453e33e5ae347337336748041bc054478a96dd51f3d4fc51",
|
|
2712
|
+
"fabric-service.mjs": "1b4639a10c530c1241d317c5e9cf3a10cd96e5d9af8b29fc1b1844de5c63bfec",
|
|
2713
|
+
"fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
|
|
2714
|
+
"fabric-interop-rules.mjs": "57a8081307956e8c381119b361fa5232a7bf791815be8e3541eb5492820e2ea5",
|
|
2715
|
+
"fabric-interop.mjs": "581983ec3e197e5c2340ad25e8468320c69a2459c90097c7fcf2699766b4fb26",
|
|
2716
|
+
"fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
|
|
2717
|
+
"fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
|
|
2718
|
+
"fabric-board-link.mjs": "4cd6d4e21713a38c8e56f7d59db3148638b8c27c08e774d96a0f6283c6049e0a",
|
|
2719
|
+
"fabric-board-session.mjs": "e9eefb5d34dc66f6d6b528a9c9e85960fd884e6ea5e179c26cbfe3c054ccf7bc",
|
|
2720
|
+
"fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
|
|
2721
|
+
"fabric-board.mjs": "9e7bfe4d412521982224ef12bebfc2ff255eeeaf8d96cf40234ac18de68e7a59",
|
|
2722
|
+
"fabric-http.mjs": "f886bcb49c4353d039387ac9568738dda26697a94dd07fc6a6204500f0bc27bf",
|
|
2723
|
+
"fabric-openrouter.mjs": "f8f0168691f968b7cd85f36aa98e2167e88cc44f1104ff514fd0c7699b339253",
|
|
2724
|
+
"fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
|
|
2725
|
+
"share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
|
|
2574
2726
|
}
|
|
2575
2727
|
},
|
|
2576
2728
|
"data": {
|
|
@@ -5,9 +5,9 @@ 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.48.1"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
|
-
contract-commit: "
|
|
10
|
+
contract-commit: "2f020a696453ae299b123517f14957f6a1354379"
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Creating Fabric-compatible agents
|
|
@@ -136,7 +136,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
|
|
|
136
136
|
The effect class is declared, never defaulted: ask the author what the agent does outside itself when the request
|
|
137
137
|
does not say. Cost presets are only for a run whose cost varies; their amounts come from the author or measured runs.
|
|
138
138
|
|
|
139
|
-
Pin exactly contract `0.1.0` at commit `
|
|
139
|
+
Pin exactly contract `0.1.0` at commit `2f020a696453ae299b123517f14957f6a1354379` and
|
|
140
140
|
read the pinned guide before implementing protocol details. If this skill is installed
|
|
141
141
|
without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
|
|
142
142
|
contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
|