owlrunkit 0.16.0 → 0.17.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 (31) hide show
  1. package/CHANGELOG.md +40 -3
  2. package/README.md +61 -12
  3. package/docs/architecture/OWLCODA_RUNKIT_017_AGENT_NATIVE_PRODUCT_DEFINITION.md +154 -0
  4. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +74 -16
  5. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  6. package/package.json +1 -1
  7. package/packages/attest/distribution-manifest-v1.json +10 -0
  8. package/packages/attest/src/quick.mjs +10 -0
  9. package/registry-release-policy-v1.json +5 -5
  10. package/sbom.spdx.json +5 -5
  11. package/scripts/runkit-contract/core-contract.mjs +12 -2
  12. package/scripts/runkit-contract/fleet-discovery.mjs +768 -26
  13. package/scripts/runkit-contract/formal-workflow.mjs +201 -0
  14. package/scripts/runkit-contract/lifecycle-orchestration.mjs +7 -2
  15. package/scripts/runkit-contract/project-cli-resolver.mjs +3 -0
  16. package/scripts/runkit-contract/project-control-parser.mjs +62 -3
  17. package/scripts/runkit-contract/runkit-cli.mjs +162 -13
  18. package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +14 -2
  19. package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +33 -2
  20. package/scripts/runkit-contract/schemas/team-project-definition-v1.schema.json +108 -0
  21. package/scripts/runkit-contract/schemas/team-project-event-v1.schema.json +99 -0
  22. package/scripts/runkit-contract/schemas/team-project-status-v1.schema.json +50 -0
  23. package/scripts/runkit-contract/schemas/verification-envelope-v1.schema.json +102 -0
  24. package/scripts/runkit-contract/sync-core016-release-identity.mjs +9 -4
  25. package/scripts/runkit-contract/sync-release-identity.mjs +224 -0
  26. package/scripts/runkit-contract/team-project.mjs +1006 -0
  27. package/scripts/runkit-contract/templates/team-project-definition.json +30 -0
  28. package/scripts/runkit-contract/templates/team-project-event.json +9 -0
  29. package/scripts/runkit-contract/templates/verification-envelope.json +28 -0
  30. package/scripts/runkit-contract/verification-envelope-check.mjs +45 -0
  31. package/scripts/runkit-contract/verification-envelope.mjs +846 -0
