@passioncode-ai/fabric-agent-adapter 0.38.0 → 0.39.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 +77 -0
- package/README.md +20 -16
- package/bin/fabric-agent-adapter.js +6 -6
- 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 +15 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -4
- 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 +44 -43
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +12 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +19 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +19 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +6 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +8 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +26 -26
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +10 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +14 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +8 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +20 -43
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +9 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +33 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +40 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +32 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +52 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +35 -35
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +27 -27
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +31 -31
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +9 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +8 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Usage report
|
|
1
|
+
# Usage report: what the agent spent (DEC-0021)
|
|
2
2
|
|
|
3
3
|
Fabric Agent Contract DEC-0021, `docs/specification/service.md` → *Usage report*, schema
|
|
4
4
|
`schemas/service-usage.schema.json`. Optional. A service that calls paid models or tools should
|
|
@@ -36,6 +36,11 @@ ledger.record(k.makeUsageReceipt('openrouter', model, { inputTokens, outputToken
|
|
|
36
36
|
records `cost_usd=None`. The report counts it in `unpricedCalls`, and a host shows a lower bound
|
|
37
37
|
(«≥ $x») or «unknown». A call that is really free (a template, a cached answer) records
|
|
38
38
|
`cost_usd=0.0` with `price-list`.
|
|
39
|
+
- **Unknown tokens are named too** (DEC-0091). When the provider gave no token count (a stream cut off, a local
|
|
40
|
+
model without a counter), pass `tokens_measured=False` / `tokensMeasured: false` with the tokens you have (often 0).
|
|
41
|
+
The receipt carries `tokensMeasured: false`, and the report counts the call in `unmeasuredCalls` on its day and model
|
|
42
|
+
row, never more than `calls`; a host then shows the token totals as a lower bound. A job whose usage you send as
|
|
43
|
+
`null` or `partial` ([interop](interop.md)) is such a call.
|
|
39
44
|
- **No content.** A receipt carries no prompt, output, caller or user id. The kits accept only the
|
|
40
45
|
fields above.
|
|
41
46
|
- If the service already keeps a ledger (a jobs table with token counts), build the receipts from
|
|
@@ -59,7 +64,7 @@ left out.
|
|
|
59
64
|
it, and the host only shows it.
|
|
60
65
|
|
|
61
66
|
**Every limit (DEC-0027).** Pass `budgets` to list every spending limit the service applies,
|
|
62
|
-
enforced or not, so the spend view shows all of them
|
|
67
|
+
enforced or not, so the spend view shows all of them; the operator asked for that on 2026-10-07:
|
|
63
68
|
|
|
64
69
|
```python
|
|
65
70
|
ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budgets=[
|
|
@@ -74,7 +79,7 @@ ledger.report(service_id=ID, budget={"period": "month", "limitUsd": 100}, budget
|
|
|
74
79
|
way `budget` is, so `budget` and its twin in `budgets` never disagree. A per-order limit
|
|
75
80
|
(`per_job`, `approval`) gets `spentUsd: null`. Every other limit states its own spend.
|
|
76
81
|
- `scope` is `machine`, `project`, `pool` or `job`; a `project` or `pool` limit names its
|
|
77
|
-
`subject`. A limit has at most one window
|
|
82
|
+
`subject`. A limit has at most one window (`period`, `windowSeconds` or `since`), and it agrees
|
|
78
83
|
with the kind (`daily`: the day or 86400 s; `monthly`: the month or 28–31 days; `velocity`: a
|
|
79
84
|
rolling window). `tripped: true` (work stopped by it now) needs `enforced: true`. An unknown
|
|
80
85
|
kind is shown generically, never refused.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py
CHANGED
|
@@ -90,12 +90,12 @@ def marker_problem(plist: dict) -> Optional[str]:
|
|
|
90
90
|
fix = ("a copy that finds the lock held exits 75 at once and KeepAlive respawns it every ThrottleInterval; "
|
|
91
91
|
"regenerate the plist with launchd_plist / launchdPlist and reinstall")
|
|
92
92
|
if marker["kind"] == "conflict":
|
|
93
|
-
return "%s
|
|
93
|
+
return "%s: %s" % (fs.marker_conflict_message(marker), fix)
|
|
94
94
|
if marker["kind"] == "supervised":
|
|
95
|
-
return "the marker names %s, not launchd
|
|
95
|
+
return "the marker names %s, not launchd: %s" % (marker["manager"], fix)
|
|
96
96
|
if marker.get("unreadable") is not None:
|
|
97
|
-
return "the marker %r names no supervisor
|
|
98
|
-
return "no %s=launchd
|
|
97
|
+
return "the marker %r names no supervisor: %s" % (marker["unreadable"], fix)
|
|
98
|
+
return "no %s=launchd: %s" % (fs.SUPERVISOR_ENV, fix)
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def priority_problems(plist: dict) -> list:
|
|
@@ -729,7 +729,7 @@ class Probe:
|
|
|
729
729
|
marker or "%s: %s=launchd, so a copy that finds the lock held backs off" % (plist_path.name, fs.SUPERVISOR_ENV))
|
|
730
730
|
slow = priority_problems(plist)
|
|
731
731
|
self.add("lifecycle.priority", "FAIL" if slow else "PASS",
|
|
732
|
-
"; ".join(slow) + "
|
|
732
|
+
"; ".join(slow) + ". macOS may starve the service under load and hosts then see an outage; use ProcessType Standard"
|
|
733
733
|
if slow else "%s: scheduled as a standard process" % plist_path.name)
|
|
734
734
|
if sys.platform != "darwin" or not shutil.which("launchctl"):
|
|
735
735
|
self.add("lifecycle.one-copy", "NOT_RUN", "launchctl is not available")
|
|
@@ -920,49 +920,26 @@ def usage_problems(report: Any, service_id: str, instance: str) -> List[str]:
|
|
|
920
920
|
days = report.get("days")
|
|
921
921
|
if not isinstance(days, list) or len(days) > fs.USAGE_DAYS:
|
|
922
922
|
return out + ["days is not a list of at most 31"]
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
def priced(row: Dict[str, Any], at: str) -> None:
|
|
926
|
-
calls, unpriced, cost = row.get("calls", 0), row.get("unpricedCalls", 0), row.get("costUsd")
|
|
927
|
-
if unpriced > calls:
|
|
928
|
-
out.append("%s: more unpriced calls than calls" % at)
|
|
929
|
-
if calls and unpriced == calls and cost is not None:
|
|
930
|
-
out.append("%s: every call is unpriced, so the cost must be null, not %r" % (at, cost))
|
|
931
|
-
if unpriced < calls and cost is None:
|
|
932
|
-
out.append("%s: priced calls carry a cost" % at)
|
|
933
|
-
if cost is not None and (not isinstance(cost, (int, float)) or cost < 0):
|
|
934
|
-
out.append("%s: costUsd is not a non-negative number" % at)
|
|
935
|
-
|
|
936
|
-
previous = ""
|
|
923
|
+
# The arithmetic of FAC-SEM-025 is the kit's `usage_findings`, the contract's `usageRules` held to its vectors: one
|
|
924
|
+
# implementation, so the probe never disagrees with the kit and never throws on a count that is not a number.
|
|
937
925
|
for i, day in enumerate(days):
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date) or date <= previous:
|
|
941
|
-
out.append("%s: dates must run forward" % at)
|
|
942
|
-
previous = date
|
|
943
|
-
priced(day, at)
|
|
944
|
-
models = day.get("byModel") if isinstance(day, dict) else None
|
|
945
|
-
if not isinstance(models, list):
|
|
946
|
-
out.append("%s: byModel missing" % at)
|
|
926
|
+
if not isinstance(day, dict):
|
|
927
|
+
out.append("days[%d]: not an object" % i)
|
|
947
928
|
continue
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
for j, m in enumerate(models):
|
|
929
|
+
if not isinstance(day.get("byModel"), list):
|
|
930
|
+
out.append("days[%d]: byModel missing" % i)
|
|
931
|
+
continue
|
|
932
|
+
for row, at in [(day, "days[%d]" % i)] + [(m, "days[%d].byModel[%d]" % (i, j)) for j, m in enumerate(day["byModel"]) if isinstance(m, dict)]:
|
|
933
|
+
cost = row.get("costUsd")
|
|
934
|
+
if cost is not None and (isinstance(cost, bool) or not isinstance(cost, (int, float)) or cost < 0):
|
|
935
|
+
out.append("%s: costUsd is not a non-negative number" % at)
|
|
936
|
+
for j, m in enumerate(day["byModel"]):
|
|
957
937
|
# DEC-0046, DEC-0061: a row's route is a login in its vendor's runner, an issued key, or the platform's
|
|
958
938
|
# own metered models (a listing in the cloud).
|
|
959
|
-
if "route" in m and m["route"] not in fs.USAGE_ROUTES:
|
|
960
|
-
out.append("%
|
|
961
|
-
# DEC-0065: the window and the project attribution, as the contract's usageRules reads them (FAC-SEM-025).
|
|
939
|
+
if isinstance(m, dict) and "route" in m and m["route"] not in fs.USAGE_ROUTES:
|
|
940
|
+
out.append("days[%d].byModel[%d]: route %r is not subscription, key or platform-metered" % (i, j, m["route"]))
|
|
962
941
|
for finding in fs.usage_findings(report):
|
|
963
|
-
|
|
964
|
-
if path.startswith(("/byProject", "/generatedAt")) or "generatedAt" in message or "exists in its month" in message:
|
|
965
|
-
out.append("%s: %s" % (path or "/", message))
|
|
942
|
+
out.append("%s: %s" % (finding["instancePath"] or "/", finding["message"]))
|
|
966
943
|
return out
|
|
967
944
|
|
|
968
945
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py
CHANGED
|
@@ -408,7 +408,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
408
408
|
for c in report["copies"]:
|
|
409
409
|
print(" %-8s %s (%s)" % (c["state"], c["path"], c["version"] or "no version marker"))
|
|
410
410
|
if not report["copies"]:
|
|
411
|
-
print(" no kit copy" + ("
|
|
411
|
+
print(" no kit copy" + (" (services %s declare a dashboard)" % ", ".join(report["services"])
|
|
412
412
|
if report["dashboard"] and report["services"] else ""))
|
|
413
413
|
if report["services"]:
|
|
414
414
|
print(" services from this repository: %s" % ", ".join(report["services"]))
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs
CHANGED
|
@@ -362,7 +362,7 @@ export class BoardSession {
|
|
|
362
362
|
try {
|
|
363
363
|
return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined });
|
|
364
364
|
} catch (e) {
|
|
365
|
-
if (e instanceof HubError) throw new BoardError('unreachable', 'Fabric\'s hub did not answer; a write may have been stored
|
|
365
|
+
if (e instanceof HubError) throw new BoardError('unreachable', 'Fabric\'s hub did not answer; a write may have been stored, so send it again with the same idempotency key.', true);
|
|
366
366
|
throw e;
|
|
367
367
|
}
|
|
368
368
|
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs
CHANGED
|
@@ -178,11 +178,17 @@ function authentic(token, jwks, typ) {
|
|
|
178
178
|
return { compact, kid, x: found.x };
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
const ORG_CONTEXT = /^org:[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
182
|
+
/** DEC-0090: a partner actor is exactly {partner: org:<id>}, never with a sub, a session or an act. */
|
|
183
|
+
export const isPartnerActor = (node) => isObject(node) && typeof node.partner === 'string' && ORG_CONTEXT.test(node.partner) && Object.keys(node).length === 1;
|
|
184
|
+
|
|
185
|
+
/** Depth of an actor chain; Infinity for a chain that is not one (a missing or non-string sub at any level). A partner
|
|
186
|
+
* actor ends the chain and counts, never begins it (DEC-0090). */
|
|
182
187
|
export function chainDepth(act) {
|
|
183
188
|
let depth = 0;
|
|
184
189
|
let node = act;
|
|
185
190
|
while (node !== undefined) {
|
|
191
|
+
if (depth > 0 && isPartnerActor(node)) return depth + 1; // a partner actor ends the chain, never begins it
|
|
186
192
|
if (!isObject(node) || !isString(node.sub)) return Number.POSITIVE_INFINITY;
|
|
187
193
|
depth += 1;
|
|
188
194
|
if (depth > 64) return depth;
|
|
@@ -193,7 +199,7 @@ export function chainDepth(act) {
|
|
|
193
199
|
|
|
194
200
|
function chainOf(act) {
|
|
195
201
|
const chain = [];
|
|
196
|
-
for (let node = act; node; node = node.act) chain.push(node.sub);
|
|
202
|
+
for (let node = act; node; node = 'act' in node ? node.act : undefined) chain.push('partner' in node ? node.partner : node.sub);
|
|
197
203
|
return chain;
|
|
198
204
|
}
|
|
199
205
|
|
|
@@ -212,6 +218,7 @@ function actorChainDepth(act) {
|
|
|
212
218
|
if (act === undefined) return undefined;
|
|
213
219
|
let depth = 0;
|
|
214
220
|
for (let node = act; node !== undefined; node = node.act) {
|
|
221
|
+
if (depth > 0 && isPartnerActor(node)) return depth + 1; // DEC-0090: innermost only, it ends the chain
|
|
215
222
|
if (!isObject(node) || typeof node.sub !== 'string' || !AGENT_ID.test(node.sub)) return undefined;
|
|
216
223
|
if (node.session !== undefined && !(typeof node.session === 'string' && OPAQUE_ID.test(node.session))) return undefined;
|
|
217
224
|
depth += 1;
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs
CHANGED
|
@@ -300,9 +300,17 @@ export function placementTrust(record, machineId, agentRef, context = PERSONAL_C
|
|
|
300
300
|
const machine = list(record.machines).find((entry) => entry.id === machineId);
|
|
301
301
|
const placement = list(machine?.packages).find((entry) => entry.agentRef === agentRef && contextOf(entry) === context);
|
|
302
302
|
if (!placement) return undefined;
|
|
303
|
+
// DEC-0090: a partner's key is trusted only in the contexts its scope names.
|
|
304
|
+
if (!scopeAdmits(record, placement.publisherKid, context)) return { publisher: [] };
|
|
303
305
|
return { publisher: list(record.publisherKeys).filter((key) => key.kid === placement.publisherKid) };
|
|
304
306
|
}
|
|
305
307
|
|
|
308
|
+
/** Whether a placement in `context` under `kid` may use that key (DEC-0090): an unscoped key anywhere, a scoped one only in its contexts. */
|
|
309
|
+
function scopeAdmits(record, kid, context) {
|
|
310
|
+
const scopes = list(record.publisherScopes).filter((scope) => scope.kid === kid);
|
|
311
|
+
return scopes.length === 0 || scopes.some((scope) => Array.isArray(scope.contexts) && scope.contexts.includes(context));
|
|
312
|
+
}
|
|
313
|
+
|
|
306
314
|
const MAX_WINDOW_MS = MAX_VALIDITY_SECONDS * 1000;
|
|
307
315
|
const isRevision = (value) => Number.isSafeInteger(value) && value >= 1;
|
|
308
316
|
const refused = (fault, detail) => ({ certified: false, fault, detail });
|
|
@@ -472,6 +480,27 @@ function placements(value, flag) {
|
|
|
472
480
|
});
|
|
473
481
|
}
|
|
474
482
|
|
|
483
|
+
/**
|
|
484
|
+
* FAC-SEM-122 on one record (DEC-0090): a partner's publisher key is scoped once, to a key the record pins, and a
|
|
485
|
+
* placement under it is in one of the organization contexts its scope names, never `personal` or another.
|
|
486
|
+
*/
|
|
487
|
+
function partnerScopes(value, flag) {
|
|
488
|
+
const pinned = new Set(list(value.publisherKeys).map((key) => key.kid));
|
|
489
|
+
const scoped = new Set();
|
|
490
|
+
indexed(value.publisherScopes).forEach(([scope, index]) => {
|
|
491
|
+
if (scoped.has(scope.kid)) flag(`/publisherScopes/${index}/kid`, `publisher key ${show(scope.kid)} is scoped twice`);
|
|
492
|
+
scoped.add(scope.kid);
|
|
493
|
+
if (!pinned.has(scope.kid)) flag(`/publisherScopes/${index}/kid`, `publisher key ${show(scope.kid)} is not in publisherKeys, so the scope limits no key`);
|
|
494
|
+
});
|
|
495
|
+
indexed(value.machines).forEach(([machine, m]) => {
|
|
496
|
+
indexed(machine.packages).forEach(([placement, p]) => {
|
|
497
|
+
if (!scopeAdmits(value, placement.publisherKid, contextOf(placement))) {
|
|
498
|
+
flag(`/machines/${m}/packages/${p}/publisherKid`, `publisher key ${show(placement.publisherKid)} is a partner's, trusted only in the contexts its scope names, not in ${show(contextOf(placement))}`);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
475
504
|
function secretReferences(value, flag) {
|
|
476
505
|
const machineIds = new Set(list(value.machines).map((machine) => machine.id));
|
|
477
506
|
indexed(value.machines).forEach(([machine, m]) => {
|
|
@@ -573,6 +602,7 @@ function recordFindings(value) {
|
|
|
573
602
|
revisionWhole(value, flagger(findings, 'FAC-SEM-072'));
|
|
574
603
|
keys(value, flagger(findings, 'FAC-SEM-073'));
|
|
575
604
|
placements(value, flagger(findings, 'FAC-SEM-074'));
|
|
605
|
+
partnerScopes(value, flagger(findings, 'FAC-SEM-122'));
|
|
576
606
|
secretReferences(value, flagger(findings, 'FAC-SEM-075'));
|
|
577
607
|
grantsAndRevocations(value, flagger(findings, 'FAC-SEM-076'));
|
|
578
608
|
indexed(value.shareGrants).forEach(([grant, index]) => findings.push(...grantContextFindings(grant, `/shareGrants/${index}`)));
|
|
@@ -613,6 +643,9 @@ function installation(value) {
|
|
|
613
643
|
indexed(machine.secrets).forEach(([secret, index]) => {
|
|
614
644
|
if (secret.agentRef === pkg.agentRef && contextOf(secret) === context && !declared.has(secret.slot)) flag075(`/record/machines/${machineIndex}/secrets/${index}/slot`, `the package declares no slot ${show(secret.slot)}: a secret goes only where a placed package declares it`);
|
|
615
645
|
});
|
|
646
|
+
if (placement && !scopeAdmits(record, placement.publisherKid, context)) {
|
|
647
|
+
flagger(findings, 'FAC-SEM-122')(`/record/machines/${machineIndex}/packages/${placementIndex}/publisherKid`, `publisher key ${show(placement.publisherKid)} is a partner's, not trusted in ${show(context)}`);
|
|
648
|
+
}
|
|
616
649
|
if (placement) {
|
|
617
650
|
const flag111 = flagger(findings, 'FAC-SEM-111');
|
|
618
651
|
for (const [suffix, message] of placementGrantProblems(record, placement)) flag111(`/record/machines/${machineIndex}/packages/${placementIndex}${suffix}`, message);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs
CHANGED
|
@@ -420,7 +420,7 @@ export function readFolder(folder) {
|
|
|
420
420
|
}
|
|
421
421
|
};
|
|
422
422
|
walk(folder, '');
|
|
423
|
-
if (refused.length) throw new PackageError(`Not packed: remove these or pack a clean copy
|
|
423
|
+
if (refused.length) throw new PackageError(`Not packed: remove these or pack a clean copy: ${refused.sort().join('; ')}`);
|
|
424
424
|
return files;
|
|
425
425
|
}
|
|
426
426
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/exten
|
|
|
17
17
|
// The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
|
|
18
18
|
// document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
19
19
|
export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
20
|
-
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.
|
|
20
|
+
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.1';
|
|
21
21
|
export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
22
22
|
export const EXIT_ALREADY_RUNNING = 75;
|
|
23
23
|
// Lifecycle rules: references/lifecycle.md (the rule for this kit's users; it implements the public
|
|
@@ -862,13 +862,13 @@ export function buildWellKnown({ id, instance, name, version, build, startedAt,
|
|
|
862
862
|
|
|
863
863
|
/** Each contract protocol's published revisions, oldest first (the contract's `PROTOCOL_REVISIONS`); a test holds it equal to the contract's. */
|
|
864
864
|
export const PROTOCOL_REVISIONS = new Map(Object.entries({
|
|
865
|
-
'access-log': ['1'], approval: ['1'], 'activity-summary': ['1'], 'enroll-token': ['1'], entitlement: ['1'], 'entitlement-key-set': ['1'], 'estate-record': ['1'], 'execution-packet': ['1'],
|
|
865
|
+
'access-log': ['1'], approval: ['1'], 'activity-summary': ['1'], 'coding-agents': ['1'], 'enroll-token': ['1'], entitlement: ['1'], 'entitlement-key-set': ['1'], 'estate-record': ['1'], 'execution-packet': ['1'],
|
|
866
866
|
'fabric-activity': ['0.1'], 'fabric-dashboard': ['0.1'], 'fabric-delegation': ['0.1'], 'fabric-device': ['0.1'],
|
|
867
867
|
'fabric-interop': ['0.1', '0.2'], 'fabric-local-channel': ['0.1'], 'fabric-local-runner': ['0.1'], 'fabric-operator-channel': ['0.1'],
|
|
868
868
|
'fabric-package': ['1'], 'fabric-project-comms': ['0.1'], 'fabric-proposal': ['1'], 'fabric-provider': ['0.1'], 'fabric-relay': ['0.1'],
|
|
869
869
|
'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], heartbeat: ['1'], 'learning-note': ['1'],
|
|
870
870
|
memory: ['0.1'], 'pairing-message': ['1'], pipeline: ['0.1'], 'revocation-delta': ['1'], 'session-handoff': ['1'],
|
|
871
|
-
'share-registration': ['1'], 'share-registration-receipt': ['1'], 'window-lease': ['1'],
|
|
871
|
+
'share-registration': ['1'], 'share-registration-receipt': ['1'], 'support-quality-daily': ['1'], 'window-lease': ['1'],
|
|
872
872
|
}));
|
|
873
873
|
export const MAX_PROTOCOLS = 64;
|
|
874
874
|
const REVISION = /^([a-z][a-z0-9.-]{0,63})\/([0-9]{1,4}(?:\.[0-9]{1,4}){0,2})$/;
|
|
@@ -1023,7 +1023,9 @@ const COUNTS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTok
|
|
|
1023
1023
|
* One model call. `projectId` (a URI, kept in its normal form, DEC-0076) and `taskRunId` (the service's own run id) attribute
|
|
1024
1024
|
* it to the project the service ran it for (DEC-0065); they never name a caller, session or person.
|
|
1025
1025
|
*/
|
|
1026
|
-
|
|
1026
|
+
// tokensMeasured: false marks a call whose token counts are a lower bound or unknown (DEC-0091): the report counts it
|
|
1027
|
+
// in unmeasuredCalls and its token totals are a lower bound.
|
|
1028
|
+
export function makeUsageReceipt(provider, model, { inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, costUsd = null, costBasis, at, route, projectId, taskRunId, tokensMeasured = true } = {}) {
|
|
1027
1029
|
if (!PROVIDER.test(provider ?? '')) throw new ServiceError('provider must be a lowercase name such as anthropic or openrouter.');
|
|
1028
1030
|
model = String(model ?? '').trim();
|
|
1029
1031
|
if (!model || model.length > 128) throw new ServiceError('model must be 1 to 128 characters.');
|
|
@@ -1033,10 +1035,11 @@ export function makeUsageReceipt(provider, model, { inputTokens, outputTokens, c
|
|
|
1033
1035
|
const basis = costBasis ?? (costUsd === null ? 'unknown' : 'provider');
|
|
1034
1036
|
if (!COST_BASES.includes(basis) || (costUsd === null) !== (basis === 'unknown')) throw new ServiceError('costBasis is provider or price-list with a cost, unknown without one.');
|
|
1035
1037
|
if (route !== undefined && !USAGE_ROUTES.includes(route)) throw new ServiceError('route is subscription, key or platform-metered (DEC-0046, DEC-0061).');
|
|
1038
|
+
if (tokensMeasured !== true && tokensMeasured !== false) throw new ServiceError('tokensMeasured is true or false.');
|
|
1036
1039
|
if (taskRunId !== undefined && projectId === undefined) throw new ServiceError('a task run belongs to a project: give projectId with taskRunId.');
|
|
1037
1040
|
if (projectId !== undefined && !(typeof projectId === 'string' && /^[A-Za-z][A-Za-z0-9+.-]*:\S+$/.test(projectId))) throw new ServiceError('projectId is the URI of the project the service ran the call for.');
|
|
1038
1041
|
if (taskRunId !== undefined && !(typeof taskRunId === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(taskRunId))) throw new ServiceError("taskRunId is the service's own run id: ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$.");
|
|
1039
|
-
return { at: at ?? nowIso(), provider, model, ...counts, costUsd, costBasis: basis, ...(route === undefined ? {} : { route }),
|
|
1042
|
+
return { at: at ?? nowIso(), provider, model, ...counts, costUsd, costBasis: basis, ...(tokensMeasured ? {} : { tokensMeasured: false }), ...(route === undefined ? {} : { route }),
|
|
1040
1043
|
...(projectId === undefined ? {} : { projectId: normalIdentifier(projectId) }), ...(taskRunId === undefined ? {} : { taskRunId }) };
|
|
1041
1044
|
}
|
|
1042
1045
|
|
|
@@ -1104,6 +1107,8 @@ function pricedRow(row, at) {
|
|
|
1104
1107
|
const calls = usageCount(row.calls), unpriced = usageCount(row.unpricedCalls);
|
|
1105
1108
|
const flag = (p, message) => [{ code: 'FAC-SEM-025', instancePath: p, message }];
|
|
1106
1109
|
if (unpriced > calls) return flag(`${at}/unpricedCalls`, 'more unpriced calls than calls');
|
|
1110
|
+
// DEC-0091: calls whose token count is unknown are some of the calls, never more.
|
|
1111
|
+
if (usageCount(row.unmeasuredCalls) > calls) return flag(`${at}/unmeasuredCalls`, 'more calls with unknown tokens than calls');
|
|
1107
1112
|
if (calls > 0 && unpriced === calls && row.costUsd !== null) return flag(`${at}/costUsd`, 'every call is unpriced, so the cost is unknown (null), not a number');
|
|
1108
1113
|
if (unpriced < calls && row.costUsd === null) return flag(`${at}/costUsd`, 'priced calls carry a cost; null is only for a row with no priced call');
|
|
1109
1114
|
return [];
|
|
@@ -1114,6 +1119,25 @@ function pricedRow(row, at) {
|
|
|
1114
1119
|
* date exists, not after `generatedAt`'s UTC date, at most 30 days before it) and the project attribution (each project
|
|
1115
1120
|
* once by its normal form, each task run once; calls, priced calls and known costs bounded). Never throws.
|
|
1116
1121
|
*/
|
|
1122
|
+
/** The contract's `toNumber` (src/findings.ts): `Number()` without its throw on an object such as {"toString": 1}. */
|
|
1123
|
+
function toNumber(value) {
|
|
1124
|
+
try {
|
|
1125
|
+
return Number(value);
|
|
1126
|
+
} catch {
|
|
1127
|
+
return Number.NaN;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/** The contract's `show`: `String()`, or the value's JSON where `String()` would throw. */
|
|
1132
|
+
function show(value) {
|
|
1133
|
+
if (typeof value === 'string') return value;
|
|
1134
|
+
try {
|
|
1135
|
+
return String(value);
|
|
1136
|
+
} catch {
|
|
1137
|
+
return JSON.stringify(value) ?? typeof value;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1117
1141
|
export function usageFindings(report) {
|
|
1118
1142
|
const value = typeof report === 'object' && report !== null && !Array.isArray(report) ? report : {};
|
|
1119
1143
|
const findings = [];
|
|
@@ -1122,23 +1146,24 @@ export function usageFindings(report) {
|
|
|
1122
1146
|
let previous = '';
|
|
1123
1147
|
days.forEach((day, i) => {
|
|
1124
1148
|
const at = `/days/${i}`;
|
|
1125
|
-
const date =
|
|
1149
|
+
const date = show(day.date ?? '');
|
|
1126
1150
|
if (date <= previous) flag(`${at}/date`, `days must run forward without a repeat (${date} after ${previous})`);
|
|
1127
1151
|
previous = date;
|
|
1128
1152
|
findings.push(...pricedRow(day, at));
|
|
1129
1153
|
const models = usageList(day.byModel);
|
|
1130
1154
|
models.forEach((m, j) => findings.push(...pricedRow(m, `${at}/byModel/${j}`)));
|
|
1131
1155
|
if (!models.length) {
|
|
1132
|
-
const spent = ['calls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'].some((f) =>
|
|
1156
|
+
const spent = ['calls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'].some((f) => toNumber(day[f] ?? 0) > 0) || toNumber(day.costUsd ?? 0) > 0;
|
|
1133
1157
|
if (spent) flag(`${at}/byModel`, 'a day with calls, tokens or cost names the models they belong to');
|
|
1134
1158
|
return;
|
|
1135
1159
|
}
|
|
1136
|
-
for (const field of ['calls', 'unpricedCalls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']) {
|
|
1137
|
-
const sum = models.reduce((n, m) => n +
|
|
1138
|
-
if (
|
|
1160
|
+
for (const field of ['calls', 'unpricedCalls', 'unmeasuredCalls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']) {
|
|
1161
|
+
const sum = models.reduce((n, m) => n + toNumber(m[field] ?? 0), 0);
|
|
1162
|
+
if (toNumber(day[field] ?? 0) !== sum) flag(`${at}/${field}`, `the day's ${field} is not the sum of its models (${show(day[field] ?? 0)} ≠ ${sum})`);
|
|
1139
1163
|
}
|
|
1140
1164
|
const known = models.map((m) => (typeof m.costUsd === 'number' ? m.costUsd : null)).filter((x) => x !== null).reduce((n, x) => n + x, 0);
|
|
1141
|
-
|
|
1165
|
+
// The contract's !close(day, known): Infinity or NaN is never close, so it is a finding.
|
|
1166
|
+
if (typeof day.costUsd === 'number' && !(Math.abs(day.costUsd - known) <= 0.000001 + 1e-9 * Math.max(Math.abs(day.costUsd), Math.abs(known)))) flag(`${at}/costUsd`, "the day's cost is not the sum of its models' known costs");
|
|
1142
1167
|
});
|
|
1143
1168
|
if (days.length) {
|
|
1144
1169
|
const generated = strictTime(value.generatedAt);
|
|
@@ -1162,8 +1187,8 @@ export function usageFindings(report) {
|
|
|
1162
1187
|
const at = `/byProject/${p}`;
|
|
1163
1188
|
const normal = normalIdentifier(project.projectId);
|
|
1164
1189
|
if (normal !== undefined) {
|
|
1165
|
-
if (seen.has(normal)) flag(`${at}/projectId`, `each project is one row: ${
|
|
1166
|
-
else seen.set(normal,
|
|
1190
|
+
if (seen.has(normal)) flag(`${at}/projectId`, `each project is one row: ${show(project.projectId)} names the project ${seen.get(normal)} already names`);
|
|
1191
|
+
else seen.set(normal, show(project.projectId));
|
|
1167
1192
|
}
|
|
1168
1193
|
projectCalls += usageCount(project.calls);
|
|
1169
1194
|
projectCost += usageCost(project.costUsd);
|
|
@@ -1272,6 +1297,7 @@ export function usageReport(receipts, { id, instance = 'default', now = Date.now
|
|
|
1272
1297
|
row.calls += 1;
|
|
1273
1298
|
for (const c of COUNTS) row[c] += Number(r[c] ?? 0);
|
|
1274
1299
|
if (r.costUsd === null || r.costUsd === undefined) row.unpricedCalls += 1; else row.cost += r.costUsd;
|
|
1300
|
+
if (r.tokensMeasured === false) row.unmeasuredCalls = (row.unmeasuredCalls ?? 0) + 1; // absent is 0 (DEC-0091)
|
|
1275
1301
|
row.bases.add(r.costBasis ?? 'unknown');
|
|
1276
1302
|
}
|
|
1277
1303
|
const outDays = [...days.keys()].sort().map((date) => {
|
|
@@ -1281,6 +1307,7 @@ export function usageReport(receipts, { id, instance = 'default', now = Date.now
|
|
|
1281
1307
|
const day = emptyRow({ date });
|
|
1282
1308
|
for (const m of models) {
|
|
1283
1309
|
for (const c of ['calls', 'unpricedCalls', ...COUNTS]) day[c] += m[c];
|
|
1310
|
+
if (m.unmeasuredCalls) day.unmeasuredCalls = (day.unmeasuredCalls ?? 0) + m.unmeasuredCalls;
|
|
1284
1311
|
day.cost += m.costUsd ?? 0;
|
|
1285
1312
|
}
|
|
1286
1313
|
return { ...closeRow(day), byModel: models };
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py
CHANGED
|
@@ -483,7 +483,7 @@ class BoardSession:
|
|
|
483
483
|
return self.post(self.grant.origin, body, self.grant.bearer(), session=self.mcp_session, timeout=self.timeout,
|
|
484
484
|
method=method, protocol_version=self.protocol_version if self.mcp_session else None)
|
|
485
485
|
except _hub.HubError as exc:
|
|
486
|
-
raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored
|
|
486
|
+
raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored, so send it again with "
|
|
487
487
|
"the same idempotency key.", True) from exc
|
|
488
488
|
|
|
489
489
|
def open(self) -> None:
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py
CHANGED
|
@@ -334,11 +334,22 @@ def _authentic(token: Any, jwks: Dict[str, Any], typ: str) -> Dict[str, Any]:
|
|
|
334
334
|
return {"compact": compact, "kid": kid, "x": found["x"]}
|
|
335
335
|
|
|
336
336
|
|
|
337
|
+
_ORG_CONTEXT = re.compile(r"org:[A-Za-z0-9][A-Za-z0-9_-]{0,127}\Z")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def is_partner_actor(node: Any) -> bool:
|
|
341
|
+
"""DEC-0090: a partner actor is exactly {partner: org:<id>}, never with a sub, a session or an act."""
|
|
342
|
+
return isinstance(node, dict) and len(node) == 1 and isinstance(node.get("partner"), str) and _ORG_CONTEXT.match(node["partner"]) is not None
|
|
343
|
+
|
|
344
|
+
|
|
337
345
|
def chain_depth(act: Any) -> float:
|
|
338
|
-
"""Depth of an actor chain; infinity for a chain that is not one (a missing or non-string sub at any level).
|
|
346
|
+
"""Depth of an actor chain; infinity for a chain that is not one (a missing or non-string sub at any level). A
|
|
347
|
+
partner actor ends the chain and counts, never begins it (DEC-0090)."""
|
|
339
348
|
depth = 0
|
|
340
349
|
node = act
|
|
341
350
|
while node is not _MISSING:
|
|
351
|
+
if depth > 0 and is_partner_actor(node):
|
|
352
|
+
return depth + 1
|
|
342
353
|
if not isinstance(node, dict) or not _is_string(node.get("sub")):
|
|
343
354
|
return math.inf
|
|
344
355
|
depth += 1
|
|
@@ -352,7 +363,7 @@ def _chain_of(act: Dict[str, Any]) -> List[str]:
|
|
|
352
363
|
chain = []
|
|
353
364
|
node: Any = act
|
|
354
365
|
while isinstance(node, dict):
|
|
355
|
-
chain.append(node["sub"])
|
|
366
|
+
chain.append(node["partner"] if "partner" in node else node["sub"]) # the contract's chainOf, partner first
|
|
356
367
|
node = node.get("act")
|
|
357
368
|
return chain
|
|
358
369
|
|
|
@@ -379,6 +390,8 @@ def _actor_chain_depth(act: Any) -> Optional[int]:
|
|
|
379
390
|
depth = 0
|
|
380
391
|
node = act
|
|
381
392
|
while node is not _MISSING:
|
|
393
|
+
if depth > 0 and is_partner_actor(node): # DEC-0090: innermost only, it ends the chain
|
|
394
|
+
return depth + 1
|
|
382
395
|
if not isinstance(node, dict) or not _full(AGENT_ID, node.get("sub")):
|
|
383
396
|
return None
|
|
384
397
|
if "session" in node and not _full(OPAQUE_ID, node["session"]):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py
CHANGED
|
@@ -734,9 +734,18 @@ def placement_trust(record: Dict[str, Any], machine_id: str, agent_ref: str, con
|
|
|
734
734
|
if _same(_get(p, "agentRef"), agent_ref) and _same(_context_of(p), context)), None)
|
|
735
735
|
if placement is None:
|
|
736
736
|
return None
|
|
737
|
+
if not _scope_admits(record, _get(placement, "publisherKid"), context): # DEC-0090: a partner's key, in its contexts only
|
|
738
|
+
return {"publisher": []}
|
|
737
739
|
return {"publisher": [k for k in _list(_get(record, "publisherKeys")) if _same(_get(k, "kid"), _get(placement, "publisherKid"))]}
|
|
738
740
|
|
|
739
741
|
|
|
742
|
+
def _scope_admits(record: Any, kid: Any, context: Any) -> bool:
|
|
743
|
+
"""DEC-0090: a key no `publisherScopes[]` entry names is trusted in any context; a partner's key, only in a context
|
|
744
|
+
one of its scopes lists."""
|
|
745
|
+
scopes = [s for s in _list(_get(record, "publisherScopes")) if _same(_get(s, "kid"), kid)]
|
|
746
|
+
return not scopes or any(isinstance(_get(s, "contexts"), list) and _contains(s["contexts"], context) for s in scopes)
|
|
747
|
+
|
|
748
|
+
|
|
740
749
|
#: v4 §7, Freshness: an estate revision's window is at most 30 days (DEC-0055 item 2).
|
|
741
750
|
_MAX_WINDOW_MS = MAX_VALIDITY_SECONDS * 1000
|
|
742
751
|
|
|
@@ -953,6 +962,25 @@ def _placements(value: Dict[str, Any], flag) -> None:
|
|
|
953
962
|
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")))
|
|
954
963
|
|
|
955
964
|
|
|
965
|
+
def _partner_scopes(value: Dict[str, Any], flag) -> None:
|
|
966
|
+
"""FAC-SEM-122 on one record (DEC-0090): a partner's publisher key is scoped once, to a key the record pins, and a
|
|
967
|
+
placement under it is in one of the organization contexts its scope names, never ``personal`` or another."""
|
|
968
|
+
pinned = [_get(k, "kid") for k in _list(_get(value, "publisherKeys"))]
|
|
969
|
+
scoped: List[Any] = []
|
|
970
|
+
for scope, index in _indexed(_get(value, "publisherScopes")):
|
|
971
|
+
if _contains(scoped, _get(scope, "kid")):
|
|
972
|
+
flag("/publisherScopes/%d/kid" % index, "publisher key %s is scoped twice" % _show(_get(scope, "kid")))
|
|
973
|
+
scoped.append(_get(scope, "kid"))
|
|
974
|
+
if not _contains(pinned, _get(scope, "kid")):
|
|
975
|
+
flag("/publisherScopes/%d/kid" % index, "publisher key %s is not in publisherKeys, so the scope limits no key" % _show(_get(scope, "kid")))
|
|
976
|
+
for machine, m in _indexed(_get(value, "machines")):
|
|
977
|
+
for placement, p in _indexed(_get(machine, "packages")):
|
|
978
|
+
if not _scope_admits(value, _get(placement, "publisherKid"), _context_of(placement)):
|
|
979
|
+
flag("/machines/%d/packages/%d/publisherKid" % (m, p),
|
|
980
|
+
"publisher key %s is a partner's, trusted only in the contexts its scope names, not in %s"
|
|
981
|
+
% (_show(_get(placement, "publisherKid")), _show(_context_of(placement))))
|
|
982
|
+
|
|
983
|
+
|
|
956
984
|
def _secret_references(value: Dict[str, Any], flag) -> None:
|
|
957
985
|
machine_ids = [_get(m, "id") for m in _list(_get(value, "machines"))]
|
|
958
986
|
for machine, m in _indexed(_get(value, "machines")):
|
|
@@ -1110,6 +1138,7 @@ def _record_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
1110
1138
|
_revision_whole(value, _finding(findings, "FAC-SEM-072"))
|
|
1111
1139
|
_keys(value, _finding(findings, "FAC-SEM-073"))
|
|
1112
1140
|
_placements(value, _finding(findings, "FAC-SEM-074"))
|
|
1141
|
+
_partner_scopes(value, _finding(findings, "FAC-SEM-122"))
|
|
1113
1142
|
_secret_references(value, _finding(findings, "FAC-SEM-075"))
|
|
1114
1143
|
_grants_and_revocations(value, _finding(findings, "FAC-SEM-076"))
|
|
1115
1144
|
for grant, index in _indexed(_get(value, "shareGrants")): # FAC-SEM-110 (DEC-0082)
|
|
@@ -1156,6 +1185,9 @@ def _installation(value: Dict[str, Any]) -> List[Dict[str, str]]:
|
|
|
1156
1185
|
if _same(_get(secret, "agentRef"), _get(pkg, "agentRef")) and _same(_context_of(secret), context) and not _contains(declared, _get(secret, "slot")):
|
|
1157
1186
|
flag075("/record/machines/%d/secrets/%d/slot" % (machine_index, index),
|
|
1158
1187
|
"the package declares no slot %s: a secret goes only where a placed package declares it" % _show(_get(secret, "slot")))
|
|
1188
|
+
if placement is not None and not _scope_admits(record, _get(placement, "publisherKid"), context):
|
|
1189
|
+
_finding(findings, "FAC-SEM-122")("/record/machines/%d/packages/%d/publisherKid" % (machine_index, placement_index),
|
|
1190
|
+
"publisher key %s is a partner's, not trusted in %s" % (_show(_get(placement, "publisherKid")), _show(context)))
|
|
1159
1191
|
if placement is not None:
|
|
1160
1192
|
flag111 = _finding(findings, "FAC-SEM-111")
|
|
1161
1193
|
for suffix, message in _placement_grant_problems(record, placement):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py
CHANGED
|
@@ -648,7 +648,7 @@ def read_folder(folder: Any) -> Dict[str, bytes]:
|
|
|
648
648
|
continue
|
|
649
649
|
files[rel] = full.read_bytes()
|
|
650
650
|
if refused:
|
|
651
|
-
raise PackageError("Not packed: remove these or pack a clean copy
|
|
651
|
+
raise PackageError("Not packed: remove these or pack a clean copy: " + "; ".join(sorted(refused)))
|
|
652
652
|
return files
|
|
653
653
|
|
|
654
654
|
|