@audiofool/moondog 0.1.0 → 0.2.0-beta.3

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 (125) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +156 -68
  3. package/contracts/v1/definitions.schema.json +1 -169
  4. package/package.json +7 -23
  5. package/scripts/contract-lib.mjs +0 -7
  6. package/scripts/contract-semantics.mjs +0 -1904
  7. package/scripts/moondog.mjs +285 -74
  8. package/src/core/apple-library-source-status.mjs +0 -16
  9. package/src/core/apple-projection-domain-services.mjs +253 -53
  10. package/src/core/application-update.mjs +320 -0
  11. package/src/core/capability-catalog.mjs +290 -55
  12. package/src/core/listener-avoids.mjs +101 -0
  13. package/src/core/listening-now.mjs +108 -0
  14. package/src/core/listening-profile-domain-services.mjs +189 -41
  15. package/src/core/listening-upkeep.mjs +31 -0
  16. package/src/core/moondog-application.mjs +2115 -107
  17. package/src/core/playback-preferences.mjs +97 -0
  18. package/src/core/spotify-listening-intent.mjs +124 -0
  19. package/src/core/synthetic-domain-services.mjs +24 -3
  20. package/src/core/track-title.mjs +107 -0
  21. package/src/demo/moondog-demo.mjs +4 -22
  22. package/src/i18n/index.mjs +285 -0
  23. package/src/i18n/messages/de.mjs +135 -0
  24. package/src/i18n/messages/en.mjs +136 -0
  25. package/src/i18n/messages/es.mjs +135 -0
  26. package/src/i18n/messages/fr.mjs +135 -0
  27. package/src/i18n/messages/it.mjs +135 -0
  28. package/src/i18n/messages/ja.mjs +135 -0
  29. package/src/i18n/messages/ko.mjs +135 -0
  30. package/src/i18n/messages/pt.mjs +135 -0
  31. package/src/i18n/messages/zh-Hant.mjs +135 -0
  32. package/src/i18n/messages/zh.mjs +135 -0
  33. package/src/i18n/preferences.mjs +34 -0
  34. package/src/i18n/screen/es.mjs +967 -0
  35. package/src/i18n/screen/ja.mjs +967 -0
  36. package/src/i18n/screen/pt.mjs +967 -0
  37. package/src/i18n/screen/zh.mjs +967 -0
  38. package/src/importers/apple-music-library/index.mjs +0 -16
  39. package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
  40. package/src/integrations/apple-music/catalog.mjs +166 -14
  41. package/src/integrations/apple-music/music-app.mjs +232 -0
  42. package/src/integrations/apple-music/storefront.mjs +28 -0
  43. package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
  44. package/src/integrations/lastfm/history.mjs +221 -0
  45. package/src/integrations/lastfm/settings.mjs +75 -0
  46. package/src/integrations/netease/ncm-cli-player.mjs +328 -0
  47. package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
  48. package/src/integrations/public-playlists.mjs +2 -1
  49. package/src/integrations/spotify/account-data-profile.mjs +14 -6
  50. package/src/integrations/spotify/authentication.mjs +149 -15
  51. package/src/integrations/spotify/catalog-resolver.mjs +9 -6
  52. package/src/integrations/spotify/connection.mjs +2 -1
  53. package/src/integrations/spotify/credential-store.mjs +4 -0
  54. package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
  55. package/src/integrations/spotify/history-archive.mjs +1 -1
  56. package/src/integrations/spotify/library-snapshot.mjs +227 -0
  57. package/src/integrations/spotify/recent-activity.mjs +2 -0
  58. package/src/integrations/spotify/service.mjs +873 -69
  59. package/src/integrations/spotify/web-api-client.mjs +410 -127
  60. package/src/integrations/web/brave-web.mjs +45 -0
  61. package/src/integrations/web/codex-web.mjs +15 -82
  62. package/src/integrations/web/deepseek-web.mjs +81 -0
  63. package/src/integrations/web/web-research.mjs +119 -0
  64. package/src/integrations/web/web-result.mjs +215 -0
  65. package/src/integrations/youtube-music/takeout.mjs +3 -1
  66. package/src/memory/local-memory-store.mjs +542 -57
  67. package/src/profile/history-import.mjs +42 -6
  68. package/src/profile/listener-corrections.mjs +22 -0
  69. package/src/profile/listener-profile-build.mjs +461 -0
  70. package/src/profile/listener-profile-store.mjs +130 -0
  71. package/src/profile/listener-profile-verification.mjs +68 -0
  72. package/src/profile/listener-profile-worker.mjs +179 -0
  73. package/src/profile/listening-history-store.mjs +640 -17
  74. package/src/profile/listening-profile-projection.mjs +454 -86
  75. package/src/profile/local-music-data.mjs +1 -1
  76. package/src/profile/moondog-suggestions.mjs +184 -0
  77. package/src/profile/music-providers.mjs +11 -7
  78. package/src/profile/profile-exploration.mjs +92 -0
  79. package/src/profile/recorded-measures.mjs +24 -0
  80. package/src/profile/spotify-archive-taste.mjs +0 -4
  81. package/src/runtime/pi/agent-runtime.mjs +2536 -692
  82. package/src/runtime/pi/authentication.mjs +34 -1
  83. package/src/runtime/pi/configured-runtime.mjs +22 -6
  84. package/src/runtime/pi/conversation-name.mjs +53 -0
  85. package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
  86. package/src/runtime/pi/model-catalog.mjs +28 -5
  87. package/src/runtime/pi/model-settings.mjs +202 -0
  88. package/src/runtime/pi/model-transport.mjs +10 -0
  89. package/src/runtime/pi/persistent-credential-store.mjs +67 -31
  90. package/src/runtime/pi/profile-build-runtime.mjs +161 -0
  91. package/src/runtime/pi/public-model-catalog.mjs +288 -0
  92. package/src/runtime/pi/runtime-settings.mjs +10 -2
  93. package/src/runtime/pi/skills/listener-profile/SKILL.md +48 -0
  94. package/src/runtime/pi/tool-trace.mjs +37 -0
  95. package/src/surfaces/cli/auth-command.mjs +12 -4
  96. package/src/surfaces/cli/brand-components.mjs +310 -132
  97. package/src/surfaces/cli/brand-theme.mjs +51 -28
  98. package/src/surfaces/cli/command-completions.mjs +58 -42
  99. package/src/surfaces/cli/format-output.mjs +199 -388
  100. package/src/surfaces/cli/history-import-view.mjs +292 -139
  101. package/src/surfaces/cli/import-guides.mjs +2 -0
  102. package/src/surfaces/cli/listener-profile-command.mjs +92 -0
  103. package/src/surfaces/cli/listening-menu.mjs +7 -4
  104. package/src/surfaces/cli/profile-command.mjs +30 -21
  105. package/src/surfaces/cli/seasons-chart.mjs +87 -0
  106. package/src/surfaces/cli/spotify-command.mjs +54 -6
  107. package/src/surfaces/cli/taste-profile-model.mjs +78 -79
  108. package/src/surfaces/cli/taste-profile-view.mjs +33 -26
  109. package/src/surfaces/cli/terminal-art.mjs +140 -35
  110. package/src/surfaces/cli/tui.mjs +1066 -357
  111. package/src/surfaces/cli/update-command.mjs +32 -0
  112. package/src/surfaces/cli/web-command.mjs +53 -14
  113. package/src/surfaces/html/tasteprint.mjs +43 -70
  114. package/contracts/v1/capability-policy.schema.json +0 -572
  115. package/contracts/v1/profile-evidence.schema.json +0 -237
  116. package/contracts/v1/tool-invocation.schema.json +0 -318
  117. package/contracts/v1/tool-receipt.schema.json +0 -1066
  118. package/scripts/demo-studio.mjs +0 -58
  119. package/src/core/apple-data-migration.mjs +0 -50
  120. package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
  121. package/src/demo/moondog-tasteprint-demo.mjs +0 -631
  122. package/src/integrations/spotify/index.mjs +0 -80
  123. package/src/surfaces/web/showcase.mjs +0 -272
  124. package/src/surfaces/web/studio-page.mjs +0 -4148
  125. package/src/surfaces/web/studio.mjs +0 -2288
