@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +15 -15
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +1 -1
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -4
  10. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +44 -43
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -1
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +12 -12
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +4 -4
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +1 -1
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +4 -4
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +19 -9
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +19 -9
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/events-and-notifications.md +6 -6
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +8 -6
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +26 -26
  26. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -1
  27. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -4
  28. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +10 -10
  29. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +14 -14
  30. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +4 -4
  31. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +3 -3
  32. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -5
  33. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +8 -3
  34. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +20 -43
  35. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +1 -1
  36. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -1
  37. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +9 -2
  38. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +33 -0
  39. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +1 -1
  40. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +40 -13
  41. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -1
  42. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -2
  43. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +32 -0
  44. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +1 -1
  45. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +52 -18
  46. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +1 -1
  47. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +35 -35
  48. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +1 -1
  49. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +27 -27
  50. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
  51. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +31 -31
  52. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +9 -9
  53. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +8 -8
  54. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -6
  55. 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.0';
20
+ export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.1';
21
21
  export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
22
22
  export const EXIT_ALREADY_RUNNING = 75;
23
23
  // Lifecycle rules: references/lifecycle.md (the rule for this kit's users; it implements the public
@@ -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
- export function makeUsageReceipt(provider, model, { inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, costUsd = null, costBasis, at, route, projectId, taskRunId } = {}) {
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 = String(day.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) => Number(day[f] ?? 0) > 0) || Number(day.costUsd ?? 0) > 0;
1156
+ const spent = ['calls', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'].some((f) => toNumber(day[f] ?? 0) > 0) || toNumber(day.costUsd ?? 0) > 0;
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 + Number(m[field] ?? 0), 0);
1138
- if (Number(day[field] ?? 0) !== sum) flag(`${at}/${field}`, `the day's ${field} is not the sum of its models (${String(day[field] ?? 0)} ≠ ${sum})`);
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
- if (typeof day.costUsd === 'number' && Math.abs(day.costUsd - known) > 0.000001 + 1e-9 * Math.max(Math.abs(day.costUsd), Math.abs(known))) flag(`${at}/costUsd`, "the day's cost is not the sum of its models' known costs");
1165
+ // The contract's !close(day, known): Infinity or NaN is never close, so it is a finding.
1166
+ if (typeof day.costUsd === 'number' && !(Math.abs(day.costUsd - known) <= 0.000001 + 1e-9 * Math.max(Math.abs(day.costUsd), Math.abs(known)))) flag(`${at}/costUsd`, "the day's cost is not the sum of its models' known costs");
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: ${String(project.projectId)} names the project ${seen.get(normal)} already names`);
1166
- else seen.set(normal, String(project.projectId));
1190
+ if (seen.has(normal)) flag(`${at}/projectId`, `each project is one row: ${show(project.projectId)} names the project ${seen.get(normal)} already names`);
1191
+ else seen.set(normal, show(project.projectId));
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 };
@@ -483,7 +483,7 @@ class BoardSession:
483
483
  return self.post(self.grant.origin, body, self.grant.bearer(), session=self.mcp_session, timeout=self.timeout,
484
484
  method=method, protocol_version=self.protocol_version if self.mcp_session else None)
485
485
  except _hub.HubError as exc:
486
- raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored — send it again with "
486
+ raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored, so send it again with "
487
487
  "the same idempotency key.", True) from exc
488
488
 
489
489
  def open(self) -> None:
@@ -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 — " + "; ".join(sorted(refused)))
651
+ raise PackageError("Not packed: remove these or pack a clean copy: " + "; ".join(sorted(refused)))
652
652
  return files
653
653
 
654
654
 
@@ -18,6 +18,7 @@ import hashlib
18
18
  import hmac
19
19
  import http.server
20
20
  import json
21
+ import math
21
22
  import ntpath
22
23
  import os
23
24
  from pathlib import Path, PurePath
@@ -46,7 +47,7 @@ EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/service
46
47
  # The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
47
48
  # document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
48
49
  DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
49
- DASHBOARD_KIT = "fabric-dashboard-kit@1.1.0"
50
+ DASHBOARD_KIT = "fabric-dashboard-kit@1.1.1"
50
51
  REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
51
52
  EXIT_ALREADY_RUNNING = 75
52
53
  # Lifecycle rules: references/lifecycle.md, the rule for this kit's users, which implements the public
@@ -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) -> Dict[str, Any]:
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
- return value if isinstance(value, int) and not isinstance(value, bool) and 0 <= value <= 2 ** 53 - 1 else 0
1738
+ """The contract's `count`: Number.isSafeInteger and not negative, so 5.0 (which JavaScript reads as 5) counts."""
1739
+ import fabric_estate # noqa: E402
1740
+ return int(value) if not isinstance(value, bool) and fabric_estate._safe_integer(value) and value >= 0 else 0
1741
+
1742
+
1743
+ def _usage_money(value: Any) -> Optional[float]:
1744
+ """A JSON number as JavaScript holds it (an integer beyond a double is Infinity), or None for anything else."""
1745
+ import fabric_estate # noqa: E402
1746
+ return fabric_estate._double(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
1728
1747
 
1729
1748
 
1730
1749
  def _usage_cost(value: Any) -> float:
1731
- return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) and value == value and value >= 0 and value != float("inf") else 0.0
1750
+ """The contract's `cost`: a known, finite, non-negative cost, or 0."""
1751
+ money = _usage_money(value)
1752
+ return money if money is not None and math.isfinite(money) and money >= 0 else 0.0
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.get(f) or 0) > 0 for f in ("calls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens")
1799
- if isinstance(day.get(f) or 0, (int, float))) or (isinstance(day.get("costUsd"), (int, float)) and day["costUsd"] > 0)
1823
+ spent = any(number(day, f) > 0 for f in ("calls", "inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens")) \
1824
+ or number(day, "costUsd") > 0
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 = sum(m.get(field) or 0 for m in models if isinstance(m.get(field) or 0, (int, float)))
1805
- if (day.get(field) or 0) != total:
1806
- flag("%s/%s" % (at, field), "the day's %s is not the sum of its models (%s ≠ %s)" % (field, day.get(field) or 0, total))
1807
- known = sum(m["costUsd"] for m in models if isinstance(m.get("costUsd"), (int, float)) and not isinstance(m.get("costUsd"), bool))
1808
- day_cost = day.get("costUsd")
1809
- if isinstance(day_cost, (int, float)) and not isinstance(day_cost, bool) and abs(day_cost - known) > 0.000001 + 1e-9 * max(abs(day_cost), abs(known)):
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 = isinstance(project.get("costUsd"), (int, float)) and not isinstance(project.get("costUsd"), bool) and _usage_cost(project.get("costUsd")) == project.get("costUsd")
1888
+ known_cost = _usage_cost(project.get("costUsd")) == _usage_money(project.get("costUsd")) # as JavaScript holds it
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 — %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
261
+ rows = "".join("<li>%s: %s</li>" % (html.escape(e["at"]), html.escape(e["text"]))
262
262
  for e in reversed(svc.log.fetch(None, 20)))
263
263
  return self._send(200, PAGE.format(name=html.escape(svc.name), agent=html.escape("%s.%s" % (svc.id, svc.instance)), status="ready", rows=rows),
264
264
  content_type="text/html; charset=utf-8")