owlrunkit 0.19.1 → 0.20.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 (42) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +107 -3
  3. package/docs/architecture/OWLCODA_RUNKIT_0191_PROFILE_RECONCILE_PATCH_PRODUCT_DECISION_20260813.md +11 -5
  4. package/docs/architecture/OWLCODA_RUNKIT_0192_FORMAL_USABILITY_PRODUCT_DECISION_20260813.md +141 -0
  5. package/docs/architecture/OWLCODA_RUNKIT_020_TEAM_DELIVERY_PHASE1_PRODUCT_DECISION_20260813.md +296 -0
  6. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +74 -9
  7. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  8. package/package.json +1 -1
  9. package/packages/attest/distribution-manifest-v1.json +10 -0
  10. package/packages/attest/src/quick.mjs +42 -4
  11. package/registry-release-policy-v1.json +5 -5
  12. package/sbom.spdx.json +5 -5
  13. package/scripts/runkit-contract/core-contract.mjs +42 -2
  14. package/scripts/runkit-contract/efficiency-metrics.mjs +9 -7
  15. package/scripts/runkit-contract/formal-preflight.mjs +352 -0
  16. package/scripts/runkit-contract/formal-supplemental-evidence.mjs +713 -0
  17. package/scripts/runkit-contract/formal-workflow.mjs +529 -59
  18. package/scripts/runkit-contract/onboarding-doctor.mjs +32 -13
  19. package/scripts/runkit-contract/project-cli-resolver.mjs +33 -3
  20. package/scripts/runkit-contract/project-external-delivery.mjs +16 -9
  21. package/scripts/runkit-contract/quick-attest.mjs +24 -0
  22. package/scripts/runkit-contract/runkit-cli.mjs +445 -52
  23. package/scripts/runkit-contract/schemas/candidate-transfer-packet-v1.schema.json +98 -0
  24. package/scripts/runkit-contract/schemas/formal-check-v2.schema.json +231 -0
  25. package/scripts/runkit-contract/schemas/formal-envelope-preflight-v1.schema.json +212 -0
  26. package/scripts/runkit-contract/schemas/formal-evidence-summary-v1.schema.json +114 -0
  27. package/scripts/runkit-contract/schemas/formal-supplemental-evidence-v1.schema.json +103 -0
  28. package/scripts/runkit-contract/schemas/team-acceptance-packet-v2.schema.json +109 -0
  29. package/scripts/runkit-contract/schemas/team-delivery-status-v1.schema.json +72 -0
  30. package/scripts/runkit-contract/schemas/team-packet-validation-v1.schema.json +33 -0
  31. package/scripts/runkit-contract/schemas/team-project-event-v5.schema.json +89 -0
  32. package/scripts/runkit-contract/schemas/team-project-status-v3.schema.json +173 -0
  33. package/scripts/runkit-contract/schemas/team-recommendation-v1.schema.json +121 -0
  34. package/scripts/runkit-contract/schemas/team-task-packet-v2.schema.json +228 -0
  35. package/scripts/runkit-contract/snapshot.mjs +125 -5
  36. package/scripts/runkit-contract/source-candidate.mjs +271 -69
  37. package/scripts/runkit-contract/sync-release-identity.mjs +39 -4
  38. package/scripts/runkit-contract/team-delivery.mjs +906 -0
  39. package/scripts/runkit-contract/team-project-successor.mjs +7 -4
  40. package/scripts/runkit-contract/team-project.mjs +440 -28
  41. package/scripts/runkit-contract/usage-mode.mjs +9 -3
  42. package/scripts/runkit-contract/verification-envelope.mjs +425 -37
