@passioncode-ai/passioncode 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +15 -15
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +44 -43
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +12 -12
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +19 -9
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +19 -9
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +6 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +8 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +26 -26
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +10 -10
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +14 -14
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +4 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +3 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +8 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +20 -43
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +9 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +33 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +40 -13
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +32 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +52 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +35 -35
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +27 -27
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +31 -31
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +9 -9
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +8 -8
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -6
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
|
@@ -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/payload/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:
|
|
@@ -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"]):
|
|
@@ -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):
|
|
@@ -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
|
|
|
@@ -18,6 +18,7 @@ import hashlib
|
|
|
18
18
|
import hmac
|
|
19
19
|
import http.server
|
|
20
20
|
import json
|
|
21
|
+
import math
|
|
21
22
|
import ntpath
|
|
22
23
|
import os
|
|
23
24
|
from pathlib import Path, PurePath
|
|
@@ -46,7 +47,7 @@ EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/service
|
|
|
46
47
|
# The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
|
|
47
48
|
# document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
48
49
|
DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
|
|
49
|
-
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.
|
|
50
|
+
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.1"
|
|
50
51
|
REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
|
|
51
52
|
EXIT_ALREADY_RUNNING = 75
|
|
52
53
|
# Lifecycle rules: references/lifecycle.md, the rule for this kit's users, which implements the public
|
|
@@ -1356,7 +1357,7 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
1356
1357
|
#: Each contract protocol's published revisions, oldest first (the contract's `PROTOCOL_REVISIONS`, DEC-0067); a test
|
|
1357
1358
|
#: holds it equal to the contract's register at the pin.
|
|
1358
1359
|
PROTOCOL_REVISIONS: Dict[str, Tuple[str, ...]] = {
|
|
1359
|
-
"access-log": ("1",), "approval": ("1",), "activity-summary": ("1",), "enroll-token": ("1",), "entitlement": ("1",),
|
|
1360
|
+
"access-log": ("1",), "approval": ("1",), "activity-summary": ("1",), "coding-agents": ("1",), "enroll-token": ("1",), "entitlement": ("1",),
|
|
1360
1361
|
"entitlement-key-set": ("1",), "estate-record": ("1",), "execution-packet": ("1",),
|
|
1361
1362
|
"fabric-activity": ("0.1",), "fabric-dashboard": ("0.1",), "fabric-delegation": ("0.1",), "fabric-device": ("0.1",),
|
|
1362
1363
|
"fabric-interop": ("0.1", "0.2"), "fabric-local-channel": ("0.1",), "fabric-local-runner": ("0.1",),
|
|
@@ -1364,7 +1365,7 @@ PROTOCOL_REVISIONS: Dict[str, Tuple[str, ...]] = {
|
|
|
1364
1365
|
"fabric-provider": ("0.1",), "fabric-relay": ("0.1",), "fabric-secret-envelope": ("1",), "fabric-service": ("0.1",),
|
|
1365
1366
|
"fabric-settings-backup": ("1",), "heartbeat": ("1",), "learning-note": ("1",), "memory": ("0.1",), "pairing-message": ("1",), "pipeline": ("0.1",),
|
|
1366
1367
|
"revocation-delta": ("1",), "session-handoff": ("1",), "share-registration": ("1",),
|
|
1367
|
-
"share-registration-receipt": ("1",), "window-lease": ("1",),
|
|
1368
|
+
"share-registration-receipt": ("1",), "support-quality-daily": ("1",), "window-lease": ("1",),
|
|
1368
1369
|
}
|
|
1369
1370
|
MAX_PROTOCOLS = 64
|
|
1370
1371
|
_REVISION = re.compile(r"([a-z][a-z0-9.-]{0,63})/([0-9]{1,4}(?:\.[0-9]{1,4}){0,2})", re.ASCII)
|
|
@@ -1622,9 +1623,13 @@ def make_usage_receipt(provider: str, model: str, *, input_tokens: int, output_t
|
|
|
1622
1623
|
cache_read_tokens: int = 0, cache_write_tokens: int = 0,
|
|
1623
1624
|
cost_usd: Optional[float] = None, cost_basis: Optional[str] = None,
|
|
1624
1625
|
at: Optional[str] = None, route: Optional[str] = None,
|
|
1625
|
-
project_id: Optional[str] = None, task_run_id: Optional[str] = None
|
|
1626
|
+
project_id: Optional[str] = None, task_run_id: Optional[str] = None,
|
|
1627
|
+
tokens_measured: bool = True) -> Dict[str, Any]:
|
|
1626
1628
|
"""One model call, from the provider's own usage numbers. No prompt, output or caller.
|
|
1627
1629
|
|
|
1630
|
+
`tokens_measured=False` marks a call whose token counts are a lower bound or unknown (the provider returned none,
|
|
1631
|
+
the stream was cut): the report counts it in `unmeasuredCalls` and its token totals are a lower bound (DEC-0091).
|
|
1632
|
+
|
|
1628
1633
|
`project_id` (a URI, kept in its normal form, DEC-0076) and `task_run_id` (the service's own run id) attribute the
|
|
1629
1634
|
call to the project the service ran it for (DEC-0065); they never name a caller, session or person.
|
|
1630
1635
|
|
|
@@ -1650,6 +1655,10 @@ def make_usage_receipt(provider: str, model: str, *, input_tokens: int, output_t
|
|
|
1650
1655
|
raise ServiceError("route is subscription, key or platform-metered (DEC-0046, DEC-0061).")
|
|
1651
1656
|
receipt = {"at": at or now_iso(), "provider": provider, "model": model, **counts,
|
|
1652
1657
|
"costUsd": None if cost_usd is None else float(cost_usd), "costBasis": basis}
|
|
1658
|
+
if tokens_measured is not True:
|
|
1659
|
+
if tokens_measured is not False:
|
|
1660
|
+
raise ServiceError("tokens_measured is True or False.")
|
|
1661
|
+
receipt["tokensMeasured"] = False
|
|
1653
1662
|
if route is not None:
|
|
1654
1663
|
receipt["route"] = route # the operator's own login in its vendor's runner, an issued key, or the platform's metered models
|
|
1655
1664
|
if task_run_id is not None and project_id is None:
|
|
@@ -1703,6 +1712,8 @@ def _by_project(receipts: List[Dict[str, Any]], first: str, last: str) -> List[D
|
|
|
1703
1712
|
|
|
1704
1713
|
def _add_totals(row: Dict[str, Any], receipt: Dict[str, Any]) -> None:
|
|
1705
1714
|
row["calls"] += 1
|
|
1715
|
+
if receipt.get("tokensMeasured") is False:
|
|
1716
|
+
row["unmeasuredCalls"] = row.get("unmeasuredCalls", 0) + 1 # written only when above 0: absent is 0 (DEC-0091)
|
|
1706
1717
|
for name in ("inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"):
|
|
1707
1718
|
row[name] += int(receipt.get(name) or 0)
|
|
1708
1719
|
if receipt.get("costUsd") is None:
|
|
@@ -1724,11 +1735,21 @@ _DAY_MS = 86_400_000
|
|
|
1724
1735
|
|
|
1725
1736
|
|
|
1726
1737
|
def _usage_count(value: Any) -> int:
|
|
1727
|
-
|
|
1738
|
+
"""The contract's `count`: Number.isSafeInteger and not negative, so 5.0 (which JavaScript reads as 5) counts."""
|
|
1739
|
+
import fabric_estate # noqa: E402
|
|
1740
|
+
return int(value) if not isinstance(value, bool) and fabric_estate._safe_integer(value) and value >= 0 else 0
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
def _usage_money(value: Any) -> Optional[float]:
|
|
1744
|
+
"""A JSON number as JavaScript holds it (an integer beyond a double is Infinity), or None for anything else."""
|
|
1745
|
+
import fabric_estate # noqa: E402
|
|
1746
|
+
return fabric_estate._double(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
|
|
1728
1747
|
|
|
1729
1748
|
|
|
1730
1749
|
def _usage_cost(value: Any) -> float:
|
|
1731
|
-
|
|
1750
|
+
"""The contract's `cost`: a known, finite, non-negative cost, or 0."""
|
|
1751
|
+
money = _usage_money(value)
|
|
1752
|
+
return money if money is not None and math.isfinite(money) and money >= 0 else 0.0
|
|
1732
1753
|
|
|
1733
1754
|
|
|
1734
1755
|
def _exceeds(a: float, b: float) -> bool:
|
|
@@ -1760,6 +1781,8 @@ def _priced_row(row: Dict[str, Any], at: str) -> List[Dict[str, str]]:
|
|
|
1760
1781
|
flag = lambda path, message: [{"code": "FAC-SEM-025", "instancePath": path, "message": message}] # noqa: E731
|
|
1761
1782
|
if unpriced > calls:
|
|
1762
1783
|
return flag(at + "/unpricedCalls", "more unpriced calls than calls")
|
|
1784
|
+
if _usage_count(row.get("unmeasuredCalls")) > calls: # DEC-0091: unknown token counts are some of the calls
|
|
1785
|
+
return flag(at + "/unmeasuredCalls", "more calls with unknown tokens than calls")
|
|
1763
1786
|
if calls > 0 and unpriced == calls and row.get("costUsd", _MISSING_COST) is not None:
|
|
1764
1787
|
return flag(at + "/costUsd", "every call is unpriced, so the cost is unknown (null), not a number")
|
|
1765
1788
|
if unpriced < calls and row.get("costUsd", _MISSING_COST) is None:
|
|
@@ -1785,8 +1808,7 @@ def usage_findings(report: Any) -> List[Dict[str, str]]:
|
|
|
1785
1808
|
previous = ""
|
|
1786
1809
|
for i, day in enumerate(days):
|
|
1787
1810
|
at = "/days/%d" % i
|
|
1788
|
-
date = day.get("date")
|
|
1789
|
-
date = "" if date is None else (date if isinstance(date, str) else json.dumps(date))
|
|
1811
|
+
date = fabric_estate._show("" if day.get("date") is None else day["date"]) # the contract's show(day.date ?? "")
|
|
1790
1812
|
if date <= previous:
|
|
1791
1813
|
flag(at + "/date", "days must run forward without a repeat (%s after %s)" % (date, previous))
|
|
1792
1814
|
previous = date
|
|
@@ -1794,19 +1816,29 @@ def usage_findings(report: Any) -> List[Dict[str, str]]:
|
|
|
1794
1816
|
models = _usage_list(day.get("byModel"))
|
|
1795
1817
|
for j, m in enumerate(models):
|
|
1796
1818
|
findings.extend(_priced_row(m, "%s/byModel/%d" % (at, j)))
|
|
1819
|
+
# The contract reads every count as `toNumber(x ?? 0)` and writes it with `show`: a string count is read as a
|
|
1820
|
+
# number, and what is no number (an object, "abc") is NaN, which equals no sum, so it is a finding.
|
|
1821
|
+
number = lambda row, f: fabric_estate._js_to_number(row.get(f)) # noqa: E731 - None and absent read as 0
|
|
1797
1822
|
if not models:
|
|
1798
|
-
spent = any((day
|
|
1799
|
-
|
|
1823
|
+
spent = any(number(day, f) > 0 for f in ("calls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens")) \
|
|
1824
|
+
or number(day, "costUsd") > 0
|
|
1800
1825
|
if spent:
|
|
1801
1826
|
flag(at + "/byModel", "a day with calls, tokens or cost names the models they belong to")
|
|
1802
1827
|
continue
|
|
1803
|
-
for field in ("calls", "unpricedCalls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"):
|
|
1804
|
-
total =
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1828
|
+
for field in ("calls", "unpricedCalls", "unmeasuredCalls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"):
|
|
1829
|
+
total = 0.0
|
|
1830
|
+
for m in models:
|
|
1831
|
+
total += number(m, field) # in order, in doubles, as JavaScript adds
|
|
1832
|
+
if number(day, field) != total:
|
|
1833
|
+
shown = fabric_estate._show(0 if day.get(field) is None else day[field])
|
|
1834
|
+
flag("%s/%s" % (at, field), "the day's %s is not the sum of its models (%s ≠ %s)" % (field, shown, fabric_estate._js_string(total)))
|
|
1835
|
+
known = 0.0
|
|
1836
|
+
for m in models:
|
|
1837
|
+
money = _usage_money(m.get("costUsd"))
|
|
1838
|
+
known += money if money is not None else 0.0
|
|
1839
|
+
day_cost = _usage_money(day.get("costUsd"))
|
|
1840
|
+
# The contract's !close(day, known): Infinity or NaN is never close, so it is a finding.
|
|
1841
|
+
if day_cost is not None and not abs(day_cost - known) <= 0.000001 + 1e-9 * max(abs(day_cost), abs(known)):
|
|
1810
1842
|
flag(at + "/costUsd", "the day's cost is not the sum of its models' known costs")
|
|
1811
1843
|
# The window.
|
|
1812
1844
|
if days:
|
|
@@ -1853,7 +1885,7 @@ def usage_findings(report: Any) -> List[Dict[str, str]]:
|
|
|
1853
1885
|
run_priced += max(0, _usage_count(run.get("calls")) - _usage_count(run.get("unpricedCalls")))
|
|
1854
1886
|
findings.extend(_priced_row(run, "%s/taskRuns/%d" % (at, r)))
|
|
1855
1887
|
project_priced = max(0, _usage_count(project.get("calls")) - _usage_count(project.get("unpricedCalls")))
|
|
1856
|
-
known_cost =
|
|
1888
|
+
known_cost = _usage_cost(project.get("costUsd")) == _usage_money(project.get("costUsd")) # as JavaScript holds it
|
|
1857
1889
|
if run_calls > _usage_count(project.get("calls")):
|
|
1858
1890
|
flag(at + "/taskRuns", "a project's task runs hold more calls than the project (%d > %d)" % (run_calls, _usage_count(project.get("calls"))))
|
|
1859
1891
|
elif run_priced > project_priced:
|
|
@@ -2001,6 +2033,8 @@ def usage_report(receipts: Iterable[Dict[str, Any]], *, service_id: str, instanc
|
|
|
2001
2033
|
for m in models:
|
|
2002
2034
|
for name in ("calls", "unpricedCalls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"):
|
|
2003
2035
|
day[name] += m[name]
|
|
2036
|
+
if m.get("unmeasuredCalls"):
|
|
2037
|
+
day["unmeasuredCalls"] = day.get("unmeasuredCalls", 0) + m["unmeasuredCalls"]
|
|
2004
2038
|
day["_cost"] += m["costUsd"] or 0.0
|
|
2005
2039
|
day = _close_totals(day)
|
|
2006
2040
|
day["byModel"] = models
|
|
@@ -258,7 +258,7 @@ def make_handler(svc: Service):
|
|
|
258
258
|
if url.path == "/":
|
|
259
259
|
if not self._session_ok():
|
|
260
260
|
return self._send(401, "Open this dashboard from Fabric Dashboards.", content_type="text/plain")
|
|
261
|
-
rows = "".join("<li>%s
|
|
261
|
+
rows = "".join("<li>%s: %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
|
|
262
262
|
for e in reversed(svc.log.fetch(None, 20)))
|
|
263
263
|
return self._send(200, PAGE.format(name=html.escape(svc.name), agent=html.escape("%s.%s" % (svc.id, svc.instance)), status="ready", rows=rows),
|
|
264
264
|
content_type="text/html; charset=utf-8")
|