owlrunkit 0.19.1 → 0.19.2
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/CHANGELOG.md +37 -0
- package/README.md +71 -0
- package/docs/architecture/OWLCODA_RUNKIT_0191_PROFILE_RECONCILE_PATCH_PRODUCT_DECISION_20260813.md +11 -5
- package/docs/architecture/OWLCODA_RUNKIT_0192_FORMAL_USABILITY_PRODUCT_DECISION_20260813.md +141 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +50 -7
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +5 -0
- package/packages/attest/src/quick.mjs +37 -4
- package/registry-release-policy-v1.json +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +35 -2
- package/scripts/runkit-contract/efficiency-metrics.mjs +9 -7
- package/scripts/runkit-contract/formal-preflight.mjs +352 -0
- package/scripts/runkit-contract/formal-supplemental-evidence.mjs +713 -0
- package/scripts/runkit-contract/formal-workflow.mjs +529 -59
- package/scripts/runkit-contract/onboarding-doctor.mjs +32 -13
- package/scripts/runkit-contract/project-cli-resolver.mjs +30 -3
- package/scripts/runkit-contract/project-external-delivery.mjs +16 -9
- package/scripts/runkit-contract/quick-attest.mjs +24 -0
- package/scripts/runkit-contract/runkit-cli.mjs +365 -49
- package/scripts/runkit-contract/schemas/formal-check-v2.schema.json +231 -0
- package/scripts/runkit-contract/schemas/formal-envelope-preflight-v1.schema.json +212 -0
- package/scripts/runkit-contract/schemas/formal-evidence-summary-v1.schema.json +114 -0
- package/scripts/runkit-contract/schemas/formal-supplemental-evidence-v1.schema.json +103 -0
- package/scripts/runkit-contract/schemas/team-project-event-v5.schema.json +89 -0
- package/scripts/runkit-contract/schemas/team-project-status-v3.schema.json +173 -0
- package/scripts/runkit-contract/snapshot.mjs +125 -5
- package/scripts/runkit-contract/source-candidate.mjs +271 -69
- package/scripts/runkit-contract/sync-release-identity.mjs +39 -4
- package/scripts/runkit-contract/team-project-successor.mjs +7 -4
- package/scripts/runkit-contract/team-project.mjs +440 -28
- package/scripts/runkit-contract/usage-mode.mjs +9 -3
- 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"
|
|
147
|
+
if (requestedMode === "managed"
|
|
148
|
+
|| (requestedMode === "auto" && (continuityReasons.length > 0 || projectDriverPresent))) {
|
|
147
149
|
continuity = "project_driver";
|
|
148
|
-
triggerReasons.push(...(continuityReasons.length > 0
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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({
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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({
|
|
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
|
-
|
|
831
|
-
|
|
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 {
|