owlrunkit 0.25.0 → 0.25.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.
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import {
4
- closeSync, constants, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync,
4
+ accessSync, closeSync, constants, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync,
5
5
  mkdtempSync, openSync, readSync, readdirSync, realpathSync, unlinkSync,
6
6
  watch, writeFileSync,
7
7
  } from "node:fs";
@@ -9,6 +9,8 @@ import { tmpdir } from "node:os";
9
9
  import path from "node:path";
10
10
 
11
11
  import { RUNTIME_ROOT, currentCoreIdentity } from "./core-contract.mjs";
12
+ import { linuxRuntimeHelper } from "./linux-runtime.mjs";
13
+ import { BUBBLEWRAP_PEER_BACKEND, bubblewrapPeerArgs, probeBubblewrapPeer } from "./peer-linux-bwrap.mjs";
12
14
  import { safeIdentifier } from "./provenance-common.mjs";
13
15
  import { resolveVerificationEnvelopeBackendV1 } from "./verification-envelope.mjs";
14
16
 
@@ -195,9 +197,37 @@ function projection(task, state) {
195
197
  };
196
198
  }
197
199
 
200
+ export function resolvePeerRecoveryBackend() {
201
+ if (process.platform === "linux") {
202
+ try {
203
+ const executable = linuxRuntimeHelper();
204
+ if (!executable) return { available: false };
205
+ const result = spawnSync(executable, ["probe"], {
206
+ timeout: 3000, env: {}, stdio: ["ignore", "ignore", "pipe"], encoding: "utf8",
207
+ });
208
+ if (result.status === 0) {
209
+ return { available: true, id: "linux_landlock_seccomp_v1", executable,
210
+ digest: digest(regularBytes(executable, CHECKPOINT_LIMIT)) };
211
+ }
212
+ const reason = result.stderr?.trim() || result.error?.code;
213
+ if (result.status !== 77) return { available: false, reason };
214
+ const fallback = probeBubblewrapPeer(executable);
215
+ if (!fallback.available) {
216
+ return { available: false, reason: [reason, fallback.reason].filter(Boolean).join("; ") };
217
+ }
218
+ const helperHash = digest(regularBytes(executable, CHECKPOINT_LIMIT));
219
+ const sandboxHash = digest(regularBytes(fallback.executable, CHECKPOINT_LIMIT));
220
+ return { ...fallback, digest: digest(JSON.stringify([helperHash, sandboxHash])) };
221
+ } catch (error) { return { available: false, reason: error.code }; }
222
+ }
223
+ return resolveVerificationEnvelopeBackendV1();
224
+ }
225
+
198
226
  function requireBackend() {
199
- const backend = resolveVerificationEnvelopeBackendV1();
200
- if (!backend.available) throw new Error("peer_recovery_enforced_backend_unavailable");
227
+ const backend = resolvePeerRecoveryBackend();
228
+ if (!backend.available) {
229
+ throw new Error(["peer_recovery_enforced_backend_unavailable", backend.reason].filter(Boolean).join(": "));
230
+ }
201
231
  return backend;
202
232
  }
203
233
 
@@ -357,35 +387,69 @@ function reportExit(task, attemptId, processExit) {
357
387
  }
358
388
  }
359
389
 
