owlrunkit 0.13.0

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 (128) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +674 -0
  3. package/NOTICE.md +137 -0
  4. package/README.md +44 -0
  5. package/SOURCE.md +10 -0
  6. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
  7. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
  8. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
  9. package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
  10. package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
  11. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
  12. package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
  13. package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
  14. package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
  15. package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
  16. package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
  17. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
  18. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
  19. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
  20. package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
  21. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
  22. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
  23. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
  24. package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
  25. package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
  26. package/package.json +24 -0
  27. package/packages/attest/LICENSE +674 -0
  28. package/packages/attest/README.md +63 -0
  29. package/packages/attest/cli/owlcoda-attest.mjs +155 -0
  30. package/packages/attest/consumer-surface-v1.json +255 -0
  31. package/packages/attest/distribution-manifest-v1.json +104 -0
  32. package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
  33. package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
  34. package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
  35. package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
  36. package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
  37. package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
  38. package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
  39. package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
  40. package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
  41. package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
  42. package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
  43. package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
  44. package/packages/attest/fixtures/index.json +90 -0
  45. package/packages/attest/sbom.spdx.json +33 -0
  46. package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
  47. package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
  48. package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
  49. package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
  50. package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
  51. package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
  52. package/packages/attest/src/canonical.mjs +12 -0
  53. package/packages/attest/src/formal.mjs +762 -0
  54. package/packages/attest/src/index.mjs +113 -0
  55. package/packages/attest/src/offline-bundle.mjs +108 -0
  56. package/packages/attest/src/quick-receipt-contract.mjs +156 -0
  57. package/packages/attest/src/quick.mjs +274 -0
  58. package/packages/attest/src/reference-contract.mjs +64 -0
  59. package/packages/attest/src/reference.mjs +131 -0
  60. package/packages/attest/src/workspace-snapshot.mjs +171 -0
  61. package/registry-release-policy-v1.json +34 -0
  62. package/sbom.spdx.json +39 -0
  63. package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
  64. package/scripts/runkit-contract/core-contract.mjs +392 -0
  65. package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
  66. package/scripts/runkit-contract/delivery-create.mjs +252 -0
  67. package/scripts/runkit-contract/delivery-selection.mjs +31 -0
  68. package/scripts/runkit-contract/finalize.mjs +790 -0
  69. package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
  70. package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
  71. package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
  72. package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
  73. package/scripts/runkit-contract/offline-store.mjs +248 -0
  74. package/scripts/runkit-contract/profile-impact.mjs +223 -0
  75. package/scripts/runkit-contract/provenance-common.mjs +153 -0
  76. package/scripts/runkit-contract/quick-attest.mjs +4 -0
  77. package/scripts/runkit-contract/quick-canonical.mjs +41 -0
  78. package/scripts/runkit-contract/quick-metrics.mjs +86 -0
  79. package/scripts/runkit-contract/quick-receipt.mjs +139 -0
  80. package/scripts/runkit-contract/quick-verify.mjs +201 -0
  81. package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
  82. package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
  83. package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
  84. package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
  85. package/scripts/runkit-contract/repair-execution.mjs +997 -0
  86. package/scripts/runkit-contract/resource-preflight.mjs +829 -0
  87. package/scripts/runkit-contract/resume-execution.mjs +569 -0
  88. package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
  89. package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
  90. package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
  91. package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
  92. package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
  93. package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
  94. package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
  95. package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
  96. package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
  97. package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
  98. package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
  99. package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
  100. package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
  101. package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
  102. package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
  103. package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
  104. package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
  105. package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
  106. package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
  107. package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
  108. package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
  109. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
  110. package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
  111. package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
  112. package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
  113. package/scripts/runkit-contract/snapshot.mjs +340 -0
  114. package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
  115. package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
  116. package/scripts/runkit-contract/templates/finalize-request.json +18 -0
  117. package/scripts/runkit-contract/templates/goal-contract.json +11 -0
  118. package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
  119. package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
  120. package/scripts/runkit-contract/templates/resume-request.json +10 -0
  121. package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
  122. package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
  123. package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
  124. package/scripts/runkit-contract/templates/worker-lease.json +7 -0
  125. package/scripts/runkit-contract/verification-context.mjs +141 -0
  126. package/scripts/runkit-contract/verification-plan.mjs +608 -0
  127. package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
  128. package/scripts/runkit-contract/visual-smoke.mjs +410 -0