@@ -0,0 +1,846 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ realpathSync,
10
+ rmSync,
11
+ statSync,
12
+ } from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+
16
+ import {
17
+ assertAllowedKeys,
18
+ isRecord,
19
+ safeIdentifier,
20
+ safeRelativePath,
21
+ writeJsonExclusiveAtomically,
22
+ } from "./provenance-common.mjs";
23
+
24
+ const ENVELOPE_SCHEMA = "OwlCodaRunKitVerificationEnvelopeV1";
25
+ const RECEIPT_SCHEMA = "OwlCodaRunKitVerificationEnvelopeReceiptV1";
26
+ const SECRET_NAME = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSCODE|CREDENTIAL|PRIVATE_KEY|API_KEY)(?:_|$)/iu;
27
+ const DEFAULT_FORBIDDEN_READS = [
28
+ ".env",
29
+ ".env.local",
30
+ ".git-credentials",
31
+ ".npmrc",
32
+ ".ssh/**",
33
+ ".aws/**",
34
+ ".config/gcloud/**",
35
+ ".codex/**",
36
+ ".owlcoda/**",
37
+ ];
38
+
39
+ function canonical(value) {
40
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
41
+ if (isRecord(value)) {
42
+ return `{${Object.keys(value).sort().map(key => (
43
+ `${JSON.stringify(key)}:${canonical(value[key])}`
44
+ )).join(",")}}`;
45
+ }
46
+ const encoded = JSON.stringify(value);
47
+ if (encoded === undefined) throw new Error("Verification Envelope is not canonical JSON.");
48
+ return encoded;
49
+ }
50
+
51
+ function sha256(value) {
52
+ return createHash("sha256").update(value).digest("hex");
53
+ }
54
+
55
+ function stringArray(value, label, { allowEmpty = true } = {}) {
56
+ if (!Array.isArray(value)
57
+ || (!allowEmpty && value.length === 0)
58
+ || value.some(item => typeof item !== "string" || item.length === 0)
59
+ || new Set(value).size !== value.length) {
60
+ throw new Error(`${label} must be a unique string array${allowEmpty ? "" : " with at least one entry"}.`);
61
+ }
62
+ return [...value];
63
+ }
64
+
65
+ function argvList(value, label) {
66
+ if (!Array.isArray(value) || value.some(item => typeof item !== "string")) {
67
+ throw new Error(`${label} must be a string array.`);
68
+ }
69
+ return [...value];
70
+ }
71
+
72
+ function patternRoot(pattern) {
73
+ return pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
74
+ }
75
+
76
+ function patternCovers(owner, candidate) {
77
+ if (owner === candidate) return true;
78
+ if (!owner.endsWith("/**")) return false;
79
+ const prefix = patternRoot(owner);
80
+ const candidateRoot = patternRoot(candidate);
81
+ return candidateRoot === prefix || candidateRoot.startsWith(`${prefix}/`);
82
+ }
83
+
84
+ function patternsOverlap(left, right) {
85
+ return left.some(a => right.some(b => patternCovers(a, b) || patternCovers(b, a)));
86
+ }
87
+
88
+ function validatePatterns(value, label, { allowEmpty = false } = {}) {
89
+ const patterns = stringArray(value, label, { allowEmpty });
90
+ return patterns.map((pattern) => {
91
+ const root = patternRoot(pattern);
92
+ if (pattern.includes("*") && !pattern.endsWith("/**")) {
93
+ throw new Error(`${label} only supports exact paths or directory/** patterns.`);
94
+ }
95
+ safeRelativePath(root, label);
96
+ return pattern;
97
+ }).sort();
98
+ }
99
+
100
+ function within(root, candidate) {
101
+ const relative = path.relative(root, candidate);
102
+ return relative === ""
103
+ || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
104
+ }
105
+
106
+ function assertNoSymlink(root, absolutePath, label, { allowMissingLeaf = false } = {}) {
107
+ if (!within(root, absolutePath)) throw new Error(`${label} escapes the workspace.`);
108
+ const relative = path.relative(root, absolutePath);
109
+ let current = root;
110
+ for (const [index, segment] of relative.split(path.sep).filter(Boolean).entries()) {
111
+ current = path.join(current, segment);
112
+ if (!existsSync(current)) {
113
+ if (allowMissingLeaf) return;
114
+ throw new Error(`${label} does not exist: ${path.relative(root, current)}.`);
115
+ }
116
+ const stat = lstatSync(current);
117
+ if (stat.isSymbolicLink()) throw new Error(`${label} must not traverse a symlink.`);
118
+ if (index < relative.split(path.sep).filter(Boolean).length - 1 && !stat.isDirectory()) {
119
+ throw new Error(`${label} has a non-directory ancestor.`);
120
+ }
121
+ }
122
+ }
123
+
124
+ function validateCommand(value, label) {
125
+ if (value === null) return null;
126
+ assertAllowedKeys(value, label, ["executable", "argv", "timeoutMs"]);
127
+ if (typeof value.executable !== "string" || !path.isAbsolute(value.executable)) {
128
+ throw new Error(`${label} executable must be an absolute regular file.`);
129
+ }
130
+ let executable;
131
+ try {
132
+ const stat = lstatSync(value.executable);
133
+ executable = realpathSync(value.executable);
134
+ if (stat.isSymbolicLink() || !stat.isFile() || executable !== value.executable) {
135
+ throw new Error("not-real");
136
+ }
137
+ } catch {
138
+ throw new Error(`${label} executable must be an absolute regular file without symlinks.`);
139
+ }
140
+ const argv = argvList(value.argv, `${label} argv`);
141
+ if (!Number.isSafeInteger(value.timeoutMs) || value.timeoutMs < 100 || value.timeoutMs > 3_600_000) {
142
+ throw new Error(`${label} timeoutMs must be an integer from 100 to 3600000.`);
143
+ }
144
+ return { executable, argv, timeoutMs: value.timeoutMs };
145
+ }
146
+
147
+ function homeForbiddenPaths() {
148
+ const home = os.homedir();
149
+ return [
150
+ path.join(home, ".ssh"),
151
+ path.join(home, ".aws"),
152
+ path.join(home, ".npmrc"),
153
+ path.join(home, ".git-credentials"),
154
+ path.join(home, ".config", "gcloud"),
155
+ path.join(home, ".codex"),
156
+ path.join(home, ".owlcoda"),
157
+ ];
158
+ }
159
+
160
+ export function validateVerificationEnvelopeV1({ workspaceRoot, envelope }) {
161
+ const root = realpathSync(workspaceRoot);
162
+ assertAllowedKeys(envelope, "Verification Envelope", [
163
+ "schemaVersion", "envelopeId", "cwd", "lockfiles", "paths",
164
+ "environment", "network", "process", "phases",
165
+ ]);
166
+ if (envelope.schemaVersion !== ENVELOPE_SCHEMA) {
167
+ throw new Error(`Verification Envelope schemaVersion must be ${ENVELOPE_SCHEMA}.`);
168
+ }
169
+ safeIdentifier(envelope.envelopeId, "envelopeId");
170
+ const cwd = safeRelativePath(envelope.cwd, "Verification Envelope cwd", { allowDot: true });
171
+ const cwdPath = path.resolve(root, cwd);
172
+ assertNoSymlink(root, cwdPath, "Verification Envelope cwd");
173
+ if (!lstatSync(cwdPath).isDirectory()) throw new Error("Verification Envelope cwd must be a directory.");
174
+
175
+ const lockfiles = validatePatterns(envelope.lockfiles, "Verification Envelope lockfiles", { allowEmpty: true });
176
+ for (const lockfile of lockfiles) {
177
+ if (lockfile.endsWith("/**")) throw new Error("Verification Envelope lockfiles must be exact files.");
178
+ const absolute = path.join(root, lockfile);
179
+ assertNoSymlink(root, absolute, "Verification Envelope lockfile");
180
+ if (!lstatSync(absolute).isFile()) throw new Error(`Verification Envelope lockfile is not a file: ${lockfile}.`);
181
+ }
182
+
183
+ assertAllowedKeys(envelope.paths, "Verification Envelope paths", [
184
+ "immutableSource", "declaredOutput", "disposableScratch", "forbidden",
185
+ ]);
186
+ const paths = {
187
+ immutableSource: validatePatterns(envelope.paths.immutableSource, "immutableSource"),
188
+ declaredOutput: validatePatterns(envelope.paths.declaredOutput, "declaredOutput", { allowEmpty: true }),
189
+ disposableScratch: validatePatterns(envelope.paths.disposableScratch, "disposableScratch", { allowEmpty: true }),
190
+ forbidden: validatePatterns(envelope.paths.forbidden, "forbidden", { allowEmpty: true }),
191
+ };
192
+ if (!lockfiles.every(lockfile => paths.immutableSource.some(pattern => patternCovers(pattern, lockfile)))) {
193
+ throw new Error("Every lockfile must be included in immutableSource.");
194
+ }
195
+ const classes = Object.entries(paths);
196
+ for (let left = 0; left < classes.length; left += 1) {
197
+ for (let right = left + 1; right < classes.length; right += 1) {
198
+ if (patternsOverlap(classes[left][1], classes[right][1])) {
199
+ throw new Error(`Verification Envelope path classes overlap: ${classes[left][0]} and ${classes[right][0]}.`);
200
+ }
201
+ }
202
+ }
203
+ for (const pattern of paths.immutableSource) {
204
+ const absolute = path.join(root, patternRoot(pattern));
205
+ assertNoSymlink(root, absolute, "immutableSource");
206
+ const stat = lstatSync(absolute);
207
+ if (pattern.endsWith("/**") ? !stat.isDirectory() : !stat.isFile()) {
208
+ throw new Error(`immutableSource type does not match pattern: ${pattern}.`);
209
+ }
210
+ }
211
+ for (const pattern of [...paths.declaredOutput, ...paths.disposableScratch, ...paths.forbidden]) {
212
+ assertNoSymlink(root, path.join(root, patternRoot(pattern)), "Verification Envelope path", {
213
+ allowMissingLeaf: true,
214
+ });
215
+ }
216
+
217
+ assertAllowedKeys(envelope.environment, "Verification Envelope environment", ["allowNames", "values"]);
218
+ const allowNames = stringArray(envelope.environment.allowNames, "environment allowNames").sort();
219
+ if (allowNames.some(name => !/^[A-Z_][A-Z0-9_]*$/u.test(name) || SECRET_NAME.test(name))) {
220
+ throw new Error("Verification Envelope environment names must be non-secret uppercase identifiers.");
221
+ }
222
+ if (!isRecord(envelope.environment.values)) throw new Error("Verification Envelope environment values must be an object.");
223
+ const values = {};
224
+ for (const [name, value] of Object.entries(envelope.environment.values)) {
225
+ if (!allowNames.includes(name)) throw new Error(`Verification Envelope environment value ${name} is not allowlisted.`);
226
+ if (typeof value !== "string") throw new Error(`Verification Envelope environment value ${name} must be a string.`);
227
+ if (SECRET_NAME.test(name)) throw new Error(`Verification Envelope credential-like environment value is forbidden: ${name}.`);
228
+ values[name] = value;
229
+ }
230
+
231
+ assertAllowedKeys(envelope.network, "Verification Envelope network", ["mode"]);
232
+ if (!new Set(["deny", "loopback"]).has(envelope.network.mode)) {
233
+ throw new Error("Verification Envelope network mode must be deny or loopback.");
234
+ }
235
+ assertAllowedKeys(envelope.process, "Verification Envelope process", [
236
+ "allowSubprocesses", "allowedExecutables", "allowBackgroundAfterFinish",
237
+ ]);
238
+ if (typeof envelope.process.allowSubprocesses !== "boolean"
239
+ || typeof envelope.process.allowBackgroundAfterFinish !== "boolean") {
240
+ throw new Error("Verification Envelope process booleans are required.");
241
+ }
242
+ if (envelope.process.allowBackgroundAfterFinish) {
243
+ throw new Error("Verification Envelope cannot allow background processes after finish.");
244
+ }
245
+ const allowedExecutables = stringArray(
246
+ envelope.process.allowedExecutables,
247
+ "process allowedExecutables",
248
+ ).map((executable) => {
249
+ if (!path.isAbsolute(executable)) throw new Error("Allowed executable must be absolute.");
250
+ const stat = lstatSync(executable);
251
+ const resolved = realpathSync(executable);
252
+ if (stat.isSymbolicLink() || !stat.isFile() || resolved !== executable) {
253
+ throw new Error("Allowed executable must be a regular file without symlinks.");
254
+ }
255
+ return executable;
256
+ }).sort();
257
+ if (!envelope.process.allowSubprocesses && allowedExecutables.length > 0) {
258
+ throw new Error("allowedExecutables requires allowSubprocesses=true.");
259
+ }
260
+
261
+ assertAllowedKeys(envelope.phases, "Verification Envelope phases", ["setup", "check", "teardown"]);
262
+ const phases = {
263
+ setup: validateCommand(envelope.phases.setup, "setup phase"),
264
+ check: validateCommand(envelope.phases.check, "check phase"),
265
+ teardown: validateCommand(envelope.phases.teardown, "teardown phase"),
266
+ };
267
+ if (phases.check === null) throw new Error("Verification Envelope check phase is required.");
268
+ const normalized = {
269
+ schemaVersion: ENVELOPE_SCHEMA,
270
+ envelopeId: envelope.envelopeId,
271
+ cwd,
272
+ lockfiles,
273
+ paths,
274
+ environment: { allowNames, values },
275
+ network: { mode: envelope.network.mode },
276
+ process: {
277
+ allowSubprocesses: envelope.process.allowSubprocesses,
278
+ allowedExecutables,
279
+ allowBackgroundAfterFinish: false,
280
+ },
281
+ phases,
282
+ };
283
+ return {
284
+ envelope: normalized,
285
+ envelopeSha256: sha256(canonical(normalized)),
286
+ builtInForbiddenReads: [...DEFAULT_FORBIDDEN_READS],
287
+ authorizationGranted: false,
288
+ };
289
+ }
290
+
291
+ function commandExists(command) {
292
+ try {
293
+ const stat = lstatSync(command);
294
+ return stat.isFile() && !stat.isSymbolicLink() && realpathSync(command) === command;
295
+ } catch {
296
+ return false;
297
+ }
298
+ }
299
+
300
+ export function resolveVerificationEnvelopeBackendV1({ platform = process.platform } = {}) {
301
+ if (platform === "darwin" && commandExists("/usr/bin/sandbox-exec")) {
302
+ return {
303
+ id: "macos_sandbox_exec_v1",
304
+ available: true,
305
+ executable: "/usr/bin/sandbox-exec",
306
+ capabilities: {
307
+ fileWritePolicy: "enforced",
308
+ forbiddenReadPolicy: "enforced",
309
+ networkPolicy: "enforced",
310
+ processExecPolicy: "enforced",
311
+ processCleanup: "verified",
312
+ },
313
+ };
314
+ }
315
+ return {
316
+ id: "unavailable",
317
+ available: false,
318
+ reason: platform === "linux"
319
+ ? "linux_backend_cannot_yet_prove_process_exec_policy"
320
+ : "supported_enforcement_backend_not_found",
321
+ capabilities: {
322
+ fileWritePolicy: "unavailable",
323
+ forbiddenReadPolicy: "unavailable",
324
+ networkPolicy: "unavailable",
325
+ processExecPolicy: "unavailable",
326
+ processCleanup: "unavailable",
327
+ },
328
+ };
329
+ }
330
+
331
+ function schemeString(value) {
332
+ return JSON.stringify(value).replaceAll("\\", "\\\\");
333
+ }
334
+
335
+ function sandboxPathRule(operation, absolutePath, isTree) {
336
+ return `(${operation} (${isTree ? "subpath" : "literal"} ${schemeString(absolutePath)}))`;
337
+ }
338
+
339
+ function buildMacPolicy({ root, envelope, phase }) {
340
+ const lines = [
341
+ "(version 1)",
342
+ "(allow default)",
343
+ "(deny file-write*)",
344
+ ];
345
+ for (const pattern of [...envelope.paths.declaredOutput, ...envelope.paths.disposableScratch]) {
346
+ const absolute = path.join(root, patternRoot(pattern));
347
+ lines.push(sandboxPathRule("allow file-write*", absolute, pattern.endsWith("/**")));
348
+ }
349
+ const forbidden = new Map();
350
+ for (const pattern of [...DEFAULT_FORBIDDEN_READS, ...envelope.paths.forbidden]) {
351
+ forbidden.set(path.join(root, patternRoot(pattern)), pattern.endsWith("/**"));
352
+ }
353
+ for (const absolute of homeForbiddenPaths()) forbidden.set(absolute, statSafeDirectory(absolute));
354
+ for (const [absolute, isTree] of forbidden) {
355
+ lines.push(sandboxPathRule("deny file-read*", absolute, isTree));
356
+ }
357
+ lines.push("(deny network*)");
358
+ if (envelope.network.mode === "loopback") {
359
+ lines.push("(allow network* (local ip))");
360
+ }
361
+ lines.push("(deny process-exec)");
362
+ lines.push(sandboxPathRule("allow process-exec", phase.executable, false));
363
+ if (envelope.process.allowSubprocesses) {
364
+ for (const executable of envelope.process.allowedExecutables) {
365
+ lines.push(sandboxPathRule("allow process-exec", executable, false));
366
+ }
367
+ } else {
368
+ lines.push("(deny process-fork)");
369
+ }
370
+ return `${lines.join("\n")}\n`;
371
+ }
372
+
373
+ function statSafeDirectory(value) {
374
+ try {
375
+ return statSync(value).isDirectory();
376
+ } catch {
377
+ return value.endsWith(path.sep) || !path.extname(value);
378
+ }
379
+ }
380
+
381
+ function mkdirDeclaredRoots(root, patterns) {
382
+ for (const pattern of patterns) {
383
+ const absolute = path.join(root, patternRoot(pattern));
384
+ const directory = pattern.endsWith("/**") ? absolute : path.dirname(absolute);
385
+ mkdirSync(directory, { recursive: true });
386
+ assertNoSymlink(root, directory, "Verification Envelope writable path");
387
+ }
388
+ }
389
+
390
+ function walkFiles(root, absolute, relativeBase, files) {
391
+ const stat = lstatSync(absolute);
392
+ if (stat.isSymbolicLink()) throw new Error(`Verification Envelope path contains symlink: ${relativeBase}.`);
393
+ if (stat.isFile()) {
394
+ files.push({
395
+ path: relativeBase.split(path.sep).join("/"),
396
+ sha256: sha256(readFileSync(absolute)),
397
+ bytes: stat.size,
398
+ mode: stat.mode & 0o777,
399
+ });
400
+ return;
401
+ }
402
+ if (!stat.isDirectory()) throw new Error(`Verification Envelope path is not a regular file or directory: ${relativeBase}.`);
403
+ for (const entry of readdirSync(absolute).sort()) {
404
+ walkFiles(root, path.join(absolute, entry), path.join(relativeBase, entry), files);
405
+ }
406
+ }
407
+
408
+ function snapshotPatterns(root, patterns, { missingAllowed = false } = {}) {
409
+ const files = [];
410
+ for (const pattern of patterns) {
411
+ const relative = patternRoot(pattern);
412
+ const absolute = path.join(root, relative);
413
+ if (!existsSync(absolute)) {
414
+ if (missingAllowed) continue;
415
+ throw new Error(`Verification Envelope path disappeared: ${pattern}.`);
416
+ }
417
+ if (pattern.endsWith("/**")) {
418
+ walkFiles(root, absolute, relative, files);
419
+ } else {
420
+ const stat = lstatSync(absolute);
421
+ if (stat.isSymbolicLink() || !stat.isFile()) {
422
+ throw new Error(`Verification Envelope exact path is not a regular file: ${pattern}.`);
423
+ }
424
+ files.push({
425
+ path: relative,
426
+ sha256: sha256(readFileSync(absolute)),
427
+ bytes: stat.size,
428
+ mode: stat.mode & 0o777,
429
+ });
430
+ }
431
+ }
432
+ const unique = [...new Map(files.map(row => [row.path, row])).values()]
433
+ .sort((left, right) => left.path.localeCompare(right.path));
434
+ return {
435
+ files: unique,
436
+ sha256: sha256(canonical(unique)),
437
+ };
438
+ }
439
+
440
+ function filteredEnvironment(envelope) {
441
+ const env = {};
442
+ for (const name of envelope.environment.allowNames) {
443
+ if (Object.hasOwn(envelope.environment.values, name)) {
444
+ env[name] = envelope.environment.values[name];
445
+ } else if (typeof process.env[name] === "string") {
446
+ env[name] = process.env[name];
447
+ }
448
+ }
449
+ return env;
450
+ }
451
+
452
+ function processIdsForGroup(groupId) {
453
+ if (!Number.isSafeInteger(groupId) || groupId < 1) return [];
454
+ const result = spawnSync("/bin/ps", ["-axo", "pid=,pgid="], { encoding: "utf8" });
455
+ if (result.status !== 0) return [];
456
+ return result.stdout.split("\n").map(line => line.trim().split(/\s+/u).map(Number))
457
+ .filter(([pid, pgid]) => Number.isSafeInteger(pid) && pgid === groupId && pid !== process.pid)
458
+ .map(([pid]) => pid)
459
+ .sort((left, right) => left - right);
460
+ }
461
+
462
+ function killGroup(groupId) {
463
+ const before = processIdsForGroup(groupId);
464
+ if (before.length === 0) return { terminated: [], remaining: [] };
465
+ try {
466
+ process.kill(-groupId, "SIGTERM");
467
+ } catch {}
468
+ const until = Date.now() + 250;
469
+ while (Date.now() < until && processIdsForGroup(groupId).length > 0) {
470
+ // Bounded synchronous polling keeps cleanup inside the command lifecycle.
471
+ }
472
+ let remaining = processIdsForGroup(groupId);
473
+ if (remaining.length > 0) {
474
+ try {
475
+ process.kill(-groupId, "SIGKILL");
476
+ } catch {}
477
+ remaining = processIdsForGroup(groupId);
478
+ }
479
+ return { terminated: before, remaining };
480
+ }
481
+
482
+ function executeMacPhase({ root, cwd, envelope, phase, name, backend }) {
483
+ const policy = buildMacPolicy({ root, envelope, phase });
484
+ const policySha256 = sha256(policy);
485
+ return new Promise((resolve) => {
486
+ const startedAt = new Date().toISOString();
487
+ const child = spawn(backend.executable, [
488
+ "-p",
489
+ policy,
490
+ phase.executable,
491
+ ...phase.argv,
492
+ ], {
493
+ cwd: path.join(root, cwd),
494
+ env: filteredEnvironment(envelope),
495
+ detached: true,
496
+ stdio: ["ignore", "pipe", "pipe"],
497
+ });
498
+ const stdout = [];
499
+ const stderr = [];
500
+ let timedOut = false;
501
+ let overflow = false;
502
+ let bytes = 0;
503
+ const capture = target => chunk => {
504
+ bytes += chunk.length;
505
+ if (bytes > 5 * 1024 * 1024) {
506
+ overflow = true;
507
+ try { process.kill(-child.pid, "SIGKILL"); } catch {}
508
+ return;
509
+ }
510
+ target.push(chunk);
511
+ };
512
+ child.stdout.on("data", capture(stdout));
513
+ child.stderr.on("data", capture(stderr));
514
+ const timer = setTimeout(() => {
515
+ timedOut = true;
516
+ try { process.kill(-child.pid, "SIGKILL"); } catch {}
517
+ }, phase.timeoutMs);
518
+ child.once("error", (error) => {
519
+ clearTimeout(timer);
520
+ const cleanup = killGroup(child.pid);
521
+ resolve({
522
+ name,
523
+ executable: phase.executable,
524
+ argv: [...phase.argv],
525
+ startedAt,
526
+ finishedAt: new Date().toISOString(),
527
+ timeoutMs: phase.timeoutMs,
528
+ timedOut,
529
+ outputOverflow: overflow,
530
+ exitCode: null,
531
+ signal: null,
532
+ launchError: error.message,
533
+ stdout: Buffer.concat(stdout),
534
+ stderr: Buffer.concat(stderr),
535
+ policy,
536
+ policySha256,
537
+ processGroupId: child.pid,
538
+ cleanup,
539
+ });
540
+ });
541
+ child.once("close", (code, signal) => {
542
+ clearTimeout(timer);
543
+ const cleanup = killGroup(child.pid);
544
+ resolve({
545
+ name,
546
+ executable: phase.executable,
547
+ argv: [...phase.argv],
548
+ startedAt,
549
+ finishedAt: new Date().toISOString(),
550
+ timeoutMs: phase.timeoutMs,
551
+ timedOut,
552
+ outputOverflow: overflow,
553
+ exitCode: code,
554
+ signal,
555
+ launchError: null,
556
+ stdout: Buffer.concat(stdout),
557
+ stderr: Buffer.concat(stderr),
558
+ policy,
559
+ policySha256,
560
+ processGroupId: child.pid,
561
+ cleanup,
562
+ });
563
+ });
564
+ });
565
+ }
566
+
567
+ function writeObject(artifactRoot, label, bytes) {
568
+ const hash = sha256(bytes);
569
+ const objectRoot = path.join(artifactRoot, "objects");
570
+ mkdirSync(objectRoot, { recursive: true });
571
+ const objectPath = path.join(objectRoot, hash);
572
+ if (!existsSync(objectPath)) {
573
+ writeJsonExclusiveAtomically(`${objectPath}.metadata.json`, {
574
+ schemaVersion: "OwlCodaRunKitEvidenceObjectMetadataV1",
575
+ sha256: hash,
576
+ bytes: bytes.length,
577
+ labels: [label],
578
+ });
579
+ const temporaryJson = { encoding: "base64", data: bytes.toString("base64") };
580
+ writeJsonExclusiveAtomically(objectPath, temporaryJson);
581
+ }
582
+ return { label, path: objectPath, sha256: hash, bytes: bytes.length };
583
+ }
584
+
585
+ function cleanScratch(root, patterns) {
586
+ const removed = [];
587
+ const failed = [];
588
+ for (const pattern of patterns) {
589
+ const relative = patternRoot(pattern);
590
+ const absolute = path.join(root, relative);
591
+ try {
592
+ if (existsSync(absolute)) {
593
+ rmSync(absolute, { recursive: true, force: true });
594
+ removed.push(relative);
595
+ }
596
+ } catch (error) {
597
+ failed.push({ path: relative, issue: error instanceof Error ? error.message : String(error) });
598
+ }
599
+ }
600
+ return { removed, failed };
601
+ }
602
+
603
+ function receiptBodyHash(receipt) {
604
+ const { receiptSha256: _ignored, ...body } = receipt;
605
+ return sha256(canonical(body));
606
+ }
607
+
608
+ function receiptPathFor(artifactRoot) {
609
+ return path.join(artifactRoot, "verification-envelope-receipt.json");
610
+ }
611
+
612
+ export async function runVerificationEnvelopeV1({
613
+ workspaceRoot,
614
+ envelope: rawEnvelope,
615
+ artifactRoot,
616
+ backend = resolveVerificationEnvelopeBackendV1(),
617
+ }) {
618
+ const validated = validateVerificationEnvelopeV1({ workspaceRoot, envelope: rawEnvelope });
619
+ const root = realpathSync(workspaceRoot);
620
+ const requestedRoot = path.resolve(workspaceRoot);
621
+ const requestedOutputRoot = path.resolve(artifactRoot);
622
+ if (!within(requestedRoot, requestedOutputRoot)) {
623
+ throw new Error("Verification Envelope artifactRoot must remain inside the workspace.");
624
+ }
625
+ const outputRoot = path.join(root, path.relative(requestedRoot, requestedOutputRoot));
626
+ assertNoSymlink(root, outputRoot, "Verification Envelope artifactRoot", { allowMissingLeaf: true });
627
+ mkdirSync(outputRoot, { recursive: true });
628
+ const receiptPath = receiptPathFor(outputRoot);
629
+ if (existsSync(receiptPath)) {
630
+ const verified = verifyVerificationEnvelopeReceiptV1({ workspaceRoot: root, receiptPath });
631
+ const receipt = JSON.parse(readFileSync(receiptPath, "utf8"));
632
+ if (receipt.envelopeSha256 !== validated.envelopeSha256) {
633
+ throw new Error("Existing Verification Envelope receipt binds a different envelope.");
634
+ }
635
+ return { ...receipt, receiptPath, resumed: true, verification: verified };
636
+ }
637
+ mkdirDeclaredRoots(root, [
638
+ ...validated.envelope.paths.declaredOutput,
639
+ ...validated.envelope.paths.disposableScratch,
640
+ ]);
641
+ const sourceBefore = snapshotPatterns(root, validated.envelope.paths.immutableSource);
642
+ const startedAt = new Date().toISOString();
643
+ if (!backend.available) {
644
+ const body = {
645
+ schemaVersion: RECEIPT_SCHEMA,
646
+ envelopeId: validated.envelope.envelopeId,
647
+ envelope: validated.envelope,
648
+ envelopeSha256: validated.envelopeSha256,
649
+ status: "policy_backend_unavailable",
650
+ formalEligible: false,
651
+ commandExecuted: false,
652
+ backend,
653
+ startedAt,
654
+ finishedAt: new Date().toISOString(),
655
+ phases: [],
656
+ source: { beforeSha256: sourceBefore.sha256, afterSha256: sourceBefore.sha256 },
657
+ outputs: [],
658
+ evidenceObjects: [],
659
+ cleanup: {
660
+ teardownCompleted: false,
661
+ terminatedProcessIds: [],
662
+ remainingProcessIds: [],
663
+ remainingPorts: [],
664
+ scratchRemoved: [],
665
+ issues: [backend.reason],
666
+ },
667
+ nextAllowedAction: "install_supported_enforcement_backend_or_use_captured_evidence",
668
+ authorizationGranted: false,
669
+ };
670
+ const receipt = { ...body, receiptSha256: sha256(canonical(body)) };
671
+ writeJsonExclusiveAtomically(receiptPath, receipt);
672
+ return { ...receipt, receiptPath, resumed: false };
673
+ }
674
+ if (backend.id !== "macos_sandbox_exec_v1") {
675
+ throw new Error(`Unsupported Verification Envelope backend: ${backend.id}.`);
676
+ }
677
+
678
+ const rawPhases = [];
679
+ const terminatedProcessIds = new Set();
680
+ const remainingProcessIds = new Set();
681
+ const run = async (name, phase) => {
682
+ const result = await executeMacPhase({
683
+ root,
684
+ cwd: validated.envelope.cwd,
685
+ envelope: validated.envelope,
686
+ phase,
687
+ name,
688
+ backend,
689
+ });
690
+ rawPhases.push(result);
691
+ for (const pid of result.cleanup.terminated) terminatedProcessIds.add(pid);
692
+ for (const pid of result.cleanup.remaining) remainingProcessIds.add(pid);
693
+ return result;
694
+ };
695
+ let setupPassed = true;
696
+ if (validated.envelope.phases.setup) {
697
+ const setup = await run("setup", validated.envelope.phases.setup);
698
+ setupPassed = setup.exitCode === 0 && !setup.timedOut && !setup.outputOverflow;
699
+ }
700
+ if (setupPassed) await run("check", validated.envelope.phases.check);
701
+ if (validated.envelope.phases.teardown) {
702
+ await run("teardown", validated.envelope.phases.teardown);
703
+ }
704
+ const sourceAfter = snapshotPatterns(root, validated.envelope.paths.immutableSource);
705
+ const outputs = snapshotPatterns(
706
+ root,
707
+ validated.envelope.paths.declaredOutput,
708
+ { missingAllowed: true },
709
+ ).files;
710
+ const scratchCleanup = cleanScratch(root, validated.envelope.paths.disposableScratch);
711
+ const evidenceObjects = [];
712
+ const phases = rawPhases.map((phase) => {
713
+ const stdout = writeObject(outputRoot, `${phase.name}:stdout`, phase.stdout);
714
+ const stderr = writeObject(outputRoot, `${phase.name}:stderr`, phase.stderr);
715
+ evidenceObjects.push(stdout, stderr);
716
+ return {
717
+ name: phase.name,
718
+ executable: phase.executable,
719
+ argv: phase.argv,
720
+ startedAt: phase.startedAt,
721
+ finishedAt: phase.finishedAt,
722
+ timeoutMs: phase.timeoutMs,
723
+ timedOut: phase.timedOut,
724
+ outputOverflow: phase.outputOverflow,
725
+ exitCode: phase.exitCode,
726
+ signal: phase.signal,
727
+ launchError: phase.launchError,
728
+ policySha256: phase.policySha256,
729
+ processGroupId: phase.processGroupId,
730
+ stdout: { path: stdout.path, sha256: stdout.sha256, bytes: stdout.bytes },
731
+ stderr: { path: stderr.path, sha256: stderr.sha256, bytes: stderr.bytes },
732
+ leakedProcessIds: phase.cleanup.terminated,
733
+ };
734
+ });
735
+ const teardown = phases.find(row => row.name === "teardown") ?? null;
736
+ const cleanup = {
737
+ teardownCompleted: validated.envelope.phases.teardown === null
738
+ || (teardown?.exitCode === 0 && !teardown.timedOut),
739
+ terminatedProcessIds: [...terminatedProcessIds].sort((a, b) => a - b),
740
+ remainingProcessIds: [...remainingProcessIds].sort((a, b) => a - b),
741
+ remainingPorts: [],
742
+ scratchRemoved: scratchCleanup.removed,
743
+ issues: scratchCleanup.failed,
744
+ };
745
+ const allPhasesPassed = phases.some(row => row.name === "check")
746
+ && phases.every(row => row.exitCode === 0
747
+ && !row.timedOut
748
+ && !row.outputOverflow
749
+ && row.launchError === null);
750
+ const sourceUnchanged = sourceBefore.sha256 === sourceAfter.sha256;
751
+ const formalEligible = allPhasesPassed
752
+ && sourceUnchanged
753
+ && cleanup.terminatedProcessIds.length === 0
754
+ && cleanup.remainingProcessIds.length === 0
755
+ && cleanup.issues.length === 0
756
+ && cleanup.teardownCompleted;
757
+ const body = {
758
+ schemaVersion: RECEIPT_SCHEMA,
759
+ envelopeId: validated.envelope.envelopeId,
760
+ envelope: validated.envelope,
761
+ envelopeSha256: validated.envelopeSha256,
762
+ status: formalEligible ? "verification_envelope_passed" : "verification_envelope_failed",
763
+ formalEligible,
764
+ commandExecuted: phases.length > 0,
765
+ backend: {
766
+ id: backend.id,
767
+ executable: backend.executable,
768
+ capabilities: backend.capabilities,
769
+ policySha256: sha256(canonical(phases.map(row => row.policySha256))),
770
+ },
771
+ startedAt,
772
+ finishedAt: new Date().toISOString(),
773
+ phases,
774
+ source: {
775
+ beforeSha256: sourceBefore.sha256,
776
+ afterSha256: sourceAfter.sha256,
777
+ unchanged: sourceUnchanged,
778
+ fileCount: sourceAfter.files.length,
779
+ },
780
+ outputs,
781
+ evidenceObjects: evidenceObjects.map(row => ({
782
+ label: row.label,
783
+ path: row.path,
784
+ sha256: row.sha256,
785
+ bytes: row.bytes,
786
+ })),
787
+ cleanup,
788
+ nextAllowedAction: formalEligible
789
+ ? "consume_in_formal_finish"
790
+ : "inspect_envelope_receipt_and_create_new_check",
791
+ authorizationGranted: false,
792
+ };
793
+ const receipt = { ...body, receiptSha256: sha256(canonical(body)) };
794
+ writeJsonExclusiveAtomically(receiptPath, receipt);
795
+ return { ...receipt, receiptPath, resumed: false };
796
+ }
797
+
798
+ export function verifyVerificationEnvelopeReceiptV1({ workspaceRoot, receiptPath }) {
799
+ const root = realpathSync(workspaceRoot);
800
+ const absolute = path.resolve(receiptPath);
801
+ if (!within(root, absolute)) throw new Error("Verification Envelope receipt escapes the workspace.");
802
+ assertNoSymlink(root, absolute, "Verification Envelope receipt");
803
+ const receipt = JSON.parse(readFileSync(absolute, "utf8"));
804
+ if (receipt.schemaVersion !== RECEIPT_SCHEMA) throw new Error("Verification Envelope receipt schemaVersion is invalid.");
805
+ if (receipt.receiptSha256 !== receiptBodyHash(receipt)) {
806
+ throw new Error("Verification Envelope receipt hash mismatch.");
807
+ }
808
+ const validated = validateVerificationEnvelopeV1({
809
+ workspaceRoot: root,
810
+ envelope: receipt.envelope,
811
+ });
812
+ if (validated.envelopeSha256 !== receipt.envelopeSha256) {
813
+ throw new Error("Verification Envelope receipt envelope hash mismatch.");
814
+ }
815
+ for (const object of receipt.evidenceObjects ?? []) {
816
+ const objectPath = path.resolve(object.path);
817
+ if (!within(root, objectPath)) throw new Error("Verification evidence object escapes the workspace.");
818
+ assertNoSymlink(root, objectPath, "Verification evidence object");
819
+ const stored = JSON.parse(readFileSync(objectPath, "utf8"));
820
+ const bytes = Buffer.from(stored.data, stored.encoding);
821
+ if (bytes.length !== object.bytes || sha256(bytes) !== object.sha256) {
822
+ throw new Error(`Verification evidence object hash mismatch: ${object.label}.`);
823
+ }
824
+ }
825
+ for (const output of receipt.outputs ?? []) {
826
+ const outputPath = path.join(root, output.path);
827
+ assertNoSymlink(root, outputPath, "Verification output");
828
+ const stat = lstatSync(outputPath);
829
+ if (!stat.isFile() || stat.size !== output.bytes || sha256(readFileSync(outputPath)) !== output.sha256) {
830
+ throw new Error(`Verification output hash mismatch: ${output.path}.`);
831
+ }
832
+ }
833
+ const currentSource = snapshotPatterns(root, receipt.envelope.paths.immutableSource);
834
+ if (receipt.formalEligible && currentSource.sha256 !== receipt.source.afterSha256) {
835
+ throw new Error("Verification Envelope receipt source drifted after execution.");
836
+ }
837
+ return {
838
+ status: "verification_envelope_receipt_valid",
839
+ exitCode: 0,
840
+ receiptPath: absolute,
841
+ receiptSha256: receipt.receiptSha256,
842
+ envelopeSha256: receipt.envelopeSha256,
843
+ formalEligible: receipt.formalEligible,
844
+ authorizationGranted: false,
845
+ };
846
+ }