@@ -138,14 +138,20 @@ function recommendUsageMode(input, { schemaVersion, commandPrefix }) {
138
138
  const forcedFormalReasons = formalReasonCodes(input);
139
139
  const continuityReasons = continuityReasonCodes(input);
140
140
  const requestedMode = input.requestedMode;
141
+ const projectDriverPresent = input.projectDriverPresent === true;
141
142
 
142
143
  let continuity = "none";
143
144
  let assurance = "none";
144
145
  const triggerReasons = [];
145
146
 
146
- if (requestedMode === "managed" || (requestedMode === "auto" && continuityReasons.length > 0)) {
147
+ if (requestedMode === "managed"
148
+ || (requestedMode === "auto" && (continuityReasons.length > 0 || projectDriverPresent))) {
147
149
  continuity = "project_driver";
148
- triggerReasons.push(...(continuityReasons.length > 0 ? continuityReasons : ["managed_mode_requested"]));
150
+ triggerReasons.push(...(continuityReasons.length > 0
151
+ ? continuityReasons
152
+ : projectDriverPresent
153
+ ? ["project_driver_already_initialized"]
154
+ : ["managed_mode_requested"]));
149
155
  }
150
156
 
151
157
  if (forcedFormalReasons.length > 0) {
@@ -167,7 +173,7 @@ function recommendUsageMode(input, { schemaVersion, commandPrefix }) {
167
173
  }
168
174
  } else if (requestedMode === "off") {
169
175
  triggerReasons.push("off_mode_requested");
170
- } else {
176
+ } else if (continuity === "none") {
171
177
  triggerReasons.push("durable_evidence_not_required");
172
178
  }
173
179
 
@@ -122,21 +122,23 @@ function assertNoSymlink(root, absolutePath, label, { allowMissingLeaf = false }
122
122
  }
123
123
  }
124
124
 
125
- function validateCommand(value, label) {
125
+ function validateCommand(value, label, { requireCurrentExecutable = true } = {}) {
126
126
  if (value === null) return null;
127
127
  assertAllowedKeys(value, label, ["executable", "argv", "timeoutMs"]);
128
128
  if (typeof value.executable !== "string" || !path.isAbsolute(value.executable)) {
129
129
  throw new Error(`${label} executable must be an absolute regular file.`);
130
130
  }
131
- let executable;
132
- try {
133
- const stat = lstatSync(value.executable);
134
- executable = realpathSync(value.executable);
135
- if (stat.isSymbolicLink() || !stat.isFile() || executable !== value.executable) {
136
- throw new Error("not-real");
131
+ let executable = value.executable;
132
+ if (requireCurrentExecutable) {
133
+ try {
134
+ const stat = lstatSync(value.executable);
135
+ executable = realpathSync(value.executable);
136
+ if (stat.isSymbolicLink() || !stat.isFile() || executable !== value.executable) {
137
+ throw new Error("not-real");
138
+ }
139
+ } catch {
140
+ throw new Error(`${label} executable must be an absolute regular file without symlinks.`);
137
141
  }
138
- } catch {
139
- throw new Error(`${label} executable must be an absolute regular file without symlinks.`);
140
142
  }
141
143
  const argv = argvList(value.argv, `${label} argv`);
142
144
  if (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs < 100 || value.timeoutMs > 3_600_000) {
@@ -158,7 +160,11 @@ function homeForbiddenPaths() {
158
160
  ];
159
161
  }
160
162
 
161
- export function validateVerificationEnvelopeV1({ workspaceRoot, envelope }) {
163
+ export function validateVerificationEnvelopeV1({
164
+ workspaceRoot,
165
+ envelope,
166
+ requireCurrentEnvironment = true,
167
+ }) {
162
168
  const root = realpathSync(workspaceRoot);
163
169
  assertAllowedKeys(envelope, "Verification Envelope", [
164
170
  "schemaVersion", "envelopeId", "cwd", "lockfiles", "paths",
@@ -170,15 +176,23 @@ export function validateVerificationEnvelopeV1({ workspaceRoot, envelope }) {
170
176
  safeIdentifier(envelope.envelopeId, "envelopeId");
171
177
  const cwd = safeRelativePath(envelope.cwd, "Verification Envelope cwd", { allowDot: true });
172
178
  const cwdPath = path.resolve(root, cwd);
173
- assertNoSymlink(root, cwdPath, "Verification Envelope cwd");
174
- if (!lstatSync(cwdPath).isDirectory()) throw new Error("Verification Envelope cwd must be a directory.");
179
+ if (requireCurrentEnvironment) {
180
+ assertNoSymlink(root, cwdPath, "Verification Envelope cwd");
181
+ if (!lstatSync(cwdPath).isDirectory()) {
182
+ throw new Error("Verification Envelope cwd must be a directory.");
183
+ }
184
+ }
175
185
 
176
186
  const lockfiles = validatePatterns(envelope.lockfiles, "Verification Envelope lockfiles", { allowEmpty: true });
177
187
  for (const lockfile of lockfiles) {
178
188
  if (lockfile.endsWith("/**")) throw new Error("Verification Envelope lockfiles must be exact files.");
179
- const absolute = path.join(root, lockfile);
180
- assertNoSymlink(root, absolute, "Verification Envelope lockfile");
181
- if (!lstatSync(absolute).isFile()) throw new Error(`Verification Envelope lockfile is not a file: ${lockfile}.`);
189
+ if (requireCurrentEnvironment) {
190
+ const absolute = path.join(root, lockfile);
191
+ assertNoSymlink(root, absolute, "Verification Envelope lockfile");
192
+ if (!lstatSync(absolute).isFile()) {
193
+ throw new Error(`Verification Envelope lockfile is not a file: ${lockfile}.`);
194
+ }
195
+ }
182
196
  }
183
197
 
184
198
  assertAllowedKeys(envelope.paths, "Verification Envelope paths", [
@@ -201,18 +215,20 @@ export function validateVerificationEnvelopeV1({ workspaceRoot, envelope }) {
201
215
  }
202
216
  }
203
217
  }
204
- for (const pattern of paths.immutableSource) {
205
- const absolute = path.join(root, patternRoot(pattern));
206
- assertNoSymlink(root, absolute, "immutableSource");
207
- const stat = lstatSync(absolute);
208
- if (pattern.endsWith("/**") ? !stat.isDirectory() : !stat.isFile()) {
209
- throw new Error(`immutableSource type does not match pattern: ${pattern}.`);
218
+ if (requireCurrentEnvironment) {
219
+ for (const pattern of paths.immutableSource) {
220
+ const absolute = path.join(root, patternRoot(pattern));
221
+ assertNoSymlink(root, absolute, "immutableSource");
222
+ const stat = lstatSync(absolute);
223
+ if (pattern.endsWith("/**") ? !stat.isDirectory() : !stat.isFile()) {
224
+ throw new Error(`immutableSource type does not match pattern: ${pattern}.`);
225
+ }
226
+ }
227
+ for (const pattern of [...paths.declaredOutput, ...paths.disposableScratch, ...paths.forbidden]) {
228
+ assertNoSymlink(root, path.join(root, patternRoot(pattern)), "Verification Envelope path", {
229
+ allowMissingLeaf: true,
230
+ });
210
231
  }
211
- }
212
- for (const pattern of [...paths.declaredOutput, ...paths.disposableScratch, ...paths.forbidden]) {
213
- assertNoSymlink(root, path.join(root, patternRoot(pattern)), "Verification Envelope path", {
214
- allowMissingLeaf: true,
215
- });
216
232
  }
217
233
 
218
234
  assertAllowedKeys(envelope.environment, "Verification Envelope environment", ["allowNames", "values"]);
@@ -248,10 +264,12 @@ export function validateVerificationEnvelopeV1({ workspaceRoot, envelope }) {
248
264
  "process allowedExecutables",
249
265
  ).map((executable) => {
250
266
  if (!path.isAbsolute(executable)) throw new Error("Allowed executable must be absolute.");
251
- const stat = lstatSync(executable);
252
- const resolved = realpathSync(executable);
253
- if (stat.isSymbolicLink() || !stat.isFile() || resolved !== executable) {
254
- throw new Error("Allowed executable must be a regular file without symlinks.");
267
+ if (requireCurrentEnvironment) {
268
+ const stat = lstatSync(executable);
269
+ const resolved = realpathSync(executable);
270
+ if (stat.isSymbolicLink() || !stat.isFile() || resolved !== executable) {
271
+ throw new Error("Allowed executable must be a regular file without symlinks.");
272
+ }
255
273
  }
256
274
  return executable;
257
275
  }).sort();
@@ -261,9 +279,15 @@ export function validateVerificationEnvelopeV1({ workspaceRoot, envelope }) {
261
279
 
262
280
  assertAllowedKeys(envelope.phases, "Verification Envelope phases", ["setup", "check", "teardown"]);
263
281
  const phases = {
264
- setup: validateCommand(envelope.phases.setup, "setup phase"),
265
- check: validateCommand(envelope.phases.check, "check phase"),
266
- teardown: validateCommand(envelope.phases.teardown, "teardown phase"),
282
+ setup: validateCommand(envelope.phases.setup, "setup phase", {
283
+ requireCurrentExecutable: requireCurrentEnvironment,
284
+ }),
285
+ check: validateCommand(envelope.phases.check, "check phase", {
286
+ requireCurrentExecutable: requireCurrentEnvironment,
287
+ }),
288
+ teardown: validateCommand(envelope.phases.teardown, "teardown phase", {
289
+ requireCurrentExecutable: requireCurrentEnvironment,
290
+ }),
267
291
  };
268
292
  if (phases.check === null) throw new Error("Verification Envelope check phase is required.");
269
293
  const normalized = {
@@ -289,6 +313,55 @@ export function validateVerificationEnvelopeV1({ workspaceRoot, envelope }) {
289
313
  };
290
314
  }
291
315
 
316
+ export function validateEnvelopeSourceCandidateBoundaryV1({
317
+ envelope,
318
+ sourceManifest,
319
+ }) {
320
+ if (
321
+ sourceManifest?.schemaVersion !== "OwlCodaRunKitDirtySourceManifestV2"
322
+ || !Array.isArray(sourceManifest.entries)
323
+ ) {
324
+ throw new Error("Formal envelope source boundary requires SourceCandidate V2 manifest.");
325
+ }
326
+ const writable = [
327
+ ...envelope.paths.declaredOutput,
328
+ ...envelope.paths.disposableScratch,
329
+ ];
330
+ const uncovered = [];
331
+ const writableCandidatePaths = [];
332
+ for (const entry of sourceManifest.entries) {
333
+ const candidatePaths = [entry.path, entry.previousPath].filter(Boolean);
334
+ for (const candidatePath of candidatePaths) {
335
+ if (writable.some(pattern => patternCovers(pattern, candidatePath))) {
336
+ writableCandidatePaths.push(candidatePath);
337
+ }
338
+ }
339
+ if (
340
+ entry.operation !== "deleted"
341
+ && !envelope.paths.immutableSource.some(
342
+ pattern => patternCovers(pattern, entry.path),
343
+ )
344
+ ) {
345
+ uncovered.push(entry.path);
346
+ }
347
+ }
348
+ if (uncovered.length > 0 || writableCandidatePaths.length > 0) {
349
+ const details = [
350
+ uncovered.length > 0
351
+ ? `immutableSource does not cover: ${[...new Set(uncovered)].sort().join(", ")}`
352
+ : null,
353
+ writableCandidatePaths.length > 0
354
+ ? `writable paths overlap candidate source: ${[...new Set(writableCandidatePaths)].sort().join(", ")}`
355
+ : null,
356
+ ].filter(Boolean).join("; ");
357
+ throw new Error(`Formal envelope does not preserve the SourceCandidate: ${details}.`);
358
+ }
359
+ return {
360
+ status: "source_candidate_boundary_valid",
361
+ authorizationGranted: false,
362
+ };
363
+ }
364
+
292
365
  function commandExists(command) {
293
366
  try {
294
367
  const stat = lstatSync(command);
@@ -388,6 +461,28 @@ function mkdirDeclaredRoots(root, patterns) {
388
461
  }
389
462
  }
390
463
 
464
+ function mkdirBoundTemporaryDirectory(root, envelope) {
465
+ if (!envelope.environment.allowNames.includes("TMPDIR")) return null;
466
+ if (!Object.hasOwn(envelope.environment.values, "TMPDIR")) {
467
+ throw new Error("Verification Envelope TMPDIR must be explicitly bound, not inherited.");
468
+ }
469
+ const absolute = path.resolve(root, envelope.cwd, envelope.environment.values.TMPDIR);
470
+ if (!within(root, absolute)) {
471
+ throw new Error("Verification Envelope TMPDIR must remain inside the workspace.");
472
+ }
473
+ const relative = path.relative(root, absolute).split(path.sep).join("/");
474
+ if (!envelope.paths.disposableScratch.some(pattern => (
475
+ pattern.endsWith("/**") && patternCovers(pattern, relative)
476
+ ))) {
477
+ throw new Error(
478
+ "Verification Envelope TMPDIR must be covered by a disposableScratch/** directory tree.",
479
+ );
480
+ }
481
+ mkdirSync(absolute, { recursive: true });
482
+ assertNoSymlink(root, absolute, "Verification Envelope TMPDIR");
483
+ return absolute;
484
+ }
485
+
391
486
  function walkFiles(root, absolute, relativeBase, files) {
392
487
  const stat = lstatSync(absolute);
393
488
  if (stat.isSymbolicLink()) throw new Error(`Verification Envelope path contains symlink: ${relativeBase}.`);
@@ -480,9 +575,53 @@ function killGroup(groupId) {
480
575
  return { terminated: before, remaining };
481
576
  }
482
577
 
578
+ function phaseToolchainIssue({ root, cwd, phase }) {
579
+ if (
580
+ phase.executable !== realpathSync(process.execPath)
581
+ || typeof phase.argv[0] !== "string"
582
+ || phase.argv[0].startsWith("-")
583
+ || !/\.[cm]?js$/u.test(phase.argv[0])
584
+ ) {
585
+ return null;
586
+ }
587
+ try {
588
+ const absolute = path.resolve(root, cwd, phase.argv[0]);
589
+ if (!within(root, absolute)) throw new Error("outside-workspace");
590
+ assertNoSymlink(root, absolute, "Verification Envelope Node entrypoint");
591
+ const stat = lstatSync(absolute);
592
+ if (!stat.isFile()) throw new Error("not-file");
593
+ return null;
594
+ } catch {
595
+ return "node_entrypoint_unavailable";
596
+ }
597
+ }
598
+
483
599
  function executeMacPhase({ root, cwd, envelope, phase, name, backend }) {
484
600
  const policy = buildMacPolicy({ root, envelope, phase });
485
601
  const policySha256 = sha256(policy);
602
+ const toolchainIssue = phaseToolchainIssue({ root, cwd, phase });
603
+ if (toolchainIssue !== null) {
604
+ const now = new Date().toISOString();
605
+ return Promise.resolve({
606
+ name,
607
+ executable: phase.executable,
608
+ argv: [...phase.argv],
609
+ startedAt: now,
610
+ finishedAt: now,
611
+ timeoutMs: phase.timeoutMs,
612
+ timedOut: false,
613
+ outputOverflow: false,
614
+ exitCode: null,
615
+ signal: null,
616
+ launchError: toolchainIssue,
617
+ stdout: Buffer.alloc(0),
618
+ stderr: Buffer.alloc(0),
619
+ policy,
620
+ policySha256,
621
+ processGroupId: null,
622
+ cleanup: { terminated: [], remaining: [] },
623
+ });
624
+ }
486
625
  return new Promise((resolve) => {
487
626
  const startedAt = new Date().toISOString();
488
627
  const child = spawn(backend.executable, [
@@ -606,6 +745,245 @@ function receiptPathFor(artifactRoot) {
606
745
  return path.join(artifactRoot, "verification-envelope-receipt.json");
607
746
  }
608
747
 
748
+ function exactKeys(value, label, keys) {
749
+ assertAllowedKeys(value, label, keys);
750
+ if (!isRecord(value) || keys.some(key => !Object.hasOwn(value, key))) {
751
+ throw new Error(`${label} is missing required fields.`);
752
+ }
753
+ }
754
+
755
+ function validHash(value) {
756
+ return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
757
+ }
758
+
759
+ function validNonNegativeInteger(value) {
760
+ return Number.isSafeInteger(value) && value >= 0;
761
+ }
762
+
763
+ function validateReceiptFileRow(row, label, { mode = false } = {}) {
764
+ exactKeys(row, label, mode
765
+ ? ["path", "sha256", "bytes", "mode"]
766
+ : ["path", "sha256", "bytes"]);
767
+ if (
768
+ typeof row.path !== "string"
769
+ || row.path.length === 0
770
+ || !validHash(row.sha256)
771
+ || !validNonNegativeInteger(row.bytes)
772
+ || (mode && (!Number.isSafeInteger(row.mode) || row.mode < 0 || row.mode > 0o777))
773
+ ) {
774
+ throw new Error(`${label} is invalid.`);
775
+ }
776
+ }
777
+
778
+ function validateReceiptPhase(phase, envelope) {
779
+ exactKeys(phase, "Verification Envelope receipt phase", [
780
+ "name",
781
+ "executable",
782
+ "argv",
783
+ "startedAt",
784
+ "finishedAt",
785
+ "timeoutMs",
786
+ "timedOut",
787
+ "outputOverflow",
788
+ "exitCode",
789
+ "signal",
790
+ "launchError",
791
+ "policySha256",
792
+ "processGroupId",
793
+ "stdout",
794
+ "stderr",
795
+ "leakedProcessIds",
796
+ ]);
797
+ if (!new Set(["setup", "check", "teardown"]).has(phase.name)) {
798
+ throw new Error("Verification Envelope receipt phase name is invalid.");
799
+ }
800
+ const command = envelope.phases[phase.name];
801
+ if (
802
+ command === null
803
+ || phase.executable !== command.executable
804
+ || canonical(phase.argv) !== canonical(command.argv)
805
+ || phase.timeoutMs !== command.timeoutMs
806
+ || typeof phase.startedAt !== "string"
807
+ || typeof phase.finishedAt !== "string"
808
+ || typeof phase.timedOut !== "boolean"
809
+ || typeof phase.outputOverflow !== "boolean"
810
+ || !(phase.exitCode === null || Number.isInteger(phase.exitCode))
811
+ || !(phase.signal === null || typeof phase.signal === "string")
812
+ || !(phase.launchError === null || typeof phase.launchError === "string")
813
+ || !validHash(phase.policySha256)
814
+ || !(phase.processGroupId === undefined
815
+ || phase.processGroupId === null
816
+ || Number.isSafeInteger(phase.processGroupId))
817
+ || !Array.isArray(phase.leakedProcessIds)
818
+ || phase.leakedProcessIds.some(pid => !Number.isSafeInteger(pid) || pid < 1)
819
+ ) {
820
+ throw new Error(`Verification Envelope receipt phase is invalid: ${phase.name}.`);
821
+ }
822
+ validateReceiptFileRow(phase.stdout, `${phase.name} stdout`);
823
+ validateReceiptFileRow(phase.stderr, `${phase.name} stderr`);
824
+ }
825
+
826
+ function validateReceiptDerivations(receipt, validatedEnvelope) {
827
+ exactKeys(receipt, "Verification Envelope receipt", [
828
+ "schemaVersion",
829
+ "envelopeId",
830
+ "envelope",
831
+ "envelopeSha256",
832
+ "status",
833
+ "formalEligible",
834
+ "commandExecuted",
835
+ "backend",
836
+ "startedAt",
837
+ "finishedAt",
838
+ "phases",
839
+ "source",
840
+ "outputs",
841
+ "evidenceObjects",
842
+ "cleanup",
843
+ "nextAllowedAction",
844
+ "authorizationGranted",
845
+ "receiptSha256",
846
+ ]);
847
+ if (
848
+ receipt.schemaVersion !== RECEIPT_SCHEMA
849
+ || receipt.envelopeId !== validatedEnvelope.envelope.envelopeId
850
+ || receipt.envelopeSha256 !== validatedEnvelope.envelopeSha256
851
+ || receipt.authorizationGranted !== false
852
+ || typeof receipt.startedAt !== "string"
853
+ || typeof receipt.finishedAt !== "string"
854
+ || !Array.isArray(receipt.phases)
855
+ || !Array.isArray(receipt.outputs)
856
+ || !Array.isArray(receipt.evidenceObjects)
857
+ ) {
858
+ throw new Error("Verification Envelope receipt contract is invalid.");
859
+ }
860
+ exactKeys(receipt.cleanup, "Verification Envelope cleanup", [
861
+ "teardownCompleted",
862
+ "terminatedProcessIds",
863
+ "remainingProcessIds",
864
+ "remainingPorts",
865
+ "scratchRemoved",
866
+ "issues",
867
+ ]);
868
+ if (
869
+ typeof receipt.cleanup.teardownCompleted !== "boolean"
870
+ || !Array.isArray(receipt.cleanup.terminatedProcessIds)
871
+ || !Array.isArray(receipt.cleanup.remainingProcessIds)
872
+ || !Array.isArray(receipt.cleanup.remainingPorts)
873
+ || !Array.isArray(receipt.cleanup.scratchRemoved)
874
+ || !Array.isArray(receipt.cleanup.issues)
875
+ ) {
876
+ throw new Error("Verification Envelope cleanup contract is invalid.");
877
+ }
878
+ if (receipt.backend?.available === false) {
879
+ exactKeys(receipt.backend, "Verification Envelope backend", [
880
+ "id", "available", "reason", "capabilities",
881
+ ]);
882
+ exactKeys(receipt.source, "Verification Envelope source", [
883
+ "beforeSha256", "afterSha256",
884
+ ]);
885
+ if (
886
+ receipt.backend.id !== "unavailable"
887
+ || typeof receipt.backend.reason !== "string"
888
+ || receipt.phases.length !== 0
889
+ || receipt.outputs.length !== 0
890
+ || receipt.evidenceObjects.length !== 0
891
+ || receipt.source.beforeSha256 !== receipt.source.afterSha256
892
+ || receipt.status !== "policy_backend_unavailable"
893
+ || receipt.formalEligible !== false
894
+ || receipt.commandExecuted !== false
895
+ || receipt.nextAllowedAction
896
+ !== "install_supported_enforcement_backend_or_use_captured_evidence"
897
+ ) {
898
+ throw new Error("Verification Envelope unavailable-backend receipt is inconsistent.");
899
+ }
900
+ return;
901
+ }
902
+ exactKeys(receipt.backend, "Verification Envelope backend", [
903
+ "id", "executable", "capabilities", "policySha256",
904
+ ]);
905
+ exactKeys(receipt.source, "Verification Envelope source", [
906
+ "beforeSha256", "afterSha256", "unchanged", "fileCount",
907
+ ]);
908
+ if (
909
+ receipt.backend.id !== "macos_sandbox_exec_v1"
910
+ || receipt.backend.executable !== "/usr/bin/sandbox-exec"
911
+ || !validHash(receipt.backend.policySha256)
912
+ || !validHash(receipt.source.beforeSha256)
913
+ || !validHash(receipt.source.afterSha256)
914
+ || !validNonNegativeInteger(receipt.source.fileCount)
915
+ ) {
916
+ throw new Error("Verification Envelope receipt backend or source is invalid.");
917
+ }
918
+ const phaseNames = receipt.phases.map(phase => phase.name);
919
+ if (new Set(phaseNames).size !== phaseNames.length) {
920
+ throw new Error("Verification Envelope receipt phases must be unique.");
921
+ }
922
+ for (const phase of receipt.phases) {
923
+ validateReceiptPhase(phase, validatedEnvelope.envelope);
924
+ }
925
+ const setup = receipt.phases.find(phase => phase.name === "setup") ?? null;
926
+ const expectedPhaseNames = [];
927
+ if (validatedEnvelope.envelope.phases.setup !== null) expectedPhaseNames.push("setup");
928
+ const setupPassed = setup === null
929
+ || (setup.exitCode === 0 && !setup.timedOut && !setup.outputOverflow);
930
+ if (setupPassed) expectedPhaseNames.push("check");
931
+ if (validatedEnvelope.envelope.phases.teardown !== null) expectedPhaseNames.push("teardown");
932
+ if (canonical(phaseNames) !== canonical(expectedPhaseNames)) {
933
+ throw new Error("Verification Envelope receipt phase sequence is inconsistent.");
934
+ }
935
+ const expectedEvidenceObjects = receipt.phases.flatMap(phase => [
936
+ { label: `${phase.name}:stdout`, ...phase.stdout },
937
+ { label: `${phase.name}:stderr`, ...phase.stderr },
938
+ ]);
939
+ for (const [index, object] of receipt.evidenceObjects.entries()) {
940
+ exactKeys(object, `Verification evidence object ${index}`, [
941
+ "label", "path", "sha256", "bytes",
942
+ ]);
943
+ validateReceiptFileRow(
944
+ { path: object.path, sha256: object.sha256, bytes: object.bytes },
945
+ `Verification evidence object ${index}`,
946
+ );
947
+ }
948
+ if (canonical(receipt.evidenceObjects) !== canonical(expectedEvidenceObjects)) {
949
+ throw new Error("Verification Envelope evidence objects do not match phase outputs.");
950
+ }
951
+ for (const [index, output] of receipt.outputs.entries()) {
952
+ validateReceiptFileRow(output, `Verification output ${index}`, { mode: true });
953
+ }
954
+ const sourceUnchanged = receipt.source.beforeSha256 === receipt.source.afterSha256;
955
+ const teardown = receipt.phases.find(phase => phase.name === "teardown") ?? null;
956
+ const teardownCompleted = validatedEnvelope.envelope.phases.teardown === null
957
+ || (teardown?.exitCode === 0 && !teardown.timedOut);
958
+ const allPhasesPassed = receipt.phases.some(phase => phase.name === "check")
959
+ && receipt.phases.every(phase => phase.exitCode === 0
960
+ && !phase.timedOut
961
+ && !phase.outputOverflow
962
+ && phase.launchError === null);
963
+ const formalEligible = allPhasesPassed
964
+ && sourceUnchanged
965
+ && receipt.cleanup.terminatedProcessIds.length === 0
966
+ && receipt.cleanup.remainingProcessIds.length === 0
967
+ && receipt.cleanup.issues.length === 0
968
+ && teardownCompleted;
969
+ if (
970
+ receipt.source.unchanged !== sourceUnchanged
971
+ || receipt.cleanup.teardownCompleted !== teardownCompleted
972
+ || receipt.commandExecuted !== (receipt.phases.length > 0)
973
+ || receipt.formalEligible !== formalEligible
974
+ || receipt.status !== (formalEligible
975
+ ? "verification_envelope_passed"
976
+ : "verification_envelope_failed")
977
+ || receipt.nextAllowedAction !== (formalEligible
978
+ ? "consume_in_formal_finish"
979
+ : "inspect_envelope_receipt_and_create_new_check")
980
+ || receipt.backend.policySha256
981
+ !== sha256(canonical(receipt.phases.map(phase => phase.policySha256)))
982
+ ) {
983
+ throw new Error("Verification Envelope receipt derived fields are inconsistent.");
984
+ }
985
+ }
986
+
609
987
  export async function runVerificationEnvelopeV1({
610
988
  workspaceRoot,
611
989
  envelope: rawEnvelope,
@@ -631,6 +1009,7 @@ export async function runVerificationEnvelopeV1({
631
1009
  }
632
1010
  return { ...receipt, receiptPath, resumed: true, verification: verified };
633
1011
  }
1012
+ mkdirBoundTemporaryDirectory(root, validated.envelope);
634
1013
  mkdirDeclaredRoots(root, [
635
1014
  ...validated.envelope.paths.declaredOutput,
636
1015
  ...validated.envelope.paths.disposableScratch,
@@ -792,20 +1171,25 @@ export async function runVerificationEnvelopeV1({
792
1171
  return { ...receipt, receiptPath, resumed: false };
793
1172
  }
794
1173
 
795
- export function verifyVerificationEnvelopeReceiptV1({ workspaceRoot, receiptPath }) {
1174
+ export function verifyVerificationEnvelopeReceiptV1({
1175
+ workspaceRoot,
1176
+ receiptPath,
1177
+ requireCurrentSource = true,
1178
+ }) {
796
1179
  const root = realpathSync(workspaceRoot);
797
1180
  const absolute = path.resolve(receiptPath);
798
1181
  if (!within(root, absolute)) throw new Error("Verification Envelope receipt escapes the workspace.");
799
1182
  assertNoSymlink(root, absolute, "Verification Envelope receipt");
800
1183
  const receipt = JSON.parse(readFileSync(absolute, "utf8"));
801
- if (receipt.schemaVersion !== RECEIPT_SCHEMA) throw new Error("Verification Envelope receipt schemaVersion is invalid.");
802
1184
  if (receipt.receiptSha256 !== receiptBodyHash(receipt)) {
803
1185
  throw new Error("Verification Envelope receipt hash mismatch.");
804
1186
  }
805
1187
  const validated = validateVerificationEnvelopeV1({
806
1188
  workspaceRoot: root,
807
1189
  envelope: receipt.envelope,
1190
+ requireCurrentEnvironment: requireCurrentSource,
808
1191
  });
1192
+ validateReceiptDerivations(receipt, validated);
809
1193
  if (validated.envelopeSha256 !== receipt.envelopeSha256) {
810
1194
  throw new Error("Verification Envelope receipt envelope hash mismatch.");
811
1195
  }
@@ -827,8 +1211,12 @@ export function verifyVerificationEnvelopeReceiptV1({ workspaceRoot, receiptPath
827
1211
  throw new Error(`Verification output hash mismatch: ${output.path}.`);
828
1212
  }
829
1213
  }
830
- const currentSource = snapshotPatterns(root, receipt.envelope.paths.immutableSource);
831
- if (receipt.formalEligible && currentSource.sha256 !== receipt.source.afterSha256) {
1214
+ if (
1215
+ requireCurrentSource
1216
+ && receipt.formalEligible
1217
+ && snapshotPatterns(root, receipt.envelope.paths.immutableSource).sha256
1218
+ !== receipt.source.afterSha256
1219
+ ) {
832
1220
  throw new Error("Verification Envelope receipt source drifted after execution.");
833
1221
  }
834
1222
  return {