@@ -0,0 +1,566 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { isDirectExecution } from "./core-contract.mjs";
4
+
5
+ import { validateReceiptLineage } from "./receipt-lineage.mjs";
6
+ import { validateReplayableEvidence } from "./acceptance-evidence.mjs";
7
+ import {
8
+ validateVerificationContext,
9
+ verificationContextFingerprint as computeVerificationContextFingerprint,
10
+ } from "./verification-context.mjs";
11
+
12
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/i;
13
+ const DEFERRED_STATUSES = new Set([
14
+ "ready_for_verification",
15
+ "shadow_validated",
16
+ ]);
17
+
18
+ function isRecord(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+
22
+ function issue(code, message, extra = {}) {
23
+ return { code, ...extra, message };
24
+ }
25
+
26
+ function isSha256(value) {
27
+ return typeof value === "string" && SHA256_PATTERN.test(value);
28
+ }
29
+
30
+ function isSortedUniqueStrings(values) {
31
+ return Array.isArray(values)
32
+ && values.every(
33
+ (value) => typeof value === "string"
34
+ && value.trim().length > 0
35
+ && value.trim() === value,
36
+ )
37
+ && new Set(values).size === values.length
38
+ && values.every((value, index) => index === 0 || values[index - 1] < value);
39
+ }
40
+
41
+ function isRisk(value) {
42
+ return isRecord(value)
43
+ && new Set(["lightweight", "standard", "full"]).has(value.riskMode)
44
+ && isSortedUniqueStrings(value.riskCategories)
45
+ && value.riskCategories.every(category => new Set([
46
+ "backtest",
47
+ "funds",
48
+ "migration",
49
+ "production",
50
+ "release",
51
+ ]).has(category));
52
+ }
53
+
54
+ function sameRisk(left, right) {
55
+ return left?.riskMode === right?.riskMode
56
+ && JSON.stringify(left?.riskCategories) === JSON.stringify(right?.riskCategories);
57
+ }
58
+
59
+ function validateRepairControlShape(receipt) {
60
+ const issues = [];
61
+ const hasSupersedes = receipt.supersedesReceiptSha256 !== undefined;
62
+ const hasControl = receipt.repairControl !== undefined;
63
+ if (hasSupersedes && !hasControl) {
64
+ issues.push(issue(
65
+ "repair_control_required",
66
+ "A superseding receipt requires a source-bound repairControl.",
67
+ ));
68
+ return issues;
69
+ }
70
+ if (!hasSupersedes && hasControl) {
71
+ issues.push(issue(
72
+ "repair_control_malformed",
73
+ "repairControl is only valid on a superseding receipt.",
74
+ ));
75
+ return issues;
76
+ }
77
+ if (!hasSupersedes) return issues;
78
+ if (!isSha256(receipt.supersedesReceiptSha256)) {
79
+ issues.push(issue(
80
+ "repair_control_malformed",
81
+ "supersedesReceiptSha256 must be a SHA-256.",
82
+ ));
83
+ }
84
+ if (!isRecord(receipt.repairControl)) {
85
+ issues.push(issue("repair_control_malformed", "repairControl must be an object."));
86
+ return issues;
87
+ }
88
+ const control = receipt.repairControl;
89
+ if (typeof control.repairPlanPath !== "string"
90
+ || control.repairPlanPath.length === 0
91
+ || control.parentBindingMode !== "receipt"
92
+ || !isSha256(control.repairPlanSha256)
93
+ || !isSha256(control.goalContractSha256)
94
+ || !isSha256(control.profilesSha256)
95
+ || !Array.isArray(control.executableBindings)
96
+ || control.executableBindings.length === 0) {
97
+ issues.push(issue(
98
+ "repair_control_malformed",
99
+ "repairControl is missing its plan, goal, profile, or executable binding.",
100
+ ));
101
+ }
102
+ const bindingIds = [];
103
+ for (const binding of control.executableBindings ?? []) {
104
+ if (!isRecord(binding)
105
+ || typeof binding.commandId !== "string"
106
+ || binding.commandId.length === 0
107
+ || typeof binding.executable !== "string"
108
+ || binding.executable.length === 0
109
+ || !isSha256(binding.sha256)) {
110
+ issues.push(issue(
111
+ "repair_control_malformed",
112
+ "Each repair executable binding must identify one command, executable, and SHA-256.",
113
+ ));
114
+ continue;
115
+ }
116
+ bindingIds.push(binding.commandId);
117
+ }
118
+ if (new Set(bindingIds).size !== bindingIds.length) {
119
+ issues.push(issue(
120
+ "repair_control_malformed",
121
+ "repair executable binding commandIds must be unique.",
122
+ ));
123
+ }
124
+ if (!isSha256(receipt.goalContractSha256) || !isRisk(receipt.risk)) {
125
+ issues.push(issue(
126
+ "goal_contract_binding_missing",
127
+ "A superseding receipt requires goalContractSha256 and normalized risk.",
128
+ ));
129
+ } else if (control.goalContractSha256 !== receipt.goalContractSha256
130
+ || !isRisk(control.risk)
131
+ || !sameRisk(control.risk, receipt.risk)) {
132
+ issues.push(issue(
133
+ "repair_risk_binding_mismatch",
134
+ "repairControl goal and risk must exactly match the receipt binding.",
135
+ ));
136
+ }
137
+ return issues;
138
+ }
139
+
140
+ function malformedResult(issues, lineage = null) {
141
+ return {
142
+ valid: false,
143
+ malformed: true,
144
+ accepted: false,
145
+ verificationPassed: false,
146
+ decision: "malformed_input",
147
+ activeReceiptSha256: null,
148
+ lineage,
149
+ issues,
150
+ };
151
+ }
152
+
153
+ function validateGateShapes({ sourceGate, profileImpact }) {
154
+ const issues = [];
155
+ if (!isRecord(sourceGate)) {
156
+ issues.push(issue("source_gate_not_object", "sourceGate must be an object."));
157
+ } else {
158
+ if (typeof sourceGate.status !== "string") {
159
+ issues.push(issue("malformed_source_gate_status", "sourceGate.status must be a string."));
160
+ }
161
+ if (!Number.isInteger(sourceGate.exitCode)) {
162
+ issues.push(issue("malformed_source_gate_exit_code", "sourceGate.exitCode must be an integer."));
163
+ }
164
+ if (!isSha256(sourceGate.declaredFingerprint)) {
165
+ issues.push(issue(
166
+ "malformed_declared_fingerprint",
167
+ "sourceGate.declaredFingerprint must be a 64-character hexadecimal SHA-256.",
168
+ ));
169
+ }
170
+ if (!isSha256(sourceGate.recomputedFingerprint)) {
171
+ issues.push(issue(
172
+ "malformed_recomputed_fingerprint",
173
+ "sourceGate.recomputedFingerprint must be a 64-character hexadecimal SHA-256.",
174
+ ));
175
+ }
176
+ }
177
+
178
+ if (!isRecord(profileImpact)) {
179
+ issues.push(issue("profile_impact_not_object", "profileImpact must be an object."));
180
+ } else {
181
+ if (typeof profileImpact.decision !== "string") {
182
+ issues.push(issue(
183
+ "malformed_profile_decision",
184
+ "profileImpact.decision must be a string.",
185
+ ));
186
+ }
187
+ if (!isSortedUniqueStrings(profileImpact.profileIds)) {
188
+ issues.push(issue(
189
+ "malformed_profile_ids",
190
+ "profileImpact.profileIds must be a lexically sorted array of unique non-empty strings.",
191
+ ));
192
+ }
193
+ }
194
+ return issues;
195
+ }
196
+
197
+ function validateActiveReceiptShape(receipt, contractVersion) {
198
+ const issues = [];
199
+ if (contractVersion === "0.2") {
200
+ if (receipt.schemaVersion !== "OwlCodaRunKitVerificationReceiptV2") {
201
+ issues.push(issue(
202
+ "unsupported_v02_receipt_schema",
203
+ "Contract v0.2 requires OwlCodaRunKitVerificationReceiptV2.",
204
+ ));
205
+ }
206
+ if (typeof receipt.runId !== "string" || receipt.runId.length === 0) {
207
+ issues.push(issue(
208
+ "missing_receipt_run_id",
209
+ "Contract v0.2 receipt requires runId.",
210
+ ));
211
+ }
212
+ if (!isSha256(receipt.verificationContextFingerprint)) {
213
+ issues.push(issue(
214
+ "malformed_verification_context_fingerprint",
215
+ "Contract v0.2 receipt requires verificationContextFingerprint.",
216
+ ));
217
+ }
218
+ }
219
+ if (!isSha256(receipt.sourceFingerprint)) {
220
+ issues.push(issue(
221
+ "malformed_receipt_source_fingerprint",
222
+ "The active receipt sourceFingerprint must be a 64-character hexadecimal SHA-256.",
223
+ ));
224
+ }
225
+ if (!isSortedUniqueStrings(receipt.selectedProfileIds)) {
226
+ issues.push(issue(
227
+ "malformed_selected_profile_ids",
228
+ "The active receipt selectedProfileIds must be a lexically sorted array of unique non-empty strings.",
229
+ ));
230
+ }
231
+ if (!Number.isInteger(receipt.commandRuns) || receipt.commandRuns < 0) {
232
+ issues.push(issue(
233
+ "malformed_command_runs",
234
+ "The active receipt commandRuns must be a non-negative integer.",
235
+ ));
236
+ }
237
+ if (!Array.isArray(receipt.commandReceipts)) {
238
+ issues.push(issue(
239
+ "command_receipts_not_array",
240
+ "The active receipt commandReceipts must be an array.",
241
+ ));
242
+ }
243
+ if ((receipt.goalContractSha256 !== undefined || receipt.risk !== undefined)
244
+ && (!isSha256(receipt.goalContractSha256) || !isRisk(receipt.risk))) {
245
+ issues.push(issue(
246
+ "goal_contract_binding_malformed",
247
+ "Receipt goalContractSha256 and normalized risk must either both be valid or both be absent.",
248
+ ));
249
+ }
250
+ issues.push(...validateRepairControlShape(receipt));
251
+ return issues;
252
+ }
253
+
254
+ function validateCommandReceipt(commandReceipt, index, contractVersion) {
255
+ if (!isRecord(commandReceipt)) {
256
+ return [issue(
257
+ "command_receipt_not_object",
258
+ "Each command receipt must be an object.",
259
+ { index },
260
+ )];
261
+ }
262
+
263
+ const issues = [];
264
+ if (contractVersion === "0.2") {
265
+ const evidenceValidation = validateReplayableEvidence(commandReceipt.evidence);
266
+ if (!evidenceValidation.valid) {
267
+ issues.push(...evidenceValidation.issues.map((message) => issue(
268
+ "non_replayable_command_evidence",
269
+ message,
270
+ { index },
271
+ )));
272
+ }
273
+ } else if (
274
+ typeof commandReceipt.command !== "string"
275
+ || commandReceipt.command.trim().length === 0
276
+ ) {
277
+ issues.push(issue(
278
+ "missing_command",
279
+ "Each command receipt must name the command that ran.",
280
+ { index },
281
+ ));
282
+ }
283
+ if (!Number.isInteger(commandReceipt.exitCode)) {
284
+ issues.push(issue(
285
+ "malformed_command_exit_code",
286
+ "Each command receipt exitCode must be an integer.",
287
+ { index },
288
+ ));
289
+ }
290
+ if (!isSha256(commandReceipt.stdoutSha256)) {
291
+ issues.push(issue(
292
+ "malformed_stdout_sha256",
293
+ "Each command receipt must bind a 64-character stdoutSha256.",
294
+ { index },
295
+ ));
296
+ }
297
+ if (!isSha256(commandReceipt.stderrSha256)) {
298
+ issues.push(issue(
299
+ "malformed_stderr_sha256",
300
+ "Each command receipt must bind a 64-character stderrSha256.",
301
+ { index },
302
+ ));
303
+ }
304
+ return issues;
305
+ }
306
+
307
+ export function validateVerificationReceiptGate({
308
+ contractVersion = "0.1",
309
+ receipts,
310
+ sourceGate,
311
+ profileImpact,
312
+ verificationContext = null,
313
+ } = {}) {
314
+ if (!new Set(["0.1", "0.2"]).has(contractVersion)) {
315
+ return malformedResult([
316
+ issue("unsupported_contract_version", "contractVersion must be 0.1 or 0.2."),
317
+ ]);
318
+ }
319
+ if (
320
+ Array.isArray(receipts)
321
+ && receipts.some((entry) => {
322
+ const receipt = isRecord(entry?.receipt) ? entry.receipt : entry;
323
+ return isRecord(receipt) && receipt.schemaVersion === "OwlCodaQuickVerificationReceiptV1";
324
+ })
325
+ ) {
326
+ return {
327
+ contractVersion,
328
+ valid: false,
329
+ malformed: false,
330
+ accepted: false,
331
+ verificationPassed: false,
332
+ decision: "rejected",
333
+ activeReceiptSha256: null,
334
+ lineage: null,
335
+ issues: [issue(
336
+ "quick_receipt_not_formal",
337
+ "A Quick Receipt cannot satisfy a Formal Delivery verification gate.",
338
+ )],
339
+ };
340
+ }
341
+ const lineage = validateReceiptLineage(receipts);
342
+ if (lineage.malformed) {
343
+ return malformedResult(lineage.issues, lineage);
344
+ }
345
+
346
+ const shapeIssues = validateGateShapes({ sourceGate, profileImpact });
347
+ if (shapeIssues.length > 0) return malformedResult(shapeIssues, lineage);
348
+
349
+ const issues = [];
350
+ if (!lineage.valid || !lineage.active) {
351
+ issues.push(issue(
352
+ "invalid_receipt_lineage",
353
+ "Verification acceptance requires one valid append-only receipt lineage.",
354
+ ));
355
+ }
356
+ if (sourceGate.status !== "valid" || sourceGate.exitCode !== 0) {
357
+ issues.push(issue(
358
+ "source_gate_not_valid",
359
+ "Verification acceptance requires a current valid source gate with exit code 0.",
360
+ ));
361
+ }
362
+ if (
363
+ sourceGate.declaredFingerprint.toLowerCase()
364
+ !== sourceGate.recomputedFingerprint.toLowerCase()
365
+ ) {
366
+ issues.push(issue(
367
+ "source_gate_fingerprint_mismatch",
368
+ "The current source gate declared and recomputed fingerprints must match.",
369
+ ));
370
+ }
371
+ if (profileImpact.decision !== "targeted_profiles") {
372
+ issues.push(issue(
373
+ "profile_impact_not_targeted",
374
+ "Verification acceptance requires a targeted_profiles impact decision.",
375
+ ));
376
+ }
377
+
378
+ let currentVerificationContextFingerprint = null;
379
+ if (contractVersion === "0.2") {
380
+ if (!isRecord(verificationContext)) {
381
+ issues.push(issue(
382
+ "verification_context_required",
383
+ "Contract v0.2 acceptance requires a recomputable VerificationContextV1.",
384
+ ));
385
+ } else {
386
+ const contextValidation = validateVerificationContext(verificationContext);
387
+ if (!contextValidation.valid) {
388
+ issues.push(...contextValidation.issues.map((message) => issue(
389
+ "invalid_verification_context",
390
+ message,
391
+ )));
392
+ } else {
393
+ currentVerificationContextFingerprint = computeVerificationContextFingerprint(
394
+ verificationContext,
395
+ );
396
+ }
397
+ }
398
+ }
399
+
400
+ if (!lineage.active) {
401
+ return {
402
+ valid: false,
403
+ malformed: false,
404
+ accepted: false,
405
+ verificationPassed: false,
406
+ decision: "rejected",
407
+ activeReceiptSha256: null,
408
+ lineage,
409
+ issues,
410
+ };
411
+ }
412
+
413
+ const activeReceipt = lineage.active.receipt;
414
+ const receiptShapeIssues = validateActiveReceiptShape(activeReceipt, contractVersion);
415
+ if (receiptShapeIssues.length > 0) {
416
+ return malformedResult(receiptShapeIssues, lineage);
417
+ }
418
+ const commandReceiptShapeIssues = activeReceipt.commandReceipts.flatMap(
419
+ (commandReceipt, index) => validateCommandReceipt(
420
+ commandReceipt,
421
+ index,
422
+ contractVersion,
423
+ ),
424
+ );
425
+ if (commandReceiptShapeIssues.length > 0) {
426
+ return malformedResult(commandReceiptShapeIssues, lineage);
427
+ }
428
+
429
+ if (
430
+ activeReceipt.sourceFingerprint.toLowerCase()
431
+ !== sourceGate.recomputedFingerprint.toLowerCase()
432
+ ) {
433
+ issues.push(issue(
434
+ "active_receipt_source_fingerprint_mismatch",
435
+ "The active receipt must bind the current valid source fingerprint.",
436
+ ));
437
+ }
438
+ if (
439
+ contractVersion === "0.2"
440
+ && currentVerificationContextFingerprint !== null
441
+ && activeReceipt.verificationContextFingerprint.toLowerCase()
442
+ !== currentVerificationContextFingerprint.toLowerCase()
443
+ ) {
444
+ issues.push(issue(
445
+ "verification_context_fingerprint_mismatch",
446
+ "The active receipt must bind the current verification context fingerprint.",
447
+ ));
448
+ }
449
+ if (
450
+ activeReceipt.selectedProfileIds.length !== profileImpact.profileIds.length
451
+ || activeReceipt.selectedProfileIds.some(
452
+ (profileId, index) => profileId !== profileImpact.profileIds[index],
453
+ )
454
+ ) {
455
+ issues.push(issue(
456
+ "active_receipt_profile_ids_mismatch",
457
+ "The active receipt must bind exactly the selected profile IDs.",
458
+ ));
459
+ }
460
+ if (activeReceipt.commandRuns !== activeReceipt.commandReceipts.length) {
461
+ issues.push(issue(
462
+ "command_run_count_mismatch",
463
+ "commandRuns must equal the number of commandReceipts.",
464
+ ));
465
+ }
466
+
467
+ if (activeReceipt.status === "passed") {
468
+ if (activeReceipt.commandRuns <= 0) {
469
+ issues.push(issue(
470
+ "passed_without_command_run",
471
+ "A passed receipt must bind at least one real command run.",
472
+ ));
473
+ }
474
+ for (const [index, commandReceipt] of activeReceipt.commandReceipts.entries()) {
475
+ if (isRecord(commandReceipt) && commandReceipt.exitCode !== 0) {
476
+ issues.push(issue(
477
+ "nonzero_command_exit",
478
+ "Every command bound by a passed receipt must exit with code 0.",
479
+ { index, exitCode: commandReceipt.exitCode },
480
+ ));
481
+ }
482
+ }
483
+ } else if (DEFERRED_STATUSES.has(activeReceipt.status)) {
484
+ if (activeReceipt.commandRuns !== 0 || activeReceipt.commandReceipts.length !== 0) {
485
+ issues.push(issue(
486
+ "deferred_receipt_has_command_runs",
487
+ "A deferred receipt must not claim command execution.",
488
+ ));
489
+ }
490
+ } else {
491
+ issues.push(issue(
492
+ "unsupported_active_receipt_status",
493
+ "The active receipt status must be passed, shadow_validated, or ready_for_verification.",
494
+ { status: activeReceipt.status },
495
+ ));
496
+ }
497
+
498
+ const verificationPassed = activeReceipt.status === "passed"
499
+ && issues.length === 0;
500
+ const deferred = DEFERRED_STATUSES.has(activeReceipt.status)
501
+ && issues.length === 0;
502
+
503
+ return {
504
+ contractVersion,
505
+ valid: issues.length === 0,
506
+ malformed: false,
507
+ accepted: verificationPassed,
508
+ verificationPassed,
509
+ decision: verificationPassed
510
+ ? "accepted_passed"
511
+ : deferred
512
+ ? activeReceipt.status
513
+ : "rejected",
514
+ activeReceiptSha256: lineage.active.receiptSha256.toLowerCase(),
515
+ sourceFingerprint: sourceGate.recomputedFingerprint.toLowerCase(),
516
+ verificationContextFingerprint: currentVerificationContextFingerprint,
517
+ selectedProfileIds: [...profileImpact.profileIds],
518
+ lineage,
519
+ issues,
520
+ };
521
+ }
522
+
523
+ export async function runCli(args = process.argv.slice(2)) {
524
+ if (args.length !== 1) {
525
+ return {
526
+ exitCode: 3,
527
+ output: malformedResult([
528
+ issue(
529
+ "usage_error",
530
+ "Usage: verification-receipt-gate.mjs <gate-input.json>",
531
+ ),
532
+ ]),
533
+ };
534
+ }
535
+
536
+ let input;
537
+ try {
538
+ input = JSON.parse(await readFile(args[0], "utf8"));
539
+ } catch (error) {
540
+ return {
541
+ exitCode: 3,
542
+ output: malformedResult([issue("input_error", error.message)]),
543
+ };
544
+ }
545
+
546
+ if (!isRecord(input)) {
547
+ return {
548
+ exitCode: 3,
549
+ output: malformedResult([
550
+ issue("gate_input_not_object", "Gate input must be an object."),
551
+ ]),
552
+ };
553
+ }
554
+
555
+ const output = validateVerificationReceiptGate(input);
556
+ return {
557
+ exitCode: output.malformed ? 3 : output.accepted ? 0 : 2,
558
+ output,
559
+ };
560
+ }
561
+
562
+ if (isDirectExecution(import.meta.url)) {
563
+ const { exitCode, output } = await runCli();
564
+ process.stdout.write(`${JSON.stringify(output)}\n`);
565
+ process.exitCode = exitCode;
566
+ }