@@ -1,61 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- const riskDomains = ["privacy", "account", "copyright", "spend"];
4
- const riskRank = new Map([
5
- ["none", 0],
6
- ["low", 1],
7
- ["medium", 2],
8
- ["high", 3],
9
- ]);
10
- const effectKinds = new Set([
11
- "read_external",
12
- "write_local",
13
- "write_external",
14
- "send_message",
15
- "spend",
16
- "publish",
17
- "delete",
18
- "use_credentials",
19
- ]);
20
- const executionModes = new Set(["plan", "dry_run", "execute"]);
21
- const namespacedKeyPattern =
22
- /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/;
23
- const uuidPattern =
24
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
25
- const mutatingEffects = new Set([
26
- "write_local",
27
- "write_external",
28
- "send_message",
29
- "spend",
30
- "publish",
31
- "delete",
32
- ]);
33
- const accountEffects = new Set([
34
- "write_external",
35
- "send_message",
36
- "spend",
37
- "publish",
38
- "delete",
39
- "use_credentials",
40
- ]);
41
- const allowReceiptStatuses = new Set([
42
- "simulated",
43
- "accepted",
44
- "succeeded",
45
- "failed",
46
- "timed_out",
47
- "partial",
48
- "unknown",
49
- "cancelled",
50
- ]);
51
- const executedReceiptStatuses = new Set([
52
- "accepted",
53
- "succeeded",
54
- "failed",
55
- "timed_out",
56
- "partial",
57
- "unknown",
58
- ]);
59
3
  const sensitiveKeyNames = new Set([
60
4
  "accesstoken",
61
5
  "apikey",
@@ -147,81 +91,6 @@ export function sha256Hex(value) {
147
91
  return createHash("sha256").update(source, "utf8").digest("hex");
148
92
  }
149
93
 
150
- export function invocationSemanticPayload(invocation) {
151
- if (!isPlainObject(invocation)) {
152
- throw new TypeError("ToolInvocation must be an object");
153
- }
154
-
155
- const payload = structuredClone(invocation);
156
- delete payload.semantic_digest;
157
- delete payload.confirmation_grant_ref;
158
-
159
- for (const field of [
160
- "declared_effects",
161
- "declared_risk_flags",
162
- "rights_basis_refs",
163
- ]) {
164
- payload[field]?.sort(compareCanonicalValues);
165
- }
166
- for (const field of [
167
- "source_refs",
168
- "data_classes",
169
- "planned_destinations",
170
- ]) {
171
- payload.data_manifest?.[field]?.sort(compareCanonicalValues);
172
- }
173
- payload.budget_request?.sort(compareCanonicalValues);
174
- return payload;
175
- }
176
-
177
- function compareCanonicalValues(left, right) {
178
- const leftValue = canonicalizeJson(left);
179
- const rightValue = canonicalizeJson(right);
180
- if (leftValue < rightValue) return -1;
181
- if (leftValue > rightValue) return 1;
182
- return 0;
183
- }
184
-
185
- export function computeInvocationSemanticDigest(invocation) {
186
- return sha256Hex(invocationSemanticPayload(invocation));
187
- }
188
-
189
- export function computeConfirmationScopeDigest({
190
- invocationSemanticDigest,
191
- capabilityRegistryVersion,
192
- }) {
193
- return sha256Hex({
194
- kind: "moondog.confirmation_scope",
195
- version: 1,
196
- invocation_semantic_digest: invocationSemanticDigest,
197
- capability_registry_version: capabilityRegistryVersion,
198
- });
199
- }
200
-
201
- function externalIdentity(externalRef) {
202
- if (!externalRef) return null;
203
- return {
204
- system: externalRef.system,
205
- entity_type: externalRef.entity_type,
206
- external_id: externalRef.external_id,
207
- };
208
- }
209
-
210
- export function computeIdempotencyKeyDigest(invocation) {
211
- if (typeof invocation?.idempotency_key !== "string") {
212
- throw new TypeError("Invocation idempotency key must be a string");
213
- }
214
- return sha256Hex({
215
- kind: "moondog.idempotency_scope",
216
- version: 1,
217
- subject_id: invocation.subject_id,
218
- capability_id: invocation.capability_id,
219
- capability_version: invocation.capability_version,
220
- account_identity: externalIdentity(invocation.account_ref),
221
- idempotency_key: invocation.idempotency_key,
222
- });
223
- }
224
-
225
94
  function inspectUrl(value, path, issues) {
226
95
  if (!/^[a-z][a-z0-9+.-]*:/i.test(value)) return;
227
96
 
@@ -379,1785 +248,12 @@ function canonicalEqual(left, right) {
379
248
  return canonicalizeJson(left) === canonicalizeJson(right);
380
249
  }
381
250
 
382
- function exactSetEqual(left, right) {
383
- if (!Array.isArray(left) || !Array.isArray(right)) return false;
384
- const leftSet = new Set(left);
385
- const rightSet = new Set(right);
386
- if (leftSet.size !== left.length || rightSet.size !== right.length) return false;
387
- if (leftSet.size !== rightSet.size) return false;
388
- return [...leftSet].every((value) => rightSet.has(value));
389
- }
390
-
391
- function includesCanonical(items, expected) {
392
- return Array.isArray(items) && items.some((item) => canonicalEqual(item, expected));
393
- }
394
-
395
- function includesExternalIdentity(items, expected) {
396
- return (
397
- Array.isArray(items) &&
398
- items.some((item) =>
399
- canonicalEqual(externalIdentity(item), externalIdentity(expected)),
400
- )
401
- );
402
- }
403
-
404
251
  function parseTimestamp(value) {
405
252
  if (typeof value !== "string") return null;
406
253
  const timestamp = Date.parse(value);
407
254
  return Number.isFinite(timestamp) ? timestamp : null;
408
255
  }
409
256
 
410
- function appendUniqueKeyIssues(items, keyFor, path, code, issues) {
411
- const seen = new Set();
412
- for (const item of items ?? []) {
413
- const key = keyFor(item);
414
- if (seen.has(key)) {
415
- issues.push(issue(code, path, "Semantic keys must be unique."));
416
- return;
417
- }
418
- seen.add(key);
419
- }
420
- }
421
-
422
- export function validateCapabilityDescriptor(capability) {
423
- const issues = [];
424
- if (!isPlainObject(capability)) {
425
- issues.push(
426
- issue(
427
- "registry.descriptor_missing",
428
- "/capability",
429
- "A trusted capability descriptor is required.",
430
- ),
431
- );
432
- return { ok: false, issues };
433
- }
434
-
435
- if (
436
- typeof capability.registry_version !== "string" ||
437
- capability.registry_version.trim() === ""
438
- ) {
439
- issues.push(
440
- issue(
441
- "registry.version_invalid",
442
- "/capability/registry_version",
443
- "The trusted registry version must be a non-empty string.",
444
- ),
445
- );
446
- }
447
- if (
448
- typeof capability.capability_id !== "string" ||
449
- !namespacedKeyPattern.test(capability.capability_id)
450
- ) {
451
- issues.push(
452
- issue(
453
- "registry.capability_id_invalid",
454
- "/capability/capability_id",
455
- "The trusted capability identifier is invalid.",
456
- ),
457
- );
458
- }
459
- if (
460
- !Number.isInteger(capability.capability_version) ||
461
- capability.capability_version < 1
462
- ) {
463
- issues.push(
464
- issue(
465
- "registry.capability_version_invalid",
466
- "/capability/capability_version",
467
- "The trusted capability version must be a positive integer.",
468
- ),
469
- );
470
- }
471
- if (
472
- !isPlainObject(capability.tool_ref) ||
473
- typeof capability.tool_ref.tool_id !== "string" ||
474
- !namespacedKeyPattern.test(capability.tool_ref.tool_id) ||
475
- !Number.isInteger(capability.tool_ref.contract_version) ||
476
- capability.tool_ref.contract_version < 1
477
- ) {
478
- issues.push(
479
- issue(
480
- "registry.tool_ref_invalid",
481
- "/capability/tool_ref",
482
- "The trusted tool reference is invalid.",
483
- ),
484
- );
485
- }
486
-
487
- if (
488
- !Array.isArray(capability.effects) ||
489
- capability.effects.length === 0 ||
490
- capability.effects.some((effect) => !effectKinds.has(effect))
491
- ) {
492
- issues.push(
493
- issue(
494
- "registry.effects_invalid",
495
- "/capability/effects",
496
- "Trusted effects must be a non-empty array of known effect kinds.",
497
- ),
498
- );
499
- } else {
500
- appendUniqueKeyIssues(
501
- capability.effects,
502
- (value) => value,
503
- "/capability/effects",
504
- "registry.duplicate_effect",
505
- issues,
506
- );
507
- }
508
-
509
- if (
510
- !Array.isArray(capability.allowed_execution_modes) ||
511
- capability.allowed_execution_modes.length === 0 ||
512
- capability.allowed_execution_modes.some((mode) => !executionModes.has(mode))
513
- ) {
514
- issues.push(
515
- issue(
516
- "registry.execution_modes_invalid",
517
- "/capability/allowed_execution_modes",
518
- "Trusted execution modes must be a non-empty array of known modes.",
519
- ),
520
- );
521
- } else {
522
- appendUniqueKeyIssues(
523
- capability.allowed_execution_modes,
524
- (value) => value,
525
- "/capability/allowed_execution_modes",
526
- "registry.duplicate_execution_mode",
527
- issues,
528
- );
529
- }
530
-
531
- if (isPlainObject(capability.risk_levels)) {
532
- const keys = Object.keys(capability.risk_levels);
533
- if (!exactSetEqual(keys, riskDomains)) {
534
- issues.push(
535
- issue(
536
- "registry.risk_domains_incomplete",
537
- "/capability/risk_levels",
538
- "The trusted descriptor must assign a level to every v1 risk domain.",
539
- ),
540
- );
541
- }
542
- for (const level of Object.values(capability.risk_levels)) {
543
- if (!riskRank.has(level)) {
544
- issues.push(
545
- issue(
546
- "registry.risk_level_unknown",
547
- "/capability/risk_levels",
548
- "The trusted descriptor contains an unknown risk level.",
549
- ),
550
- );
551
- break;
552
- }
553
- }
554
- } else {
555
- issues.push(
556
- issue(
557
- "registry.risk_levels_invalid",
558
- "/capability/risk_levels",
559
- "The trusted descriptor must provide a risk-level object.",
560
- ),
561
- );
562
- }
563
-
564
- return { ok: issues.length === 0, issues };
565
- }
566
-
567
- function validateChronology(invocation, policy, now, issues) {
568
- const requestedAt = parseTimestamp(invocation.requested_at);
569
- const expiresAt = parseTimestamp(invocation.expires_at);
570
- const validFrom = parseTimestamp(policy.valid_from);
571
- const validUntil = policy.valid_until
572
- ? parseTimestamp(policy.valid_until)
573
- : Number.POSITIVE_INFINITY;
574
- const nowValue = now instanceof Date ? now.getTime() : parseTimestamp(now);
575
-
576
- if (
577
- requestedAt === null ||
578
- expiresAt === null ||
579
- validFrom === null ||
580
- validUntil === null ||
581
- nowValue === null ||
582
- !Number.isFinite(nowValue)
583
- ) {
584
- issues.push(
585
- issue(
586
- "time.invalid",
587
- "/requested_at",
588
- "Invocation, policy, and evaluation timestamps must be valid.",
589
- ),
590
- );
591
- return;
592
- }
593
-
594
- if (requestedAt >= expiresAt) {
595
- issues.push(
596
- issue(
597
- "time.invocation_order",
598
- "/expires_at",
599
- "Invocation expiry must follow its request time.",
600
- ),
601
- );
602
- }
603
- if (validFrom >= validUntil) {
604
- issues.push(
605
- issue(
606
- "time.policy_order",
607
- "/policy/valid_until",
608
- "Policy expiry must follow its start time.",
609
- ),
610
- );
611
- }
612
- if (nowValue < requestedAt || nowValue >= expiresAt) {
613
- issues.push(
614
- issue(
615
- "time.invocation_inactive",
616
- "/expires_at",
617
- "The invocation is not active at evaluation time.",
618
- ),
619
- );
620
- }
621
- if (nowValue < validFrom || nowValue >= validUntil) {
622
- issues.push(
623
- issue(
624
- "time.policy_inactive",
625
- "/policy/valid_until",
626
- "The policy is not active at evaluation time.",
627
- ),
628
- );
629
- }
630
- }
631
-
632
- function validatePrivacy(invocation, policy, issues) {
633
- if (invocation.purpose_id !== policy.privacy?.purpose_id) {
634
- issues.push(
635
- issue(
636
- "privacy.purpose_denied",
637
- "/purpose_id",
638
- "The invocation purpose is outside the selected policy.",
639
- ),
640
- );
641
- }
642
- const allowedDataClasses = new Set(policy.privacy?.allowed_data_classes ?? []);
643
- for (const dataClass of invocation.data_manifest?.data_classes ?? []) {
644
- if (!allowedDataClasses.has(dataClass)) {
645
- issues.push(
646
- issue(
647
- "privacy.data_class_denied",
648
- "/data_manifest/data_classes",
649
- "The invocation contains a data class outside the selected policy.",
650
- ),
651
- );
652
- }
653
- }
654
-
655
- for (const destination of invocation.data_manifest?.planned_destinations ?? []) {
656
- const allowed = (policy.privacy?.allowed_destinations ?? []).some(
657
- (candidate) =>
658
- candidate.destination_type === destination.destination_type &&
659
- candidate.system === destination.system &&
660
- candidate.purpose_id === destination.purpose_id,
661
- );
662
- if (!allowed) {
663
- issues.push(
664
- issue(
665
- "privacy.destination_denied",
666
- "/data_manifest/planned_destinations",
667
- "The invocation contains a destination outside the selected policy.",
668
- ),
669
- );
670
- }
671
- }
672
-
673
- if (
674
- invocation.data_manifest?.raw_data_included === true &&
675
- policy.privacy?.raw_data_egress_allowed !== true
676
- ) {
677
- issues.push(
678
- issue(
679
- "privacy.raw_egress_denied",
680
- "/data_manifest/raw_data_included",
681
- "The selected policy does not permit raw data egress.",
682
- ),
683
- );
684
- }
685
-
686
- if (
687
- policy.privacy?.rights_basis_required === true &&
688
- !(invocation.rights_basis_refs?.length > 0)
689
- ) {
690
- issues.push(
691
- issue(
692
- "privacy.rights_basis_missing",
693
- "/rights_basis_refs",
694
- "The selected policy requires a rights basis.",
695
- ),
696
- );
697
- }
698
- }
699
-
700
- function validateBudgets(invocation, policy, capability, issues) {
701
- appendUniqueKeyIssues(
702
- policy.budgets,
703
- (item) => item.resource,
704
- "/policy/budgets",
705
- "budget.policy_resource_duplicate",
706
- issues,
707
- );
708
- appendUniqueKeyIssues(
709
- invocation.budget_request,
710
- (item) => item.resource,
711
- "/budget_request",
712
- "budget.request_resource_duplicate",
713
- issues,
714
- );
715
-
716
- if (capability.effects?.includes("spend") && !invocation.budget_request?.length) {
717
- issues.push(
718
- issue(
719
- "budget.request_missing",
720
- "/budget_request",
721
- "A spending capability requires a non-empty budget request.",
722
- ),
723
- );
724
- }
725
-
726
- for (const requested of invocation.budget_request ?? []) {
727
- const limit = (policy.budgets ?? []).find(
728
- (candidate) => candidate.resource === requested.resource,
729
- );
730
- if (!limit || limit.unit !== requested.unit) {
731
- issues.push(
732
- issue(
733
- "budget.resource_denied",
734
- "/budget_request",
735
- "The requested budget resource is not allowed by the selected policy.",
736
- ),
737
- );
738
- continue;
739
- }
740
- if (
741
- limit.max_per_invocation !== undefined &&
742
- requested.quantity > limit.max_per_invocation
743
- ) {
744
- issues.push(
745
- issue(
746
- "budget.invocation_limit_exceeded",
747
- "/budget_request",
748
- "The requested quantity exceeds the per-invocation budget.",
749
- ),
750
- );
751
- }
752
- }
753
- }
754
-
755
- function trustedConfirmationContext(invocation, capability, computedDigest) {
756
- const scopeDigest = computeConfirmationScopeDigest({
757
- invocationSemanticDigest: computedDigest,
758
- capabilityRegistryVersion: capability.registry_version,
759
- });
760
- return {
761
- grant_ref: invocation.confirmation_grant_ref,
762
- subject_id: invocation.subject_id,
763
- actor: invocation.actor,
764
- capability_id: invocation.capability_id,
765
- capability_version: invocation.capability_version,
766
- policy_ref: invocation.policy_ref,
767
- account_ref: invocation.account_ref,
768
- target: invocation.target,
769
- semantic_digest: computedDigest,
770
- confirmation_scope_digest: scopeDigest,
771
- capability_registry_version: capability.registry_version,
772
- budget_request_digest: sha256Hex(invocation.budget_request ?? []),
773
- data_manifest_digest: sha256Hex(invocation.data_manifest ?? {}),
774
- invocation_expires_at: invocation.expires_at,
775
- };
776
- }
777
-
778
- function operationSucceeded(result) {
779
- return result === true || (result?.ok === true && result?.valid !== false);
780
- }
781
-
782
- function verificationValid(result) {
783
- return result === true || (result?.valid === true && result?.ok !== false);
784
- }
785
-
786
- function isolatedInvocationHookContext(invocation, capability) {
787
- const invocationSnapshot = structuredClone(invocation);
788
- return {
789
- arguments: invocationSnapshot.arguments,
790
- capability: structuredClone(capability),
791
- invocation: invocationSnapshot,
792
- };
793
- }
794
-
795
- async function validateToolInvocationSemanticsUnchecked({
796
- invocation,
797
- policy,
798
- capability,
799
- now = new Date(),
800
- validateArguments,
801
- inspectIntent,
802
- verifyRightsBasis,
803
- verifyConfirmation,
804
- reserveBudget,
805
- }) {
806
- const issues = [...findUnsafePersistedData(invocation, "$")];
807
- let computedDigest = null;
808
- let confirmationVerification = null;
809
- let budgetReservation = null;
810
- let intentInspection = null;
811
-
812
- if (!isPlainObject(invocation)) {
813
- issues.push(
814
- issue(
815
- "record.invocation_invalid",
816
- "/",
817
- "Tool invocation semantics require a schema-valid object.",
818
- ),
819
- );
820
- return {
821
- ok: false,
822
- issues,
823
- computed_digest: null,
824
- confirmation_verification: null,
825
- intent_inspection: null,
826
- budget_reservation: null,
827
- };
828
- }
829
-
830
- const descriptorResult = validateCapabilityDescriptor(capability);
831
- issues.push(...descriptorResult.issues);
832
- if (!descriptorResult.ok) {
833
- return {
834
- ok: false,
835
- issues,
836
- computed_digest: null,
837
- confirmation_verification: null,
838
- intent_inspection: null,
839
- budget_reservation: null,
840
- };
841
- }
842
-
843
- try {
844
- computedDigest = computeInvocationSemanticDigest(invocation);
845
- if (invocation.semantic_digest !== computedDigest) {
846
- issues.push(
847
- issue(
848
- "digest.mismatch",
849
- "/semantic_digest",
850
- "The invocation semantic digest does not match its canonical payload.",
851
- ),
852
- );
853
- }
854
- } catch {
855
- issues.push(
856
- issue(
857
- "digest.uncomputable",
858
- "/semantic_digest",
859
- "The invocation cannot be represented as canonical JSON.",
860
- ),
861
- );
862
- }
863
-
864
- if (!isPlainObject(policy)) {
865
- issues.push(
866
- issue(
867
- "policy.missing",
868
- "/policy_ref",
869
- "The exact trusted policy revision is required.",
870
- ),
871
- );
872
- } else {
873
- if (
874
- invocation.policy_ref?.policy_id !== policy.policy_id ||
875
- invocation.policy_ref?.policy_version !== policy.policy_version
876
- ) {
877
- issues.push(
878
- issue(
879
- "policy.revision_mismatch",
880
- "/policy_ref",
881
- "The invocation does not bind the supplied policy revision.",
882
- ),
883
- );
884
- }
885
- if (policy.subject_id !== invocation.subject_id) {
886
- issues.push(
887
- issue(
888
- "policy.subject_mismatch",
889
- "/subject_id",
890
- "The selected policy belongs to another subject.",
891
- ),
892
- );
893
- }
894
- if (!policy.enabled || policy.effect !== "allow") {
895
- issues.push(
896
- issue(
897
- "policy.not_allowing",
898
- "/policy_ref",
899
- "The selected policy is disabled or does not allow execution.",
900
- ),
901
- );
902
- }
903
- if (!policy.capability_ids?.includes(invocation.capability_id)) {
904
- issues.push(
905
- issue(
906
- "policy.capability_denied",
907
- "/capability_id",
908
- "The selected policy does not include this exact capability.",
909
- ),
910
- );
911
- }
912
- if (!policy.actor_types?.includes(invocation.actor?.actor_type)) {
913
- issues.push(
914
- issue(
915
- "policy.actor_type_denied",
916
- "/actor",
917
- "The actor type is outside the selected policy.",
918
- ),
919
- );
920
- }
921
- if (!includesCanonical(policy.actor_refs, invocation.actor)) {
922
- issues.push(
923
- issue(
924
- "policy.actor_denied",
925
- "/actor",
926
- "The exact actor is outside the selected policy.",
927
- ),
928
- );
929
- }
930
- if (!policy.surfaces?.includes(invocation.surface)) {
931
- issues.push(
932
- issue(
933
- "policy.surface_denied",
934
- "/surface",
935
- "The invocation surface is outside the selected policy.",
936
- ),
937
- );
938
- }
939
- if (!policy.allowed_execution_modes?.includes(invocation.execution_mode)) {
940
- issues.push(
941
- issue(
942
- "policy.execution_mode_denied",
943
- "/execution_mode",
944
- "The execution mode is outside the selected policy.",
945
- ),
946
- );
947
- }
948
- }
949
-
950
- if (isPlainObject(capability)) {
951
- if (
952
- invocation.capability_id !== capability.capability_id ||
953
- invocation.capability_version !== capability.capability_version
954
- ) {
955
- issues.push(
956
- issue(
957
- "registry.capability_mismatch",
958
- "/capability_id",
959
- "The invocation does not bind the trusted capability revision.",
960
- ),
961
- );
962
- }
963
- if (!canonicalEqual(invocation.tool_ref, capability.tool_ref)) {
964
- issues.push(
965
- issue(
966
- "registry.tool_mismatch",
967
- "/tool_ref",
968
- "The invocation tool does not match the trusted capability descriptor.",
969
- ),
970
- );
971
- }
972
- if (!exactSetEqual(invocation.declared_effects, capability.effects)) {
973
- issues.push(
974
- issue(
975
- "registry.effects_mismatch",
976
- "/declared_effects",
977
- "Declared effects must exactly match the trusted capability descriptor.",
978
- ),
979
- );
980
- }
981
- const trustedRiskFlags = riskDomains.filter(
982
- (domain) => capability.risk_levels?.[domain] !== "none",
983
- );
984
- if (!exactSetEqual(invocation.declared_risk_flags, trustedRiskFlags)) {
985
- issues.push(
986
- issue(
987
- "registry.risks_mismatch",
988
- "/declared_risk_flags",
989
- "Declared risk flags must exactly match the trusted capability descriptor.",
990
- ),
991
- );
992
- }
993
- if (!capability.allowed_execution_modes?.includes(invocation.execution_mode)) {
994
- issues.push(
995
- issue(
996
- "registry.execution_mode_denied",
997
- "/execution_mode",
998
- "The trusted capability does not support this execution mode.",
999
- ),
1000
- );
1001
- }
1002
- }
1003
-
1004
- if (isPlainObject(policy) && isPlainObject(capability)) {
1005
- for (const effect of capability.effects ?? []) {
1006
- if (!policy.allowed_effects?.includes(effect)) {
1007
- issues.push(
1008
- issue(
1009
- "policy.effect_denied",
1010
- "/declared_effects",
1011
- "The trusted capability has an effect outside the selected policy.",
1012
- ),
1013
- );
1014
- }
1015
- }
1016
-
1017
- appendUniqueKeyIssues(
1018
- policy.risk_limits,
1019
- (item) => item.domain,
1020
- "/policy/risk_limits",
1021
- "policy.risk_domain_duplicate",
1022
- issues,
1023
- );
1024
- const riskLimits = new Map(
1025
- (policy.risk_limits ?? []).map((item) => [item.domain, item.max_level]),
1026
- );
1027
- if (!exactSetEqual([...riskLimits.keys()], riskDomains)) {
1028
- issues.push(
1029
- issue(
1030
- "policy.risk_domains_incomplete",
1031
- "/policy/risk_limits",
1032
- "The selected policy must bound every v1 risk domain.",
1033
- ),
1034
- );
1035
- }
1036
- for (const domain of riskDomains) {
1037
- const trustedLevel = capability.risk_levels?.[domain];
1038
- const allowedLevel = riskLimits.get(domain);
1039
- if (
1040
- riskRank.has(trustedLevel) &&
1041
- riskRank.has(allowedLevel) &&
1042
- riskRank.get(trustedLevel) > riskRank.get(allowedLevel)
1043
- ) {
1044
- issues.push(
1045
- issue(
1046
- "policy.risk_limit_exceeded",
1047
- `/policy/risk_limits/${domain}`,
1048
- "The trusted capability risk exceeds the selected policy limit.",
1049
- ),
1050
- );
1051
- }
1052
- }
1053
- }
1054
-
1055
- const needsAccount = (capability?.effects ?? []).some((effect) =>
1056
- accountEffects.has(effect),
1057
- );
1058
- if (needsAccount) {
1059
- if (!invocation.account_ref) {
1060
- issues.push(
1061
- issue(
1062
- "account.missing",
1063
- "/account_ref",
1064
- "This capability requires an explicit account reference.",
1065
- ),
1066
- );
1067
- }
1068
- }
1069
-
1070
- if (
1071
- invocation.account_ref &&
1072
- !includesExternalIdentity(policy?.account_refs, invocation.account_ref)
1073
- ) {
1074
- issues.push(
1075
- issue(
1076
- "account.denied",
1077
- "/account_ref",
1078
- "The account is outside the selected policy.",
1079
- ),
1080
- );
1081
- }
1082
-
1083
- const needsIdempotency = (capability?.effects ?? []).some((effect) =>
1084
- mutatingEffects.has(effect),
1085
- );
1086
- if (needsIdempotency && !invocation.idempotency_key) {
1087
- issues.push(
1088
- issue(
1089
- "idempotency.key_missing",
1090
- "/idempotency_key",
1091
- "Mutating capabilities require an idempotency key.",
1092
- ),
1093
- );
1094
- }
1095
- for (const effect of capability?.effects ?? []) {
1096
- if (
1097
- mutatingEffects.has(effect) &&
1098
- !policy?.idempotency?.required_for?.includes(effect)
1099
- ) {
1100
- issues.push(
1101
- issue(
1102
- "policy.idempotency_effect_missing",
1103
- "/policy/idempotency/required_for",
1104
- "The selected policy does not require idempotency for every trusted mutating effect.",
1105
- ),
1106
- );
1107
- }
1108
- }
1109
-
1110
- if (isPlainObject(policy)) {
1111
- validateChronology(invocation, policy, now, issues);
1112
- validatePrivacy(invocation, policy, issues);
1113
- }
1114
- if (isPlainObject(policy) && isPlainObject(capability)) {
1115
- validateBudgets(invocation, policy, capability, issues);
1116
- }
1117
-
1118
- if (typeof validateArguments !== "function") {
1119
- issues.push(
1120
- issue(
1121
- "arguments.validator_missing",
1122
- "/arguments",
1123
- "A trusted capability-specific argument validator is required.",
1124
- ),
1125
- );
1126
- } else if (issues.length === 0) {
1127
- try {
1128
- const result = await validateArguments(
1129
- isolatedInvocationHookContext(invocation, capability),
1130
- );
1131
- if (!operationSucceeded(result)) {
1132
- issues.push(
1133
- issue(
1134
- "arguments.invalid",
1135
- "/arguments",
1136
- "Capability-specific argument validation failed.",
1137
- ),
1138
- );
1139
- }
1140
- } catch {
1141
- issues.push(
1142
- issue(
1143
- "arguments.validator_error",
1144
- "/arguments",
1145
- "Capability-specific argument validation did not complete safely.",
1146
- ),
1147
- );
1148
- }
1149
- }
1150
-
1151
- if (typeof inspectIntent !== "function") {
1152
- issues.push(
1153
- issue(
1154
- "intent.inspector_missing",
1155
- "/arguments",
1156
- "A trusted capability-specific intent inspector is required.",
1157
- ),
1158
- );
1159
- } else if (issues.length === 0) {
1160
- try {
1161
- intentInspection = await inspectIntent(
1162
- isolatedInvocationHookContext(invocation, capability),
1163
- );
1164
- const inspected = intentInspection?.intent ?? intentInspection;
1165
- const declaredSecurityIntent = {
1166
- account_identity: externalIdentity(invocation.account_ref),
1167
- target: invocation.target ?? null,
1168
- data_manifest: invocation.data_manifest,
1169
- budget_request: invocation.budget_request,
1170
- };
1171
- const inspectedSecurityIntent = {
1172
- account_identity: externalIdentity(inspected?.account_ref),
1173
- target: inspected?.target ?? null,
1174
- data_manifest: inspected?.data_manifest,
1175
- budget_request: inspected?.budget_request,
1176
- };
1177
- if (
1178
- !operationSucceeded(intentInspection) ||
1179
- !canonicalEqual(declaredSecurityIntent, inspectedSecurityIntent)
1180
- ) {
1181
- issues.push(
1182
- issue(
1183
- "intent.mismatch",
1184
- "/arguments",
1185
- "Declared account, target, data movement, or budget does not match trusted intent inspection.",
1186
- ),
1187
- );
1188
- }
1189
- } catch {
1190
- issues.push(
1191
- issue(
1192
- "intent.inspector_error",
1193
- "/arguments",
1194
- "Capability-specific intent inspection did not complete safely.",
1195
- ),
1196
- );
1197
- }
1198
- }
1199
-
1200
- const rightsBasisRequired = policy?.privacy?.rights_basis_required === true;
1201
- if (rightsBasisRequired && typeof verifyRightsBasis !== "function") {
1202
- issues.push(
1203
- issue(
1204
- "rights.verifier_missing",
1205
- "/rights_basis_refs",
1206
- "A trusted rights-basis verifier is required.",
1207
- ),
1208
- );
1209
- } else if (rightsBasisRequired && issues.length === 0) {
1210
- try {
1211
- const verification = await verifyRightsBasis({
1212
- subject_id: invocation.subject_id,
1213
- rights_basis_refs: structuredClone(invocation.rights_basis_refs),
1214
- capability_id: invocation.capability_id,
1215
- data_manifest: structuredClone(invocation.data_manifest),
1216
- });
1217
- if (!verificationValid(verification)) {
1218
- issues.push(
1219
- issue(
1220
- "rights.invalid",
1221
- "/rights_basis_refs",
1222
- "The rights basis could not be independently verified.",
1223
- ),
1224
- );
1225
- }
1226
- } catch {
1227
- issues.push(
1228
- issue(
1229
- "rights.verifier_error",
1230
- "/rights_basis_refs",
1231
- "Rights-basis verification did not complete safely.",
1232
- ),
1233
- );
1234
- }
1235
- }
1236
-
1237
- const confirmationRequired = policy?.confirmation?.mode === "always";
1238
- if (
1239
- confirmationRequired &&
1240
- !uuidPattern.test(invocation.confirmation_grant_ref ?? "")
1241
- ) {
1242
- issues.push(
1243
- issue(
1244
- invocation.confirmation_grant_ref
1245
- ? "confirmation.reference_invalid"
1246
- : "confirmation.reference_missing",
1247
- "/confirmation_grant_ref",
1248
- "An always-confirm policy requires a UUID confirmation grant reference.",
1249
- ),
1250
- );
1251
- }
1252
- if (
1253
- policy?.confirmation?.mode === "never" &&
1254
- invocation.confirmation_grant_ref
1255
- ) {
1256
- issues.push(
1257
- issue(
1258
- "confirmation.reference_unexpected",
1259
- "/confirmation_grant_ref",
1260
- "A never-confirm policy cannot accept a confirmation grant reference.",
1261
- ),
1262
- );
1263
- }
1264
- if (confirmationRequired && typeof verifyConfirmation !== "function") {
1265
- issues.push(
1266
- issue(
1267
- "confirmation.verifier_missing",
1268
- "/confirmation_grant_ref",
1269
- "An independent confirmation verifier is required.",
1270
- ),
1271
- );
1272
- } else if (confirmationRequired && issues.length === 0) {
1273
- try {
1274
- confirmationVerification = await verifyConfirmation(
1275
- structuredClone(
1276
- trustedConfirmationContext(invocation, capability, computedDigest),
1277
- ),
1278
- );
1279
- if (!verificationValid(confirmationVerification)) {
1280
- issues.push(
1281
- issue(
1282
- "confirmation.invalid",
1283
- "/confirmation_grant_ref",
1284
- "The confirmation grant did not match this invocation.",
1285
- ),
1286
- );
1287
- }
1288
- } catch {
1289
- issues.push(
1290
- issue(
1291
- "confirmation.verifier_error",
1292
- "/confirmation_grant_ref",
1293
- "Confirmation verification did not complete safely.",
1294
- ),
1295
- );
1296
- }
1297
- }
1298
-
1299
- const needsBudgetReservation =
1300
- invocation.execution_mode === "execute" &&
1301
- (invocation.budget_request?.length ?? 0) > 0;
1302
- if (needsBudgetReservation && typeof reserveBudget !== "function") {
1303
- issues.push(
1304
- issue(
1305
- "budget.reserver_missing",
1306
- "/budget_request",
1307
- "An atomic budget reservation callback is required before execution.",
1308
- ),
1309
- );
1310
- } else if (needsBudgetReservation && issues.length === 0) {
1311
- try {
1312
- budgetReservation = await reserveBudget({
1313
- subject_id: invocation.subject_id,
1314
- policy_ref: invocation.policy_ref,
1315
- invocation_id: invocation.invocation_id,
1316
- semantic_digest: computedDigest,
1317
- requested: structuredClone(invocation.budget_request),
1318
- });
1319
- if (
1320
- !operationSucceeded(budgetReservation) ||
1321
- !budgetReservation.reservation_id
1322
- ) {
1323
- issues.push(
1324
- issue(
1325
- "budget.reservation_failed",
1326
- "/budget_request",
1327
- "The requested budget was not atomically reserved.",
1328
- ),
1329
- );
1330
- }
1331
- } catch {
1332
- issues.push(
1333
- issue(
1334
- "budget.reserver_error",
1335
- "/budget_request",
1336
- "Budget reservation did not complete safely.",
1337
- ),
1338
- );
1339
- }
1340
- }
1341
-
1342
- return {
1343
- ok: issues.length === 0,
1344
- issues,
1345
- computed_digest: computedDigest,
1346
- confirmation_verification: confirmationVerification,
1347
- intent_inspection: intentInspection,
1348
- budget_reservation: budgetReservation,
1349
- };
1350
- }
1351
-
1352
- export async function validateToolInvocationSemantics(context) {
1353
- try {
1354
- const sources = {
1355
- invocation: context?.invocation,
1356
- policy: context?.policy,
1357
- capability: context?.capability,
1358
- };
1359
- const fingerprints = {
1360
- invocation: canonicalizeJson(sources.invocation),
1361
- policy: canonicalizeJson(sources.policy),
1362
- capability: canonicalizeJson(sources.capability),
1363
- };
1364
- const snapshotContext = {
1365
- ...(context ?? {}),
1366
- invocation: structuredClone(sources.invocation),
1367
- policy: structuredClone(sources.policy),
1368
- capability: structuredClone(sources.capability),
1369
- };
1370
- const result =
1371
- await validateToolInvocationSemanticsUnchecked(snapshotContext);
1372
- const contextMutated =
1373
- canonicalizeJson(sources.invocation) !== fingerprints.invocation ||
1374
- canonicalizeJson(sources.policy) !== fingerprints.policy ||
1375
- canonicalizeJson(sources.capability) !== fingerprints.capability;
1376
-
1377
- if (contextMutated) {
1378
- return {
1379
- ...result,
1380
- ok: false,
1381
- issues: [
1382
- ...result.issues,
1383
- issue(
1384
- "record.context_mutated",
1385
- "/",
1386
- "Invocation, policy, and capability inputs must remain unchanged during validation.",
1387
- ),
1388
- ],
1389
- };
1390
- }
1391
- return result;
1392
- } catch {
1393
- return {
1394
- ok: false,
1395
- issues: [
1396
- issue(
1397
- "record.invocation_invalid",
1398
- "/",
1399
- "Tool invocation semantics require a complete schema-valid context.",
1400
- ),
1401
- ],
1402
- computed_digest: null,
1403
- confirmation_verification: null,
1404
- intent_inspection: null,
1405
- budget_reservation: null,
1406
- };
1407
- }
1408
- }
1409
-
1410
- function expectedPolicyDecision(status) {
1411
- if (status === "denied") return "deny";
1412
- if (status === "awaiting_confirmation") return "require_confirmation";
1413
- if (status === "requires_human") return "escalate";
1414
- if (allowReceiptStatuses.has(status)) return "allow";
1415
- return null;
1416
- }
1417
-
1418
- function budgetMap(items) {
1419
- return new Map(
1420
- (items ?? []).map((item) => [
1421
- item.resource,
1422
- { quantity: item.quantity, unit: item.unit },
1423
- ]),
1424
- );
1425
- }
1426
-
1427
- function budgetArraysEqual(left, right) {
1428
- if (!Array.isArray(left) || !Array.isArray(right)) return false;
1429
- const leftMap = budgetMap(left);
1430
- const rightMap = budgetMap(right);
1431
- if (leftMap.size !== left.length || rightMap.size !== right.length) return false;
1432
- if (leftMap.size !== rightMap.size) return false;
1433
- for (const [resource, amount] of leftMap) {
1434
- const other = rightMap.get(resource);
1435
- if (!other || !canonicalEqual(amount, other)) return false;
1436
- }
1437
- return true;
1438
- }
1439
-
1440
- function validateToolReceiptSemanticsUnchecked({
1441
- receipt,
1442
- invocation,
1443
- capability,
1444
- priorReceiptById,
1445
- }) {
1446
- const issues = [...findUnsafePersistedData(receipt, "$")];
1447
- let computedDigest = null;
1448
-
1449
- if (!isPlainObject(receipt) || !isPlainObject(invocation)) {
1450
- issues.push(
1451
- issue(
1452
- "record.receipt_context_invalid",
1453
- "/",
1454
- "Receipt semantics require schema-valid receipt and invocation objects.",
1455
- ),
1456
- );
1457
- return { ok: false, issues, computed_digest: null };
1458
- }
1459
-
1460
- const descriptorResult = validateCapabilityDescriptor(capability);
1461
- issues.push(...descriptorResult.issues);
1462
- if (!descriptorResult.ok) {
1463
- return { ok: false, issues, computed_digest: null };
1464
- }
1465
- for (const [path, items] of [
1466
- ["/usage", receipt.usage],
1467
- ["/budget_reservation/reserved", receipt.budget_reservation?.reserved],
1468
- ["/budget_reservation/actual", receipt.budget_reservation?.actual],
1469
- ]) {
1470
- appendUniqueKeyIssues(
1471
- items,
1472
- (item) => item.resource,
1473
- path,
1474
- "receipt.budget_resource_duplicate",
1475
- issues,
1476
- );
1477
- }
1478
-
1479
- try {
1480
- computedDigest = computeInvocationSemanticDigest(invocation);
1481
- } catch {
1482
- issues.push(
1483
- issue(
1484
- "digest.uncomputable",
1485
- "/invocation_semantic_digest",
1486
- "The bound invocation cannot be represented as canonical JSON.",
1487
- ),
1488
- );
1489
- }
1490
-
1491
- const exactBindings = [
1492
- ["invocation_id", invocation.invocation_id],
1493
- ["subject_id", invocation.subject_id],
1494
- ["capability_id", invocation.capability_id],
1495
- ["capability_version", invocation.capability_version],
1496
- ];
1497
- for (const [field, expected] of exactBindings) {
1498
- if (receipt[field] !== expected) {
1499
- issues.push(
1500
- issue(
1501
- "receipt.binding_mismatch",
1502
- `/${field}`,
1503
- "The receipt does not belong to the supplied invocation.",
1504
- ),
1505
- );
1506
- }
1507
- }
1508
-
1509
- if (
1510
- receipt.invocation_semantic_digest !== computedDigest ||
1511
- invocation.semantic_digest !== computedDigest
1512
- ) {
1513
- issues.push(
1514
- issue(
1515
- "receipt.digest_mismatch",
1516
- "/invocation_semantic_digest",
1517
- "The receipt and invocation do not bind the same canonical intent.",
1518
- ),
1519
- );
1520
- }
1521
-
1522
- if (
1523
- receipt.policy_decision?.policy_ref &&
1524
- !canonicalEqual(receipt.policy_decision.policy_ref, invocation.policy_ref)
1525
- ) {
1526
- issues.push(
1527
- issue(
1528
- "receipt.policy_revision_mismatch",
1529
- "/policy_decision/policy_ref",
1530
- "The receipt cites a different policy revision.",
1531
- ),
1532
- );
1533
- }
1534
- if (
1535
- receipt.policy_decision?.capability_registry_version !==
1536
- capability.registry_version
1537
- ) {
1538
- issues.push(
1539
- issue(
1540
- "receipt.registry_version_mismatch",
1541
- "/policy_decision/capability_registry_version",
1542
- "The receipt cites a different capability registry snapshot.",
1543
- ),
1544
- );
1545
- }
1546
-
1547
- const expectedDecision = expectedPolicyDecision(receipt.status);
1548
- if (
1549
- expectedDecision === null ||
1550
- receipt.policy_decision?.decision !== expectedDecision
1551
- ) {
1552
- issues.push(
1553
- issue(
1554
- "receipt.status_decision_mismatch",
1555
- "/policy_decision/decision",
1556
- "Receipt status and policy decision are inconsistent.",
1557
- ),
1558
- );
1559
- }
1560
-
1561
- for (const effect of receipt.effects ?? []) {
1562
- if (!capability.effects?.includes(effect.effect_type)) {
1563
- issues.push(
1564
- issue(
1565
- "receipt.effect_untrusted",
1566
- "/effects",
1567
- "The receipt reports an effect outside the trusted capability descriptor.",
1568
- ),
1569
- );
1570
- }
1571
- if (
1572
- invocation.execution_mode !== "execute" &&
1573
- effect.status !== "planned"
1574
- ) {
1575
- issues.push(
1576
- issue(
1577
- "receipt.non_execute_effect",
1578
- "/effects",
1579
- "Plan and dry-run invocations cannot report applied or unknown effects.",
1580
- ),
1581
- );
1582
- }
1583
- }
1584
-
1585
- if (
1586
- invocation.execution_mode !== "execute" &&
1587
- ((receipt.usage?.length ?? 0) > 0 ||
1588
- (receipt.privacy_egress?.length ?? 0) > 0)
1589
- ) {
1590
- issues.push(
1591
- issue(
1592
- "receipt.non_execute_activity",
1593
- "/status",
1594
- "Plan and dry-run invocations cannot report usage or external data egress.",
1595
- ),
1596
- );
1597
- }
1598
- const nonExecuteStatuses = new Set([
1599
- "simulated",
1600
- "denied",
1601
- "awaiting_confirmation",
1602
- "failed",
1603
- "timed_out",
1604
- "cancelled",
1605
- "requires_human",
1606
- ]);
1607
- if (
1608
- invocation.execution_mode !== "execute" &&
1609
- !nonExecuteStatuses.has(receipt.status)
1610
- ) {
1611
- issues.push(
1612
- issue(
1613
- "receipt.non_execute_status",
1614
- "/status",
1615
- "Plan and dry-run invocations must use a non-execution receipt status.",
1616
- ),
1617
- );
1618
- }
1619
- if (
1620
- invocation.execution_mode !== "execute" &&
1621
- (receipt.executor_ref ||
1622
- receipt.idempotency ||
1623
- receipt.confirmation?.consumed_at)
1624
- ) {
1625
- issues.push(
1626
- issue(
1627
- "receipt.non_execute_proof",
1628
- "/status",
1629
- "Plan and dry-run receipts cannot contain executor, idempotency, or consumed-confirmation evidence.",
1630
- ),
1631
- );
1632
- }
1633
-
1634
- if (receipt.status === "simulated" && invocation.execution_mode === "execute") {
1635
- issues.push(
1636
- issue(
1637
- "receipt.simulation_mode_mismatch",
1638
- "/status",
1639
- "A simulated receipt must belong to a plan or dry-run invocation.",
1640
- ),
1641
- );
1642
- }
1643
-
1644
- if (
1645
- new Set(["denied", "awaiting_confirmation", "requires_human"]).has(
1646
- receipt.status,
1647
- ) &&
1648
- ((receipt.effects?.length ?? 0) > 0 ||
1649
- (receipt.usage?.length ?? 0) > 0 ||
1650
- (receipt.privacy_egress?.length ?? 0) > 0 ||
1651
- receipt.budget_reservation?.state !== "none")
1652
- ) {
1653
- issues.push(
1654
- issue(
1655
- "receipt.pre_execution_activity",
1656
- "/status",
1657
- "A pre-execution receipt cannot report effects, usage, egress, or a reservation.",
1658
- ),
1659
- );
1660
- }
1661
- if (
1662
- new Set(["denied", "awaiting_confirmation", "requires_human"]).has(
1663
- receipt.status,
1664
- ) &&
1665
- (receipt.started_at ||
1666
- receipt.executor_ref ||
1667
- receipt.confirmation?.consumed_at ||
1668
- (receipt.idempotency &&
1669
- !(
1670
- receipt.status === "denied" &&
1671
- receipt.idempotency.disposition === "conflict"
1672
- )))
1673
- ) {
1674
- issues.push(
1675
- issue(
1676
- "receipt.pre_execution_proof",
1677
- "/status",
1678
- "A pre-execution receipt cannot claim execution, consumed confirmation, or non-conflict idempotency state.",
1679
- ),
1680
- );
1681
- }
1682
-
1683
- const mutating = capability.effects?.some((effect) =>
1684
- mutatingEffects.has(effect),
1685
- );
1686
- if (receipt.idempotency) {
1687
- if (!invocation.idempotency_key) {
1688
- issues.push(
1689
- issue(
1690
- "receipt.unexpected_idempotency",
1691
- "/idempotency",
1692
- "A receipt cannot claim idempotency state when the invocation has no key.",
1693
- ),
1694
- );
1695
- } else {
1696
- const keyDigest = computeIdempotencyKeyDigest(invocation);
1697
- if (
1698
- receipt.idempotency.key_digest !== keyDigest ||
1699
- receipt.idempotency.semantic_digest !== computedDigest
1700
- ) {
1701
- issues.push(
1702
- issue(
1703
- "receipt.idempotency_mismatch",
1704
- "/idempotency",
1705
- "The receipt idempotency record does not bind this invocation.",
1706
- ),
1707
- );
1708
- }
1709
- }
1710
- if (
1711
- receipt.idempotency.disposition === "conflict" &&
1712
- receipt.status !== "denied"
1713
- ) {
1714
- issues.push(
1715
- issue(
1716
- "receipt.idempotency_conflict_executed",
1717
- "/idempotency/disposition",
1718
- "An idempotency conflict must be denied before execution.",
1719
- ),
1720
- );
1721
- }
1722
- if (receipt.idempotency.disposition === "replayed") {
1723
- const prior =
1724
- priorReceiptById instanceof Map
1725
- ? priorReceiptById.get(receipt.deduplicated_from_receipt_id)
1726
- : priorReceiptById?.[receipt.deduplicated_from_receipt_id];
1727
- const priorRecordedAt = parseTimestamp(prior?.recorded_at);
1728
- const replayRecordedAt = parseTimestamp(receipt.recorded_at);
1729
- if (
1730
- !isPlainObject(prior) ||
1731
- prior.receipt_id !== receipt.deduplicated_from_receipt_id ||
1732
- prior.receipt_id === receipt.receipt_id ||
1733
- prior.subject_id !== receipt.subject_id ||
1734
- prior.capability_id !== receipt.capability_id ||
1735
- prior.capability_version !== receipt.capability_version ||
1736
- prior.invocation_semantic_digest !== computedDigest ||
1737
- prior.idempotency?.key_digest !== receipt.idempotency.key_digest ||
1738
- prior.idempotency?.semantic_digest !== computedDigest ||
1739
- !Number.isInteger(prior.attempt) ||
1740
- prior.attempt >= receipt.attempt ||
1741
- priorRecordedAt === null ||
1742
- replayRecordedAt === null ||
1743
- priorRecordedAt >= replayRecordedAt
1744
- ) {
1745
- issues.push(
1746
- issue(
1747
- "receipt.replay_source_invalid",
1748
- "/deduplicated_from_receipt_id",
1749
- "A replay must resolve to a prior receipt for the same subject and intent.",
1750
- ),
1751
- );
1752
- }
1753
- if (
1754
- (receipt.usage?.length ?? 0) > 0 ||
1755
- (receipt.privacy_egress?.length ?? 0) > 0
1756
- ) {
1757
- issues.push(
1758
- issue(
1759
- "receipt.replay_new_activity",
1760
- "/idempotency/disposition",
1761
- "A replay cannot report new usage or data egress.",
1762
- ),
1763
- );
1764
- }
1765
- }
1766
- }
1767
- if (
1768
- mutating &&
1769
- invocation.execution_mode === "execute" &&
1770
- executedReceiptStatuses.has(receipt.status)
1771
- ) {
1772
- if (!receipt.idempotency) {
1773
- issues.push(
1774
- issue(
1775
- "receipt.idempotency_missing",
1776
- "/idempotency",
1777
- "An executed mutating attempt requires an idempotency record.",
1778
- ),
1779
- );
1780
- }
1781
- }
1782
-
1783
- const confirmationRequired =
1784
- Boolean(invocation.confirmation_grant_ref) ||
1785
- capability.effects?.some((effect) => accountEffects.has(effect));
1786
- if (
1787
- confirmationRequired &&
1788
- invocation.execution_mode === "execute" &&
1789
- executedReceiptStatuses.has(receipt.status)
1790
- ) {
1791
- const scopeDigest = computeConfirmationScopeDigest({
1792
- invocationSemanticDigest: computedDigest,
1793
- capabilityRegistryVersion: capability.registry_version,
1794
- });
1795
- if (
1796
- receipt.confirmation?.grant_id !== invocation.confirmation_grant_ref ||
1797
- receipt.confirmation?.scope_digest !== scopeDigest
1798
- ) {
1799
- issues.push(
1800
- issue(
1801
- "receipt.confirmation_mismatch",
1802
- "/confirmation",
1803
- "The receipt confirmation does not bind this invocation.",
1804
- ),
1805
- );
1806
- }
1807
- const verifiedAt = parseTimestamp(receipt.confirmation?.verified_at);
1808
- const consumedAt = parseTimestamp(receipt.confirmation?.consumed_at);
1809
- const executionStartedAt = parseTimestamp(receipt.started_at);
1810
- const invocationRequestedAt = parseTimestamp(invocation.requested_at);
1811
- const invocationExpiresAt = parseTimestamp(invocation.expires_at);
1812
- if (
1813
- verifiedAt === null ||
1814
- consumedAt === null ||
1815
- verifiedAt > consumedAt ||
1816
- (executionStartedAt !== null && consumedAt > executionStartedAt) ||
1817
- (invocationRequestedAt !== null && verifiedAt < invocationRequestedAt) ||
1818
- (invocationExpiresAt !== null && consumedAt >= invocationExpiresAt)
1819
- ) {
1820
- issues.push(
1821
- issue(
1822
- "receipt.confirmation_time_order",
1823
- "/confirmation/consumed_at",
1824
- "Executed account effects require confirmation consumption before execution starts.",
1825
- ),
1826
- );
1827
- }
1828
- }
1829
-
1830
- const budgetRequested = (invocation.budget_request?.length ?? 0) > 0;
1831
- if (!budgetRequested) {
1832
- if (
1833
- receipt.budget_reservation?.state !== "none" ||
1834
- (receipt.usage?.length ?? 0) > 0
1835
- ) {
1836
- issues.push(
1837
- issue(
1838
- "receipt.unrequested_budget",
1839
- "/budget_reservation",
1840
- "The receipt reports usage or a reservation without an approved request.",
1841
- ),
1842
- );
1843
- }
1844
- } else if (
1845
- invocation.execution_mode === "execute" &&
1846
- executedReceiptStatuses.has(receipt.status)
1847
- ) {
1848
- if (receipt.budget_reservation?.state === "none") {
1849
- issues.push(
1850
- issue(
1851
- "receipt.budget_reservation_missing",
1852
- "/budget_reservation",
1853
- "An executed budgeted attempt must retain its reservation record.",
1854
- ),
1855
- );
1856
- }
1857
- if (
1858
- !budgetArraysEqual(
1859
- receipt.budget_reservation?.reserved,
1860
- invocation.budget_request,
1861
- )
1862
- ) {
1863
- issues.push(
1864
- issue(
1865
- "receipt.budget_reservation_mismatch",
1866
- "/budget_reservation/reserved",
1867
- "The receipt reservation does not match the invocation request.",
1868
- ),
1869
- );
1870
- }
1871
- const reservationRecordedAt = parseTimestamp(
1872
- receipt.budget_reservation?.recorded_at,
1873
- );
1874
- const executionStartedAt = parseTimestamp(receipt.started_at);
1875
- if (
1876
- reservationRecordedAt === null ||
1877
- executionStartedAt === null ||
1878
- reservationRecordedAt > executionStartedAt
1879
- ) {
1880
- issues.push(
1881
- issue(
1882
- "receipt.budget_reserved_after_start",
1883
- "/budget_reservation/recorded_at",
1884
- "A budget reservation must be recorded before execution starts.",
1885
- ),
1886
- );
1887
- }
1888
- if (
1889
- new Set(["succeeded", "failed", "timed_out", "partial"]).has(
1890
- receipt.status,
1891
- ) &&
1892
- !new Set(["reconciled", "released"]).has(
1893
- receipt.budget_reservation?.state,
1894
- )
1895
- ) {
1896
- issues.push(
1897
- issue(
1898
- "receipt.budget_terminal_state",
1899
- "/budget_reservation/state",
1900
- "A known terminal budgeted attempt must reconcile or release its reservation.",
1901
- ),
1902
- );
1903
- }
1904
- }
1905
-
1906
- if (
1907
- ["reconciled", "released"].includes(receipt.budget_reservation?.state) &&
1908
- !budgetArraysEqual(receipt.budget_reservation.actual, receipt.usage)
1909
- ) {
1910
- issues.push(
1911
- issue(
1912
- "receipt.budget_reconciliation_mismatch",
1913
- "/budget_reservation/actual",
1914
- "Reconciled budget actuals must match recorded usage.",
1915
- ),
1916
- );
1917
- }
1918
- if (["reconciled", "released"].includes(receipt.budget_reservation?.state)) {
1919
- const reconciledAt = parseTimestamp(
1920
- receipt.budget_reservation?.reconciled_at,
1921
- );
1922
- const executionStartedAt = parseTimestamp(receipt.started_at);
1923
- const receiptRecordedAt = parseTimestamp(receipt.recorded_at);
1924
- if (
1925
- reconciledAt === null ||
1926
- (executionStartedAt !== null && reconciledAt < executionStartedAt) ||
1927
- (receiptRecordedAt !== null && reconciledAt > receiptRecordedAt)
1928
- ) {
1929
- issues.push(
1930
- issue(
1931
- "receipt.budget_reconciliation_time",
1932
- "/budget_reservation/reconciled_at",
1933
- "Budget reconciliation must occur after execution starts and before the receipt is recorded.",
1934
- ),
1935
- );
1936
- }
1937
- }
1938
-
1939
- if (receipt.status === "unknown") {
1940
- if (receipt.error?.retryable !== false) {
1941
- issues.push(
1942
- issue(
1943
- "receipt.unknown_retryable",
1944
- "/error/retryable",
1945
- "An unknown external outcome cannot be automatically retried.",
1946
- ),
1947
- );
1948
- }
1949
- if (!new Set(["reconcile", "human_handoff"]).has(receipt.recovery?.action)) {
1950
- issues.push(
1951
- issue(
1952
- "receipt.unknown_recovery",
1953
- "/recovery/action",
1954
- "An unknown outcome requires reconciliation or human handoff.",
1955
- ),
1956
- );
1957
- }
1958
- if (budgetRequested && receipt.budget_reservation?.state !== "held") {
1959
- issues.push(
1960
- issue(
1961
- "receipt.unknown_budget_released",
1962
- "/budget_reservation/state",
1963
- "Budget must remain held until an unknown outcome is reconciled.",
1964
- ),
1965
- );
1966
- }
1967
- }
1968
-
1969
- if (
1970
- receipt.status === "succeeded" &&
1971
- (receipt.error ||
1972
- (receipt.effects ?? []).some((effect) => effect.status === "unknown"))
1973
- ) {
1974
- issues.push(
1975
- issue(
1976
- "receipt.success_contradiction",
1977
- "/status",
1978
- "A successful receipt cannot contain an error or unknown effect.",
1979
- ),
1980
- );
1981
- }
1982
- if (
1983
- receipt.status === "succeeded" &&
1984
- (receipt.effects ?? []).some((effect) => effect.status === "planned")
1985
- ) {
1986
- issues.push(
1987
- issue(
1988
- "receipt.success_effect_incomplete",
1989
- "/effects",
1990
- "A successful receipt cannot leave an effect in planned state.",
1991
- ),
1992
- );
1993
- }
1994
-
1995
- if (
1996
- receipt.status === "accepted" &&
1997
- (receipt.effects ?? []).some((effect) => effect.status !== "planned")
1998
- ) {
1999
- issues.push(
2000
- issue(
2001
- "receipt.accepted_effect_completed",
2002
- "/effects",
2003
- "An accepted request cannot claim that an effect already completed.",
2004
- ),
2005
- );
2006
- }
2007
-
2008
- if (
2009
- receipt.status === "timed_out" &&
2010
- (receipt.effects ?? []).some((effect) => effect.status === "unknown")
2011
- ) {
2012
- issues.push(
2013
- issue(
2014
- "receipt.timeout_unknown_effect",
2015
- "/status",
2016
- "A timeout with an uncertain external effect must use unknown status.",
2017
- ),
2018
- );
2019
- }
2020
-
2021
- if (
2022
- receipt.status !== "unknown" &&
2023
- (receipt.effects ?? []).some((effect) => effect.status === "unknown")
2024
- ) {
2025
- issues.push(
2026
- issue(
2027
- "receipt.unknown_effect_status",
2028
- "/status",
2029
- "Any uncertain external effect requires unknown receipt status.",
2030
- ),
2031
- );
2032
- }
2033
-
2034
- if (
2035
- receipt.status === "cancelled" &&
2036
- (receipt.effects ?? []).some((effect) =>
2037
- new Set(["applied", "unknown"]).has(effect.status),
2038
- )
2039
- ) {
2040
- issues.push(
2041
- issue(
2042
- "receipt.cancelled_effect",
2043
- "/effects",
2044
- "A cancelled receipt cannot contain applied or unknown effects.",
2045
- ),
2046
- );
2047
- }
2048
- if (
2049
- receipt.status === "cancelled" &&
2050
- budgetRequested &&
2051
- receipt.budget_reservation?.state !== "released"
2052
- ) {
2053
- issues.push(
2054
- issue(
2055
- "receipt.cancelled_budget_not_released",
2056
- "/budget_reservation/state",
2057
- "A cancelled budgeted attempt must release its reservation.",
2058
- ),
2059
- );
2060
- }
2061
-
2062
- if (
2063
- receipt.status === "requires_human" &&
2064
- receipt.recovery?.action !== "human_handoff"
2065
- ) {
2066
- issues.push(
2067
- issue(
2068
- "receipt.human_recovery_missing",
2069
- "/recovery/action",
2070
- "A human escalation receipt requires human_handoff recovery.",
2071
- ),
2072
- );
2073
- }
2074
-
2075
- const startedAt = parseTimestamp(receipt.started_at);
2076
- const finishedAt = parseTimestamp(receipt.finished_at);
2077
- const recordedAt = parseTimestamp(receipt.recorded_at);
2078
- const invocationRequestedAt = parseTimestamp(invocation.requested_at);
2079
- const invocationExpiresAt = parseTimestamp(invocation.expires_at);
2080
- if (
2081
- (startedAt !== null && finishedAt !== null && startedAt > finishedAt) ||
2082
- (finishedAt !== null && recordedAt !== null && finishedAt > recordedAt)
2083
- ) {
2084
- issues.push(
2085
- issue(
2086
- "receipt.time_order",
2087
- "/finished_at",
2088
- "Receipt timestamps are not chronological.",
2089
- ),
2090
- );
2091
- }
2092
- if (
2093
- executedReceiptStatuses.has(receipt.status) &&
2094
- (startedAt === null ||
2095
- invocationRequestedAt === null ||
2096
- invocationExpiresAt === null ||
2097
- startedAt < invocationRequestedAt ||
2098
- startedAt >= invocationExpiresAt)
2099
- ) {
2100
- issues.push(
2101
- issue(
2102
- "receipt.execution_outside_invocation_window",
2103
- "/started_at",
2104
- "Execution must start within the bound invocation validity window.",
2105
- ),
2106
- );
2107
- }
2108
-
2109
- const plannedDestinations = invocation.data_manifest?.planned_destinations ?? [];
2110
- const plannedDataClasses = new Set(
2111
- invocation.data_manifest?.data_classes ?? [],
2112
- );
2113
- for (const egress of receipt.privacy_egress ?? []) {
2114
- if (!plannedDataClasses.has(egress.data_class)) {
2115
- issues.push(
2116
- issue(
2117
- "receipt.egress_data_unplanned",
2118
- "/privacy_egress",
2119
- "The receipt reports an unplanned data class.",
2120
- ),
2121
- );
2122
- }
2123
- const planned = plannedDestinations.some(
2124
- (destination) =>
2125
- destination.destination_type === egress.destination?.destination_type &&
2126
- destination.system === egress.destination?.system &&
2127
- destination.purpose_id === egress.destination?.purpose_id,
2128
- );
2129
- if (!planned) {
2130
- issues.push(
2131
- issue(
2132
- "receipt.egress_destination_unplanned",
2133
- "/privacy_egress",
2134
- "The receipt reports an unplanned destination.",
2135
- ),
2136
- );
2137
- }
2138
- }
2139
-
2140
- return { ok: issues.length === 0, issues, computed_digest: computedDigest };
2141
- }
2142
-
2143
- export function validateToolReceiptSemantics(context) {
2144
- try {
2145
- return validateToolReceiptSemanticsUnchecked(context ?? {});
2146
- } catch {
2147
- return {
2148
- ok: false,
2149
- issues: [
2150
- issue(
2151
- "record.receipt_context_invalid",
2152
- "/",
2153
- "Receipt semantics require a complete schema-valid receipt and invocation context.",
2154
- ),
2155
- ],
2156
- computed_digest: null,
2157
- };
2158
- }
2159
- }
2160
-
2161
257
  export function validateTrackRefSemantics(trackRef) {
2162
258
  const issues = [];
2163
259
  const createdAt = parseTimestamp(trackRef.created_at);