@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.
- package/LICENSE +202 -0
- package/README.md +156 -68
- package/contracts/v1/definitions.schema.json +1 -169
- package/package.json +7 -23
- package/scripts/contract-lib.mjs +0 -7
- package/scripts/contract-semantics.mjs +0 -1904
- package/scripts/moondog.mjs +285 -74
- package/src/core/apple-library-source-status.mjs +0 -16
- package/src/core/apple-projection-domain-services.mjs +253 -53
- package/src/core/application-update.mjs +320 -0
- package/src/core/capability-catalog.mjs +290 -55
- package/src/core/listener-avoids.mjs +101 -0
- package/src/core/listening-now.mjs +108 -0
- package/src/core/listening-profile-domain-services.mjs +189 -41
- package/src/core/listening-upkeep.mjs +31 -0
- package/src/core/moondog-application.mjs +2115 -107
- package/src/core/playback-preferences.mjs +97 -0
- package/src/core/spotify-listening-intent.mjs +124 -0
- package/src/core/synthetic-domain-services.mjs +24 -3
- package/src/core/track-title.mjs +107 -0
- package/src/demo/moondog-demo.mjs +4 -22
- package/src/i18n/index.mjs +285 -0
- package/src/i18n/messages/de.mjs +135 -0
- package/src/i18n/messages/en.mjs +136 -0
- package/src/i18n/messages/es.mjs +135 -0
- package/src/i18n/messages/fr.mjs +135 -0
- package/src/i18n/messages/it.mjs +135 -0
- package/src/i18n/messages/ja.mjs +135 -0
- package/src/i18n/messages/ko.mjs +135 -0
- package/src/i18n/messages/pt.mjs +135 -0
- package/src/i18n/messages/zh-Hant.mjs +135 -0
- package/src/i18n/messages/zh.mjs +135 -0
- package/src/i18n/preferences.mjs +34 -0
- package/src/i18n/screen/es.mjs +967 -0
- package/src/i18n/screen/ja.mjs +967 -0
- package/src/i18n/screen/pt.mjs +967 -0
- package/src/i18n/screen/zh.mjs +967 -0
- package/src/importers/apple-music-library/index.mjs +0 -16
- package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
- package/src/integrations/apple-music/catalog.mjs +166 -14
- package/src/integrations/apple-music/music-app.mjs +232 -0
- package/src/integrations/apple-music/storefront.mjs +28 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
- package/src/integrations/lastfm/history.mjs +221 -0
- package/src/integrations/lastfm/settings.mjs +75 -0
- package/src/integrations/netease/ncm-cli-player.mjs +328 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
- package/src/integrations/public-playlists.mjs +2 -1
- package/src/integrations/spotify/account-data-profile.mjs +14 -6
- package/src/integrations/spotify/authentication.mjs +149 -15
- package/src/integrations/spotify/catalog-resolver.mjs +9 -6
- package/src/integrations/spotify/connection.mjs +2 -1
- package/src/integrations/spotify/credential-store.mjs +4 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
- package/src/integrations/spotify/history-archive.mjs +1 -1
- package/src/integrations/spotify/library-snapshot.mjs +227 -0
- package/src/integrations/spotify/recent-activity.mjs +2 -0
- package/src/integrations/spotify/service.mjs +873 -69
- package/src/integrations/spotify/web-api-client.mjs +410 -127
- package/src/integrations/web/brave-web.mjs +45 -0
- package/src/integrations/web/codex-web.mjs +15 -82
- package/src/integrations/web/deepseek-web.mjs +81 -0
- package/src/integrations/web/web-research.mjs +119 -0
- package/src/integrations/web/web-result.mjs +215 -0
- package/src/integrations/youtube-music/takeout.mjs +3 -1
- package/src/memory/local-memory-store.mjs +542 -57
- package/src/profile/history-import.mjs +42 -6
- package/src/profile/listener-corrections.mjs +22 -0
- package/src/profile/listener-profile-build.mjs +461 -0
- package/src/profile/listener-profile-store.mjs +130 -0
- package/src/profile/listener-profile-verification.mjs +68 -0
- package/src/profile/listener-profile-worker.mjs +179 -0
- package/src/profile/listening-history-store.mjs +640 -17
- package/src/profile/listening-profile-projection.mjs +454 -86
- package/src/profile/local-music-data.mjs +1 -1
- package/src/profile/moondog-suggestions.mjs +184 -0
- package/src/profile/music-providers.mjs +11 -7
- package/src/profile/profile-exploration.mjs +92 -0
- package/src/profile/recorded-measures.mjs +24 -0
- package/src/profile/spotify-archive-taste.mjs +0 -4
- package/src/runtime/pi/agent-runtime.mjs +2536 -692
- package/src/runtime/pi/authentication.mjs +34 -1
- package/src/runtime/pi/configured-runtime.mjs +22 -6
- package/src/runtime/pi/conversation-name.mjs +53 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
- package/src/runtime/pi/model-catalog.mjs +28 -5
- package/src/runtime/pi/model-settings.mjs +202 -0
- package/src/runtime/pi/model-transport.mjs +10 -0
- package/src/runtime/pi/persistent-credential-store.mjs +67 -31
- package/src/runtime/pi/profile-build-runtime.mjs +161 -0
- package/src/runtime/pi/public-model-catalog.mjs +288 -0
- package/src/runtime/pi/runtime-settings.mjs +10 -2
- package/src/runtime/pi/skills/listener-profile/SKILL.md +48 -0
- package/src/runtime/pi/tool-trace.mjs +37 -0
- package/src/surfaces/cli/auth-command.mjs +12 -4
- package/src/surfaces/cli/brand-components.mjs +310 -132
- package/src/surfaces/cli/brand-theme.mjs +51 -28
- package/src/surfaces/cli/command-completions.mjs +58 -42
- package/src/surfaces/cli/format-output.mjs +199 -388
- package/src/surfaces/cli/history-import-view.mjs +292 -139
- package/src/surfaces/cli/import-guides.mjs +2 -0
- package/src/surfaces/cli/listener-profile-command.mjs +92 -0
- package/src/surfaces/cli/listening-menu.mjs +7 -4
- package/src/surfaces/cli/profile-command.mjs +30 -21
- package/src/surfaces/cli/seasons-chart.mjs +87 -0
- package/src/surfaces/cli/spotify-command.mjs +54 -6
- package/src/surfaces/cli/taste-profile-model.mjs +78 -79
- package/src/surfaces/cli/taste-profile-view.mjs +33 -26
- package/src/surfaces/cli/terminal-art.mjs +140 -35
- package/src/surfaces/cli/tui.mjs +1066 -357
- package/src/surfaces/cli/update-command.mjs +32 -0
- package/src/surfaces/cli/web-command.mjs +53 -14
- package/src/surfaces/html/tasteprint.mjs +43 -70
- package/contracts/v1/capability-policy.schema.json +0 -572
- package/contracts/v1/profile-evidence.schema.json +0 -237
- package/contracts/v1/tool-invocation.schema.json +0 -318
- package/contracts/v1/tool-receipt.schema.json +0 -1066
- package/scripts/demo-studio.mjs +0 -58
- package/src/core/apple-data-migration.mjs +0 -50
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
- package/src/demo/moondog-tasteprint-demo.mjs +0 -631
- package/src/integrations/spotify/index.mjs +0 -80
- package/src/surfaces/web/showcase.mjs +0 -272
- package/src/surfaces/web/studio-page.mjs +0 -4148
- 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);
|