360
- export async function runPeerTask({ workspaceRoot, taskId, agent, generation, command }) {
361
- const backend = requireBackend();
362
- safeIdentifier(agent, "current peer");
390
+ function workerExecutable(command) {
363
391
  if (!Array.isArray(command) || !path.isAbsolute(command[0] ?? "")) {
364
392
  throw new Error("peer_recovery_absolute_worker_required");
365
393
  }
366
394
  const executable = realpathSync(command[0]);
367
395
  if (!lstatSync(executable).isFile()) throw new Error("peer_recovery_worker_invalid");
396
+ accessSync(executable, constants.X_OK);
397
+ return executable;
398
+ }
399
+
400
+ function existingHandoff(task, state, agent, generation) {
401
+ if (state.owner !== agent || state.generation !== generation || state.phase === "ready") return null;
402
+ const result = projection(task, state);
403
+ return state.phase === "exited" ? { ...result, exitCode: 2 } : result;
404
+ }
405
+
406
+ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, command, handoff = false }) {
407
+ const backend = requireBackend();
408
+ safeIdentifier(agent, "current peer");
409
+ const executable = workerExecutable(command);
368
410
  const task = taskDirectory(workspaceRoot, taskId);
369
411
  const state = current(task);
370
412
  if (state.phase !== "ready" || state.owner !== agent || state.generation !== generation) {
413
+ const prior = handoff && existingHandoff(task, state, agent, generation);
414
+ if (prior) return prior;
371
415
  throw new Error("peer_recovery_attempt_not_available");
372
416
  }
373
417
  const id = randomUUID();
374
418
  const directory = mkdtempSync(path.join(realpathSync(tmpdir()), "owlrunkit-peer-"));
375
419
  materialize(checkpoint(task, state).files, directory);
376
- const attempt = { id, directory, backend: backend.id };
420
+ const attempt = { id, directory, backend: backend.id,
421
+ ...(backend.digest ? { backendSha256: backend.digest } : {}) };
377
422
  const claimed = append(task, state, {
378
423
  attempt, phase: "running", processExit: null, event: "peer_attempt_started",
379
424
  });
380
- if (!claimed) throw new Error("peer_recovery_attempt_already_claimed");
425
+ if (!claimed) {
426
+ const prior = handoff && existingHandoff(task, current(task), agent, generation);
427
+ if (prior) return prior;
428
+ throw new Error("peer_recovery_attempt_already_claimed");
429
+ }
381
430
 
382
431
  return new Promise((resolve, reject) => {
383
- const child = spawn(backend.executable, ["-p", sandboxPolicy(executable, directory),
384
- executable, ...command.slice(1)], {
385
- cwd: directory, env: {
386
- PATH: "/usr/bin:/bin", TMPDIR: directory, OWLRUNKIT_PEER_ATTEMPT: id,
387
- OWLRUNKIT_PEER_PATHS: JSON.stringify(state.paths),
388
- }, stdio: ["pipe", "pipe", "pipe"],
432
+ const environment = {
433
+ PATH: "/usr/bin:/bin", TMPDIR: directory, OWLRUNKIT_PEER_ATTEMPT: id,
434
+ OWLRUNKIT_PEER_PATHS: JSON.stringify(state.paths),
435
+ };
436
+ let args;
437
+ let env = environment;
438
+ if (backend.id === BUBBLEWRAP_PEER_BACKEND) {
439
+ args = bubblewrapPeerArgs({
440
+ helper: backend.helper, directory, worker: executable,
441
+ args: command.slice(1), environment,
442
+ });
443
+ env = {};
444
+ } else if (backend.id === "linux_landlock_seccomp_v1") {
445
+ args = ["run", String(process.pid), directory, executable, ...command.slice(1)];
446
+ // An isolated worker cannot read host OpenSSL configuration.
447
+ env.OPENSSL_CONF = "/dev/null";
448
+ } else {
449
+ args = ["-p", sandboxPolicy(executable, directory), executable, ...command.slice(1)];
450
+ }
451
+ const child = spawn(backend.executable, args, {
452
+ cwd: directory, env, stdio: ["pipe", "pipe", "pipe"],
389
453
  });
390
454
  let watcher, pending = Buffer.alloc(0), sentWake, protocolError = null;
391
455
  let diagnostic = Buffer.alloc(0);
@@ -440,3 +504,12 @@ export async function runPeerTask({ workspaceRoot, taskId, agent, generation, co
440
504
  } catch (error) { stop(error); }
441
505
  });
442
506
  }
507
+
508
+ export async function recoverAndRunPeerTask(options, command) {
509
+ workerExecutable(command);
510
+ const recovered = await recoverPeerTask(options);
511
+ if (recovered.generation !== options.generation + 1 || recovered.owner !== options.agent) {
512
+ return recovered;
513
+ }
514
+ return runPeerTask({ ...options, generation: recovered.generation, command, handoff: true });
515
+ }
@@ -27,10 +27,18 @@ const SELF_NORMALIZED_QUICK_CORE_VERSIONS = new Set([
27
27
  "0.24.0",
28
28
  "0.24.1",
29
29
  "0.25.0",
30
+ "0.25.1",
31
+ "0.25.2",
30
32
  ]);
31
33
  const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
32
34
  const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
33
35
  const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
36
+ ["0.25.1", new Set([
37
+ "sha256:0934a076b147847a4623bc89263b677c88088b493495fc74f11485985b7903b5",
38
+ ])],
39
+ ["0.25.0", new Set([
40
+ "sha256:20e75c386f1408f11e908a6d3ee1a39316d9728d5719fa06f8d6a5aa45abd83e",
41
+ ])],
34
42
  ["0.24.1", new Set([
35
43
  "sha256:94aaa2e3a6d504374fa78cfcd341e31b33087806c9ddf5f84b3d3b47a6328e08",
36
44
  ])],
@@ -20,6 +20,7 @@ import {
20
20
  validateExecutionPin,
21
21
  validateLeaseOwnedPaths,
22
22
  } from "./core-contract.mjs";
23
+ import { acquireRunKitControlLock } from "./control-transaction-lock.mjs";
23
24
  import { buildCoverageIndex } from "./coverage-adoption.mjs";
24
25
  import { validateVerificationReceiptGate } from "./verification-receipt-gate.mjs";
25
26
  import {
@@ -103,14 +104,7 @@ function acquireResumeLock(workspaceRoot) {
103
104
  const runtimeRoot = path.join(workspaceRoot, RUNTIME_ROOT);
104
105
  assertSafeDirectory(workspaceRoot, runtimeRoot, "RunKit runtime root");
105
106
  assertSafeDirectory(runtimeRoot, path.join(runtimeRoot, "executions"), "Executions directory");
106
- const lockPath = path.join(runtimeRoot, "control.lock");
107
- try {
108
- mkdirSync(lockPath);
109
- } catch (error) {
110
- if (error?.code === "EEXIST") throw new Error("Another RunKit control transaction is already active.");
111
- throw error;
112
- }
113
- return () => rmSync(lockPath, { recursive: true, force: true });
107
+ return acquireRunKitControlLock(runtimeRoot);
114
108
  }
115
109
 
116
110
  function activeRunIds(workspaceRoot) {
@@ -95,6 +95,7 @@ import { buildHookRecoveryV1 } from "./hook-recovery.mjs";
95
95
  import {
96
96
  createPeerRecoveryTask,
97
97
  inspectPeerRecoveryTask,
98
+ recoverAndRunPeerTask,
98
99
  recoverPeerTask,
99
100
  runPeerTask,
100
101
  } from "./peer-recovery.mjs";
@@ -2454,7 +2455,7 @@ const PEER_HELP = `Usage:
2454
2455
  owlrunkit peer create --workspace <dir> --task <id> --request <json>
2455
2456
  owlrunkit peer inspect --workspace <dir> --task <id>
2456
2457
  owlrunkit peer run --workspace <dir> --task <id> --agent <id> --generation <n> -- /absolute/worker [args]
2457
- owlrunkit peer recover --workspace <dir> --task <id> --agent <id> --generation <n> --revision <n> [--wait-ms <n>]
2458
+ owlrunkit peer recover --workspace <dir> --task <id> --agent <id> --generation <n> --revision <n> [--wait-ms <n>] [-- /absolute/worker [args]]
2458
2459
  `;
2459
2460
 
2460
2461
  function runPeerCommand(rest) {
@@ -2468,10 +2469,10 @@ function runPeerCommand(rest) {
2468
2469
  create: ["request"], inspect: [], run: ["agent", "generation"],
2469
2470
  recover: ["agent", "generation", "revision", "wait-ms"],
2470
2471
  };
2471
- if (!Object.hasOwn(flags, action) || (separator !== -1 && action !== "run")) {
2472
+ if (!Object.hasOwn(flags, action) || (separator !== -1 && !["run", "recover"].includes(action))) {
2472
2473
  throw new Error(PEER_HELP);
2473
2474
  }
2474
- const options = parseOptions(optionArgs);
2475
+ const options = parseOptions(optionArgs, { boolean: ["json"] });
2475
2476
  const allowed = new Set(["workspace", "task", "json", ...flags[action]]);
2476
2477
  if (Object.keys(options).some(key => !allowed.has(key))) throw new Error(PEER_HELP);
2477
2478
  const task = { workspaceRoot: workspace(options), taskId: requireOption(options, "task") };
@@ -2489,11 +2490,13 @@ function runPeerCommand(rest) {
2489
2490
  if (separator === -1) throw new Error(PEER_HELP);
2490
2491
  return runPeerTask({ ...peer, command: args.slice(separator + 1) });
2491
2492
  }
2492
- return recoverPeerTask({ ...peer,
2493
+ const recovery = { ...peer,
2493
2494
  revision: parseCanonicalNonnegativeSafeInteger(requireOption(options, "revision"), "revision"),
2494
2495
  waitMs: options["wait-ms"] === undefined ? undefined
2495
2496
  : parseCanonicalNonnegativeSafeInteger(options["wait-ms"], "wait-ms"),
2496
- });
2497
+ };
2498
+ return separator === -1 ? recoverPeerTask(recovery)
2499
+ : recoverAndRunPeerTask(recovery, args.slice(separator + 1));
2497
2500
  }
2498
2501
 
2499
2502
  export async function runCli(argv = process.argv.slice(2), runtimeHooks = {}) {