dahrk-node 0.1.10 → 0.1.12

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 (2) hide show
  1. package/dist/main.js +721 -191
  2. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/main.ts
4
- import { execFileSync as execFileSync8 } from "child_process";
5
- import { existsSync as existsSync12, realpathSync as realpathSync4 } from "fs";
4
+ import { execFileSync as execFileSync9 } from "child_process";
5
+ import { existsSync as existsSync13, realpathSync as realpathSync5 } from "fs";
6
6
  import { randomUUID as randomUUID3 } from "crypto";
7
- import { homedir as homedir5, platform as osPlatform4 } from "os";
8
- import { basename as basename2, join as join14 } from "path";
7
+ import { homedir as homedir7, platform as osPlatform4 } from "os";
8
+ import { basename as basename2, join as join16 } from "path";
9
9
  import { pathToFileURL } from "url";
10
10
 
11
11
  // ../../packages/edge/src/ws-client.ts
@@ -67,6 +67,8 @@ function createMockRunner(runtime) {
67
67
  }
68
68
 
69
69
  // ../../packages/executor-worktree/src/claude-adapter.ts
70
+ import { homedir, tmpdir } from "os";
71
+ import { join as join2 } from "path";
70
72
  import {
71
73
  query
72
74
  } from "@anthropic-ai/claude-agent-sdk";
@@ -336,7 +338,7 @@ var ManagedMailbox = class {
336
338
  const v = this.q.shift();
337
339
  if (v !== void 0) return Promise.resolve({ value: v, done: false });
338
340
  if (this.done) return Promise.resolve({ value: void 0, done: true });
339
- return new Promise((resolve2) => this.waiters.push(resolve2));
341
+ return new Promise((resolve3) => this.waiters.push(resolve3));
340
342
  }
341
343
  };
342
344
  }
@@ -347,7 +349,7 @@ function interactiveIdleWindows(ctx) {
347
349
  return { firstReplyMs: Math.max(firstReplyMs, idleMs), idleMs };
348
350
  }
349
351
  function raceNextTurn(pending, idleMs, signal) {
350
- return new Promise((resolve2) => {
352
+ return new Promise((resolve3) => {
351
353
  let settled = false;
352
354
  let timer;
353
355
  function finish(r) {
@@ -355,7 +357,7 @@ function raceNextTurn(pending, idleMs, signal) {
355
357
  settled = true;
356
358
  if (timer) clearTimeout(timer);
357
359
  signal.removeEventListener("abort", onAbort);
358
- resolve2(r);
360
+ resolve3(r);
359
361
  }
360
362
  function onAbort() {
361
363
  finish({ kind: "cancelled" });
@@ -391,7 +393,7 @@ function createElicitTurnRouter(turns, opts) {
391
393
  const ask = (firstReply, onRaise) => {
392
394
  if (ref.settle) return Promise.resolve({ kind: "busy" });
393
395
  onRaise();
394
- return new Promise((resolve2) => {
396
+ return new Promise((resolve3) => {
395
397
  let settled = false;
396
398
  const finish = (o) => {
397
399
  if (settled) return;
@@ -399,7 +401,7 @@ function createElicitTurnRouter(turns, opts) {
399
401
  clearTimeout(timer);
400
402
  opts.signal.removeEventListener("abort", onAbort);
401
403
  ref.settle = null;
402
- resolve2(o);
404
+ resolve3(o);
403
405
  };
404
406
  const onAbort = () => finish({ kind: "cancel" });
405
407
  const timer = setTimeout(() => finish({ kind: "noreply" }), firstReply ? opts.firstReplyMs : opts.idleMs);
@@ -468,18 +470,21 @@ function toElicitQuestion(q) {
468
470
  ${lines.join("\n")}` : q.question;
469
471
  return { prompt, options, ...q.multiSelect ? { multiSelect: true } : {} };
470
472
  }
473
+ function buildElicitFromQuestions(questions) {
474
+ const first = questions[0];
475
+ const q = toElicitQuestion(first);
476
+ const prompt = questions.length > 1 ? `${q.prompt}
477
+
478
+ (Note: ${questions.length} questions were asked at once; answer this one first, then ask the rest.)` : q.prompt;
479
+ return { ...q, prompt };
480
+ }
471
481
  function createAskUserQuestionTool(deps) {
472
482
  const askTool = tool2(
473
483
  "ask_user_question",
474
484
  "Ask the human a structured multiple-choice question and wait for their selection. Use this when you need the human to choose between options before you can continue.",
475
485
  { questions: z2.array(questionSchema).min(1) },
476
486
  async (args) => {
477
- const first = args.questions[0];
478
- const question = toElicitQuestion(first);
479
- const prompt = args.questions.length > 1 ? `${question.prompt}
480
-
481
- (Note: ${args.questions.length} questions were asked at once; answer this one first, then ask the rest.)` : question.prompt;
482
- const text = await deps.ask({ ...question, prompt });
487
+ const text = await deps.ask(buildElicitFromQuestions(args.questions));
483
488
  return { content: [{ type: "text", text }] };
484
489
  }
485
490
  );
@@ -518,6 +523,26 @@ function buildBrokeredMcpServers(ctx) {
518
523
  for (const s of servers) entries[s.id] = { type: s.type, url: `${ctx.mcpProxyBaseUrl}/${s.id}` };
519
524
  return entries;
520
525
  }
526
+ function sandboxOptions(ctx) {
527
+ if (process.env.DAHRK_SANDBOX !== "1") return {};
528
+ const home = homedir();
529
+ return {
530
+ sandbox: {
531
+ enabled: true,
532
+ failIfUnavailable: false,
533
+ autoAllowBashIfSandboxed: false,
534
+ allowUnsandboxedCommands: false,
535
+ filesystem: {
536
+ allowWrite: [ctx.workspace.worktreePath, ctx.workspace.scratchPath, tmpdir()],
537
+ denyRead: [join2(home, ".ssh"), join2(home, ".aws"), join2(home, ".gnupg"), "/Volumes"]
538
+ }
539
+ }
540
+ };
541
+ }
542
+ function runtimeEnvOptions(ctx) {
543
+ if (!ctx.runtimeEnv) return {};
544
+ return { env: { ...process.env, ...ctx.runtimeEnv } };
545
+ }
521
546
  function createClaudeRunner() {
522
547
  const abortController = new AbortController();
523
548
  let cancelled = false;
@@ -533,7 +558,25 @@ function createClaudeRunner() {
533
558
  // is a Claude Code SETTINGS key (not a top-level Options field), so it rides the inline `settings`
534
559
  // object, which sits at the flag layer and overrides any project/local setting. Mirrors the cyrus
535
560
  // reference (EdgeWorker.ts writes the same key into .claude/settings.local.json).
536
- settings: { includeCoAuthoredBy: false },
561
+ settings: {
562
+ includeCoAuthoredBy: false,
563
+ // DHK-392, defence in depth. The stage runner's `fs_confine` builtin is the real block (it is
564
+ // what `canUseTool` below consults, and unlike these rules it covers Grep and Bash). These deny
565
+ // rules close the same door from inside Claude Code, for the tools its permission system does
566
+ // cover (Read/Glob/NotebookRead via `Read(...)`, Write/Edit via `Edit(...)`): credentials an
567
+ // agent has no business reading, and the mounted volumes whose scan started all this. Deny
568
+ // outranks allow, so a repo's own .claude/settings.json cannot undo them.
569
+ permissions: {
570
+ deny: [
571
+ "Read(//Volumes/**)",
572
+ "Read(~/.ssh/**)",
573
+ "Read(~/.aws/**)",
574
+ "Read(~/.gnupg/**)",
575
+ "Read(~/Library/Keychains/**)"
576
+ ]
577
+ }
578
+ },
579
+ ...sandboxOptions(ctx),
537
580
  // Inherit the REPO's .mcp.json / .claude settings (build spec section 9): do NOT set
538
581
  // strictMcpConfig. Policy enforcement around tools is M6; M4 allows tools to run and the
539
582
  // stage runner intercepts denied actions at the trace level.
@@ -543,6 +586,8 @@ function createClaudeRunner() {
543
586
  // .claude/ + CLAUDE.md + .mcp.json, which is all section 9 actually requires. Claude auth is
544
587
  // keychain/OAuth and independent of settingSources, so dropping "user" does not affect it.
545
588
  settingSources: ["project", "local"],
589
+ // Brokered inference env (DHK-89), for a managed / Docker-isolated node with no ambient login.
590
+ ...runtimeEnvOptions(ctx),
546
591
  ...ctx.config.model ? { model: ctx.config.model } : {},
547
592
  ...ctx.sessionId ? { resume: ctx.sessionId } : {},
548
593
  ...ctx.config.skill ? { skills: [ctx.config.skill] } : {}
@@ -857,6 +902,12 @@ function mapUsage2(u) {
857
902
 
858
903
  // ../../packages/executor-worktree/src/codex-adapter.ts
859
904
  var COALESCE_MS2 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
905
+ function runtimeEnvOptions2(ctx) {
906
+ if (!ctx.runtimeEnv) return {};
907
+ const env = {};
908
+ for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
909
+ return { env: { ...env, ...ctx.runtimeEnv } };
910
+ }
860
911
  function createCodexRunner() {
861
912
  const abortController = new AbortController();
862
913
  const signal = abortController.signal;
@@ -870,7 +921,7 @@ function createCodexRunner() {
870
921
  ...ctx.config.model ? { model: ctx.config.model } : {}
871
922
  });
872
923
  const openThread = (ctx) => {
873
- const codex = new Codex();
924
+ const codex = new Codex(runtimeEnvOptions2(ctx));
874
925
  const t = ctx.sessionId ? codex.resumeThread(ctx.sessionId, threadOptions(ctx)) : codex.startThread(threadOptions(ctx));
875
926
  thread = t;
876
927
  return t;
@@ -1332,8 +1383,8 @@ var PROVIDER_BY_ENV = {
1332
1383
  // ../../packages/executor-worktree/src/git-service.ts
1333
1384
  import { execFileSync } from "child_process";
1334
1385
  import { existsSync, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1335
- import { homedir, tmpdir } from "os";
1336
- import { basename, dirname, isAbsolute, join as join2 } from "path";
1386
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1387
+ import { basename, dirname, isAbsolute, join as join3 } from "path";
1337
1388
  var noopLogger = { info: () => {
1338
1389
  }, warn: () => {
1339
1390
  } };
@@ -1347,10 +1398,10 @@ function sanitizeBranchName(name) {
1347
1398
  return name.replace(/[`~^:?*[\]\\@{}\s]/g, "-").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/\.lock(\/|$)/g, "$1").replace(/^[.\-/]+/, "").replace(/[.\-/]+$/, "").replace(/-{2,}/g, "-");
1348
1399
  }
1349
1400
  function resolveWorktreesDir(override) {
1350
- return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join2(homedir(), ".dahrk", "worktrees");
1401
+ return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join3(homedir2(), ".dahrk", "worktrees");
1351
1402
  }
1352
1403
  function resolveMirrorsDir(override) {
1353
- return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join2(homedir(), ".dahrk", "mirrors");
1404
+ return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join3(homedir2(), ".dahrk", "mirrors");
1354
1405
  }
1355
1406
  function createGitService(opts = {}) {
1356
1407
  const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
@@ -1389,12 +1440,12 @@ function createGitService(opts = {}) {
1389
1440
  const entry = `${SCRATCH_DIR}/`;
1390
1441
  try {
1391
1442
  const rel = git(worktreePath, ["rev-parse", "--git-path", "info/exclude"]).trim();
1392
- const excludePath = isAbsolute(rel) ? rel : join2(worktreePath, rel);
1443
+ const excludePath = isAbsolute(rel) ? rel : join3(worktreePath, rel);
1393
1444
  const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
1394
1445
  if (existing.split("\n").some((l) => l.trim() === entry)) return;
1395
1446
  mkdirSync(dirname(excludePath), { recursive: true });
1396
- const sep3 = existing && !existing.endsWith("\n") ? "\n" : "";
1397
- writeFileSync(excludePath, `${existing}${sep3}${entry}
1447
+ const sep4 = existing && !existing.endsWith("\n") ? "\n" : "";
1448
+ writeFileSync(excludePath, `${existing}${sep4}${entry}
1398
1449
  `);
1399
1450
  } catch (e) {
1400
1451
  log.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
@@ -1419,8 +1470,8 @@ function createGitService(opts = {}) {
1419
1470
  return { headSha: git(worktreePath, ["rev-parse", "HEAD"]).trim(), dirty };
1420
1471
  };
1421
1472
  const setupAuth = (token) => {
1422
- const dir = mkdtempSync(join2(tmpdir(), "dahrk-cred-"));
1423
- const script = join2(dir, "askpass.sh");
1473
+ const dir = mkdtempSync(join3(tmpdir2(), "dahrk-cred-"));
1474
+ const script = join3(dir, "askpass.sh");
1424
1475
  writeFileSync(script, '#!/bin/sh\nprintf "%s" "$DAHRK_GIT_TOKEN"\n', { mode: 448 });
1425
1476
  return {
1426
1477
  env: { ...process.env, GIT_ASKPASS: script, DAHRK_GIT_TOKEN: token, GIT_TERMINAL_PROMPT: "0" },
@@ -1429,7 +1480,7 @@ function createGitService(opts = {}) {
1429
1480
  };
1430
1481
  const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
1431
1482
  const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
1432
- const mirrorPathFor = (repoId) => join2(mirrorsDir, sanitizeBranchName(repoId));
1483
+ const mirrorPathFor = (repoId) => join3(mirrorsDir, sanitizeBranchName(repoId));
1433
1484
  const listWorktrees = (mirror) => {
1434
1485
  let out;
1435
1486
  try {
@@ -1540,18 +1591,18 @@ function createGitService(opts = {}) {
1540
1591
  repo: spec.repo ?? spec.repoId,
1541
1592
  baseBranch: spec.baseBranch,
1542
1593
  worktreePath,
1543
- scratchPath: join2(worktreePath, ".skakel", "scratch")
1594
+ scratchPath: join3(worktreePath, ".skakel", "scratch")
1544
1595
  });
1545
1596
  return {
1546
1597
  worktreesDir,
1547
1598
  async createWorktree(spec) {
1548
1599
  const { repoId, gitUrl, baseBranch, runId } = spec;
1549
1600
  const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
1550
- const worktreePath = join2(worktreesDir, runId);
1601
+ const worktreePath = join3(worktreesDir, runId);
1551
1602
  mkdirSync(worktreesDir, { recursive: true });
1552
1603
  if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
1553
1604
  log.info(`reusing existing worktree at ${worktreePath}`);
1554
- mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
1605
+ mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1555
1606
  return refFor(spec, worktreePath);
1556
1607
  }
1557
1608
  const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
@@ -1587,7 +1638,7 @@ function createGitService(opts = {}) {
1587
1638
  if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
1588
1639
  throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
1589
1640
  }
1590
- mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
1641
+ mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1591
1642
  return refFor(spec, worktreePath);
1592
1643
  },
1593
1644
  async commitAndPush(ref, opts2) {
@@ -1693,8 +1744,8 @@ function createGitService(opts = {}) {
1693
1744
  return void 0;
1694
1745
  }
1695
1746
  };
1696
- const tmp = mkdtempSync(join2(tmpdir(), "dahrk-pr-"));
1697
- const bodyFile = join2(tmp, "body.md");
1747
+ const tmp = mkdtempSync(join3(tmpdir2(), "dahrk-pr-"));
1748
+ const bodyFile = join3(tmp, "body.md");
1698
1749
  try {
1699
1750
  writeFileSync(bodyFile, opts2.body ?? "");
1700
1751
  try {
@@ -1730,7 +1781,7 @@ function createGitService(opts = {}) {
1730
1781
  // ../../packages/executor-worktree/src/worktree-reaper.ts
1731
1782
  import { execFileSync as execFileSync2 } from "child_process";
1732
1783
  import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync2, statSync } from "fs";
1733
- import { join as join3 } from "path";
1784
+ import { join as join4 } from "path";
1734
1785
  var MINUTE = 6e4;
1735
1786
  var HOUR = 60 * MINUTE;
1736
1787
  var DEFAULTS = {
@@ -1760,7 +1811,7 @@ var canonical = (p) => {
1760
1811
  }
1761
1812
  };
1762
1813
  function lastUsedMs(worktreePath) {
1763
- const candidates = [join3(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
1814
+ const candidates = [join4(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
1764
1815
  let newest = 0;
1765
1816
  for (const p of candidates) {
1766
1817
  try {
@@ -1783,7 +1834,7 @@ function createWorktreeReaper(opts) {
1783
1834
  const log = opts.logger ?? noop;
1784
1835
  const mirrors = () => {
1785
1836
  try {
1786
- return readdirSync(opts.mirrorsDir).map((d) => join3(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
1837
+ return readdirSync(opts.mirrorsDir).map((d) => join4(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
1787
1838
  } catch {
1788
1839
  return [];
1789
1840
  }
@@ -1807,7 +1858,7 @@ function createWorktreeReaper(opts) {
1807
1858
  }
1808
1859
  const onDisk = (() => {
1809
1860
  try {
1810
- return readdirSync(opts.worktreesDir).map((d) => canonical(join3(opts.worktreesDir, d)));
1861
+ return readdirSync(opts.worktreesDir).map((d) => canonical(join4(opts.worktreesDir, d)));
1811
1862
  } catch {
1812
1863
  return [];
1813
1864
  }
@@ -1868,15 +1919,15 @@ function createWorktreeReaper(opts) {
1868
1919
  // ../../packages/executor-worktree/src/trace-writer.ts
1869
1920
  import { createHash } from "crypto";
1870
1921
  import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1871
- import { join as join4 } from "path";
1922
+ import { join as join5 } from "path";
1872
1923
  var DEFAULT_SPILL_BYTES = 8192;
1873
1924
  function createTraceWriter(scratchPath, meta, opts = {}) {
1874
1925
  const spillBytes = opts.spillBytes ?? DEFAULT_SPILL_BYTES;
1875
- const dir = join4(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
1876
- mkdirSync2(join4(dir, "blobs"), { recursive: true });
1877
- mkdirSync2(join4(dir, "raw"), { recursive: true });
1878
- const tracePath = join4(dir, "trace.jsonl");
1879
- const metaPath = join4(dir, "meta.json");
1926
+ const dir = join5(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
1927
+ mkdirSync2(join5(dir, "blobs"), { recursive: true });
1928
+ mkdirSync2(join5(dir, "raw"), { recursive: true });
1929
+ const tracePath = join5(dir, "trace.jsonl");
1930
+ const metaPath = join5(dir, "meta.json");
1880
1931
  let current = { ...meta };
1881
1932
  let nextSeq = 0;
1882
1933
  let rawCount = 0;
@@ -1888,8 +1939,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1888
1939
  const spillValue = (value) => {
1889
1940
  const data = typeof value === "string" ? value : JSON.stringify(value);
1890
1941
  const sha = createHash("sha256").update(data).digest("hex");
1891
- writeFileSync2(join4(dir, "blobs", sha), data);
1892
- return join4("blobs", sha);
1942
+ writeFileSync2(join5(dir, "blobs", sha), data);
1943
+ return join5("blobs", sha);
1893
1944
  };
1894
1945
  const spill = (event) => {
1895
1946
  if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
@@ -1919,8 +1970,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1919
1970
  return written;
1920
1971
  },
1921
1972
  writeRaw(record) {
1922
- const rel = join4("raw", `${rawCount++}.json`);
1923
- writeFileSync2(join4(dir, rel), JSON.stringify(record, null, 2));
1973
+ const rel = join5("raw", `${rawCount++}.json`);
1974
+ writeFileSync2(join5(dir, rel), JSON.stringify(record, null, 2));
1924
1975
  return rel;
1925
1976
  },
1926
1977
  finalise(patch = {}) {
@@ -1936,12 +1987,12 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1936
1987
  // ../../packages/executor-worktree/src/pack-cache.ts
1937
1988
  import { createHash as createHash2 } from "crypto";
1938
1989
  import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
1939
- import { dirname as dirname2, join as join5, relative, sep } from "path";
1990
+ import { dirname as dirname2, join as join6, relative, sep } from "path";
1940
1991
  function readManifestFiles(dir) {
1941
1992
  const out = [];
1942
1993
  const walk = (cur) => {
1943
1994
  for (const entry of readdirSync2(cur, { withFileTypes: true })) {
1944
- const abs = join5(cur, entry.name);
1995
+ const abs = join6(cur, entry.name);
1945
1996
  if (entry.isDirectory()) walk(abs);
1946
1997
  else out.push(relative(dir, abs).split(sep).join("/"));
1947
1998
  }
@@ -1952,7 +2003,7 @@ function readManifestFiles(dir) {
1952
2003
 
1953
2004
  // ../../packages/executor-worktree/src/overlay.ts
1954
2005
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
1955
- import { dirname as dirname3, join as join6 } from "path";
2006
+ import { dirname as dirname3, join as join7 } from "path";
1956
2007
  function sameBytes(dest, bytes) {
1957
2008
  try {
1958
2009
  return readFileSync3(dest).equals(bytes);
@@ -1972,8 +2023,8 @@ async function overlayComponents(opts) {
1972
2023
  }
1973
2024
  const { dir } = await cache.materialise(ref);
1974
2025
  for (const relPath of readManifestFiles(dir)) {
1975
- const src = join6(dir, relPath);
1976
- const dest = join6(worktreePath, relPath);
2026
+ const src = join7(dir, relPath);
2027
+ const dest = join7(worktreePath, relPath);
1977
2028
  const bytes = readFileSync3(src);
1978
2029
  if (existsSync4(dest)) {
1979
2030
  if (sameBytes(dest, bytes)) continue;
@@ -2160,7 +2211,7 @@ function ceilingFromEnv(env) {
2160
2211
 
2161
2212
  // ../../packages/edge/src/logger.ts
2162
2213
  import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync, renameSync, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
2163
- import { join as join7 } from "path";
2214
+ import { join as join8 } from "path";
2164
2215
  import pino from "pino";
2165
2216
 
2166
2217
  // ../../packages/edge/src/redact.ts
@@ -2350,7 +2401,7 @@ function createNodeLogger(opts = {}) {
2350
2401
  if (opts.dir && fileLevel !== "silent") {
2351
2402
  try {
2352
2403
  mkdirSync5(opts.dir, { recursive: true, mode: 448 });
2353
- const file = new RotatingFile(join7(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
2404
+ const file = new RotatingFile(join8(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
2354
2405
  streams.push({ level: fileLevel, stream: file });
2355
2406
  } catch (e) {
2356
2407
  process.stderr.write(`dahrk: file logging disabled (${e.message})
@@ -2406,15 +2457,377 @@ function denyToolRule(tool3) {
2406
2457
  }
2407
2458
 
2408
2459
  // ../../packages/edge/src/stage-runner.ts
2409
- import { execFileSync as execFileSync4 } from "child_process";
2460
+ import { execFileSync as execFileSync5 } from "child_process";
2410
2461
  import { createHash as createHash3 } from "crypto";
2411
2462
  import { mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
2412
- import { tmpdir as tmpdir2 } from "os";
2413
- import { isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve, sep as sep2 } from "path";
2463
+ import { tmpdir as tmpdir4 } from "os";
2464
+ import { isAbsolute as isAbsolute3, join as join10, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
2414
2465
  import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
2415
2466
 
2416
2467
  // ../../packages/edge/src/builtins.ts
2468
+ import { execFileSync as execFileSync4 } from "child_process";
2469
+
2470
+ // ../../packages/edge/src/fs-roots.ts
2417
2471
  import { execFileSync as execFileSync3 } from "child_process";
2472
+ import { existsSync as existsSync6, realpathSync as realpathSync2 } from "fs";
2473
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
2474
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve, sep as sep2 } from "path";
2475
+ function isUnder(root, target) {
2476
+ const rel = relative2(resolve(root), resolve(target));
2477
+ return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
2478
+ }
2479
+ function expandPath(raw, cwd) {
2480
+ const home = homedir3();
2481
+ const expanded = raw.replace(/^~(?=$|\/)/, home).replace(/^\$\{HOME\}(?=$|\/)/, home).replace(/^\$HOME(?=$|\/)/, home);
2482
+ return resolve(cwd, expanded);
2483
+ }
2484
+ function realish(p) {
2485
+ let head = p;
2486
+ const tail = [];
2487
+ while (head !== dirname4(head)) {
2488
+ if (existsSync6(head)) {
2489
+ try {
2490
+ return join9(realpathSync2.native(head), ...tail.reverse());
2491
+ } catch {
2492
+ return p;
2493
+ }
2494
+ }
2495
+ tail.push(head.slice(dirname4(head).length + 1));
2496
+ head = dirname4(head);
2497
+ }
2498
+ return p;
2499
+ }
2500
+ function withinRoots(raw, roots, need) {
2501
+ const literal = expandPath(raw, roots.cwd);
2502
+ const real = realish(literal);
2503
+ const hits = (list2) => list2.some((root) => isUnder(root, literal) || isUnder(root, real));
2504
+ if (hits(roots.deny)) return false;
2505
+ if (hits(roots.rw)) return true;
2506
+ return need === "read" && hits(roots.ro);
2507
+ }
2508
+ function gitCommonDir(worktreePath) {
2509
+ try {
2510
+ const out = execFileSync3("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
2511
+ cwd: worktreePath,
2512
+ stdio: ["ignore", "pipe", "ignore"]
2513
+ }).toString().trim();
2514
+ return out || void 0;
2515
+ } catch {
2516
+ return void 0;
2517
+ }
2518
+ }
2519
+ var pnpmStoreCache;
2520
+ function pnpmStore() {
2521
+ if (pnpmStoreCache) return pnpmStoreCache.path;
2522
+ try {
2523
+ const out = execFileSync3("pnpm", ["store", "path"], { stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).toString().trim();
2524
+ pnpmStoreCache = { path: out || void 0 };
2525
+ } catch {
2526
+ pnpmStoreCache = { path: void 0 };
2527
+ }
2528
+ return pnpmStoreCache.path;
2529
+ }
2530
+ var splitRoots = (v) => (v ?? "").split(":").map((s) => s.trim()).filter(Boolean);
2531
+ function computeFsRoots(opts) {
2532
+ const home = homedir3();
2533
+ const wt = realish(resolve(opts.worktreePath));
2534
+ const rw = [
2535
+ wt,
2536
+ ...opts.scratchPath ? [realish(resolve(opts.scratchPath))] : [],
2537
+ // Every git command in the worktree reads and writes here.
2538
+ ...[gitCommonDir(opts.worktreePath)].filter((p) => Boolean(p)).map(realish),
2539
+ // Scratch space: `mkdtemp`, git's temp files, and an agent tidying a throwaway dir it created.
2540
+ ...[tmpdir3(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
2541
+ // The safe I/O sinks. `2>/dev/null` is on a third of the shell commands real stages run; treating
2542
+ // it as a write outside the worktree would deny most of a normal build. The raw devices are NOT
2543
+ // here, so `> /dev/sda` still fails confinement (as well as shell_guard's device-write regex).
2544
+ ...["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/fd"],
2545
+ ...[pnpmStore()].filter((p) => Boolean(p)).map(realish),
2546
+ ...splitRoots(process.env.DAHRK_FS_EXTRA_ROOTS).map((p) => realish(resolve(p)))
2547
+ ];
2548
+ const ro = [
2549
+ "/usr",
2550
+ "/bin",
2551
+ "/sbin",
2552
+ "/etc",
2553
+ "/opt",
2554
+ "/nix",
2555
+ "/Library",
2556
+ "/System",
2557
+ "/proc",
2558
+ "/sys",
2559
+ join9(home, ".gitconfig"),
2560
+ join9(home, ".config"),
2561
+ join9(home, ".npmrc"),
2562
+ join9(home, ".cache"),
2563
+ join9(home, "Library", "Caches"),
2564
+ join9(home, "Library", "pnpm"),
2565
+ join9(home, ".local", "share"),
2566
+ join9(home, ".nvm"),
2567
+ join9(home, ".volta"),
2568
+ join9(home, ".asdf"),
2569
+ join9(home, ".cargo"),
2570
+ join9(home, ".rustup"),
2571
+ ...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
2572
+ ].map(realish);
2573
+ const deny = [
2574
+ join9(home, ".ssh"),
2575
+ join9(home, ".aws"),
2576
+ join9(home, ".gnupg"),
2577
+ join9(home, ".config", "gcloud"),
2578
+ join9(home, "Library", "Keychains"),
2579
+ "/Volumes",
2580
+ "/etc/shadow",
2581
+ "/etc/sudoers"
2582
+ ];
2583
+ return { rw, ro, deny, cwd: wt };
2584
+ }
2585
+
2586
+ // ../../packages/edge/src/shell-scan.ts
2587
+ var PATTERN_FLAGS = /* @__PURE__ */ new Set([
2588
+ "-e",
2589
+ "--regexp",
2590
+ "-path",
2591
+ "-ipath",
2592
+ "-name",
2593
+ "-iname",
2594
+ "-wholename",
2595
+ "--glob",
2596
+ "-g",
2597
+ "--include",
2598
+ "--exclude",
2599
+ "--exclude-dir",
2600
+ "--type",
2601
+ "-m",
2602
+ "--message"
2603
+ ]);
2604
+ var PATTERN_FIRST = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "ag", "ack", "sed", "awk", "perl"]);
2605
+ var DIR_FLAGS = /* @__PURE__ */ new Set(["-C", "--git-dir", "--work-tree", "--cwd", "--directory"]);
2606
+ var NO_PATH_ARGV0 = /* @__PURE__ */ new Set(["echo", "printf", ":", "true", "false"]);
2607
+ var WRITE_ALL = /* @__PURE__ */ new Set(["rm", "rmdir", "mkdir", "touch", "chmod", "chown", "truncate", "mkfifo"]);
2608
+ var WRITE_LAST = /* @__PURE__ */ new Set(["cp", "mv", "ln", "tee", "dd", "install", "rsync"]);
2609
+ var SHELL_ARGV0 = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "eval", "xargs", "env", "nohup", "time", "sudo"]);
2610
+ var REDIRECTS = /* @__PURE__ */ new Set([">", ">>", "<", "2>", "2>>", "&>", "<<<"]);
2611
+ function tokenise(cmd) {
2612
+ const tokens = [];
2613
+ const subshells = [];
2614
+ let cur = "";
2615
+ let quoted = false;
2616
+ let started = false;
2617
+ let quote = null;
2618
+ const push = () => {
2619
+ if (started) tokens.push({ value: cur, quoted });
2620
+ cur = "";
2621
+ quoted = false;
2622
+ started = false;
2623
+ };
2624
+ for (let i = 0; i < cmd.length; i++) {
2625
+ const c = cmd[i];
2626
+ if (quote) {
2627
+ if (c === quote) {
2628
+ quote = null;
2629
+ } else if (c === "\\" && quote === '"' && i + 1 < cmd.length) {
2630
+ cur += cmd[++i];
2631
+ started = true;
2632
+ } else {
2633
+ cur += c;
2634
+ started = true;
2635
+ }
2636
+ continue;
2637
+ }
2638
+ if (c === '"' || c === "'") {
2639
+ quote = c;
2640
+ quoted = true;
2641
+ started = true;
2642
+ continue;
2643
+ }
2644
+ if (c === "\\" && i + 1 < cmd.length) {
2645
+ cur += cmd[++i];
2646
+ started = true;
2647
+ continue;
2648
+ }
2649
+ if (c === "$" && cmd[i + 1] === "(" || c === "`") {
2650
+ const open = c === "`" ? "`" : "(";
2651
+ const close = c === "`" ? "`" : ")";
2652
+ let depth = 1;
2653
+ let j = i + (c === "`" ? 1 : 2);
2654
+ let body = "";
2655
+ for (; j < cmd.length && depth > 0; j++) {
2656
+ const d = cmd[j];
2657
+ if (d === open && close !== "`") depth++;
2658
+ else if (d === close) {
2659
+ depth--;
2660
+ if (depth === 0) break;
2661
+ }
2662
+ body += d;
2663
+ }
2664
+ if (depth > 0) return null;
2665
+ subshells.push(body);
2666
+ cur += "$SUBSHELL";
2667
+ started = true;
2668
+ i = j;
2669
+ continue;
2670
+ }
2671
+ if (/\s/.test(c)) {
2672
+ push();
2673
+ continue;
2674
+ }
2675
+ const three = cmd.slice(i, i + 3);
2676
+ const two = cmd.slice(i, i + 2);
2677
+ if (three === "<<<") {
2678
+ push();
2679
+ tokens.push({ value: three, quoted: false, operator: three });
2680
+ i += 2;
2681
+ continue;
2682
+ }
2683
+ if (["&&", "||", ">>", "2>", "&>"].includes(two)) {
2684
+ push();
2685
+ tokens.push({ value: two, quoted: false, operator: two });
2686
+ i += 1;
2687
+ continue;
2688
+ }
2689
+ if (c === ";" || c === "|" || c === "&" || c === ">" || c === "<" || c === "\n") {
2690
+ push();
2691
+ tokens.push({ value: c, quoted: false, operator: c });
2692
+ continue;
2693
+ }
2694
+ cur += c;
2695
+ started = true;
2696
+ }
2697
+ if (quote) return null;
2698
+ push();
2699
+ return { tokens, subshells };
2700
+ }
2701
+ function looksLikePath(t) {
2702
+ if (t.value.startsWith("-")) return false;
2703
+ if (/\s/.test(t.value)) return false;
2704
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(t.value)) return false;
2705
+ const s = t.value.replace(/^@/, "");
2706
+ if (s.includes("$SUBSHELL")) return false;
2707
+ if (/^\$(?!\{?HOME\b)/.test(s)) return false;
2708
+ return /^(\/|~(?:$|\/)|\$\{?HOME\}?(?:$|\/)|\.\.(?:$|\/))/.test(s) || s.includes("/../") || s.endsWith("/..");
2709
+ }
2710
+ var bare = (v) => v.replace(/^@/, "");
2711
+ function scanSimple(tokens, roots, cwd) {
2712
+ const scoped = { ...roots, cwd };
2713
+ const check = (raw, need) => withinRoots(bare(raw), scoped, need) ? null : { kind: "escape", path: bare(raw), need };
2714
+ const argv0 = tokens.find((t) => !t.operator && !t.value.includes("="))?.value ?? "";
2715
+ const cmdName = argv0.split("/").pop() ?? argv0;
2716
+ if (SHELL_ARGV0.has(cmdName)) {
2717
+ for (const t of tokens) {
2718
+ if (t.operator || t.value === argv0) continue;
2719
+ if (t.quoted || t.value.includes(" ")) {
2720
+ const inner = scanCommand(t.value, roots, cwd);
2721
+ if (inner.kind !== "ok") return inner;
2722
+ } else if (looksLikePath(t)) {
2723
+ const bad = check(t.value, "read");
2724
+ if (bad) return bad;
2725
+ }
2726
+ }
2727
+ return { kind: "ok" };
2728
+ }
2729
+ if (NO_PATH_ARGV0.has(cmdName)) {
2730
+ return { kind: "ok" };
2731
+ }
2732
+ const writeAll = WRITE_ALL.has(cmdName);
2733
+ const writeLast = WRITE_LAST.has(cmdName);
2734
+ const operands = [];
2735
+ let patternPending = PATTERN_FIRST.has(cmdName);
2736
+ for (let i = 0; i < tokens.length; i++) {
2737
+ const t = tokens[i];
2738
+ if (t.operator) continue;
2739
+ if (t.value === argv0 && i === tokens.indexOf(t)) {
2740
+ if (looksLikePath(t)) {
2741
+ const bad = check(t.value, "read");
2742
+ if (bad) return bad;
2743
+ }
2744
+ continue;
2745
+ }
2746
+ if (DIR_FLAGS.has(t.value)) {
2747
+ const operand = tokens[i + 1];
2748
+ if (operand && !operand.operator) {
2749
+ const bad = check(operand.value, "read");
2750
+ if (bad) return bad;
2751
+ i++;
2752
+ }
2753
+ continue;
2754
+ }
2755
+ if (PATTERN_FLAGS.has(t.value)) {
2756
+ i++;
2757
+ continue;
2758
+ }
2759
+ if (t.value.startsWith("-")) continue;
2760
+ if (patternPending) {
2761
+ patternPending = false;
2762
+ continue;
2763
+ }
2764
+ if (looksLikePath(t)) operands.push({ token: t, index: i });
2765
+ }
2766
+ if (cmdName === "cd") {
2767
+ const target = tokens.find((t) => !t.operator && t.value !== argv0 && !t.value.startsWith("-"));
2768
+ if (!target) return { kind: "ok" };
2769
+ const bad = check(target.value, "read");
2770
+ if (bad) return bad;
2771
+ return { kind: "cd", to: target.value };
2772
+ }
2773
+ for (let n = 0; n < operands.length; n++) {
2774
+ const entry = operands[n];
2775
+ const isLast = n === operands.length - 1;
2776
+ const need = writeAll || writeLast && isLast ? "write" : "read";
2777
+ const bad = check(entry.token.value, need);
2778
+ if (bad) return bad;
2779
+ }
2780
+ return { kind: "ok" };
2781
+ }
2782
+ function scanCommand(cmd, roots, cwd = roots.cwd) {
2783
+ if (!cmd.trim()) return { kind: "ok" };
2784
+ const lexed = tokenise(cmd);
2785
+ if (!lexed) return { kind: "unparseable", reason: "unbalanced quotes or unterminated substitution" };
2786
+ for (const body of lexed.subshells) {
2787
+ const inner = scanCommand(body, roots, cwd);
2788
+ if (inner.kind !== "ok") return inner;
2789
+ }
2790
+ let current = [];
2791
+ let cwdNow = cwd;
2792
+ const segments = [];
2793
+ for (const t of lexed.tokens) {
2794
+ if (t.operator && [";", "|", "&", "&&", "||", "\n"].includes(t.operator)) {
2795
+ segments.push(current);
2796
+ current = [];
2797
+ continue;
2798
+ }
2799
+ current.push(t);
2800
+ }
2801
+ segments.push(current);
2802
+ for (const seg of segments) {
2803
+ if (seg.length === 0) continue;
2804
+ const plain = [];
2805
+ for (let i = 0; i < seg.length; i++) {
2806
+ const t = seg[i];
2807
+ if (t.operator && REDIRECTS.has(t.operator)) {
2808
+ const target = seg[i + 1];
2809
+ if (target && !target.operator) {
2810
+ const need = t.operator === "<" || t.operator === "<<<" ? "read" : "write";
2811
+ if (!withinRoots(bare(target.value), { ...roots, cwd: cwdNow }, need)) {
2812
+ return { kind: "escape", path: bare(target.value), need };
2813
+ }
2814
+ i++;
2815
+ }
2816
+ continue;
2817
+ }
2818
+ plain.push(t);
2819
+ }
2820
+ const out = scanSimple(plain, roots, cwdNow);
2821
+ if (out.kind === "cd") {
2822
+ cwdNow = expandPath(out.to, cwdNow);
2823
+ continue;
2824
+ }
2825
+ if (out.kind !== "ok") return out;
2826
+ }
2827
+ return { kind: "ok" };
2828
+ }
2829
+
2830
+ // ../../packages/edge/src/builtins.ts
2418
2831
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
2419
2832
  "Write",
2420
2833
  "Edit",
@@ -2456,7 +2869,7 @@ function isDangerousRm(cmd) {
2456
2869
  }
2457
2870
  function currentBranch(worktreePath) {
2458
2871
  try {
2459
- return execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
2872
+ return execFileSync4("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
2460
2873
  } catch {
2461
2874
  return "";
2462
2875
  }
@@ -2475,9 +2888,54 @@ function commandOf(input) {
2475
2888
  }
2476
2889
  return "";
2477
2890
  }
2891
+ var PATH_FIELDS = ["file_path", "notebook_path", "path", "filePath", "cwd", "directory"];
2892
+ var PATH_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "apply_patch"]);
2893
+ function pathsIn(input) {
2894
+ if (!input || typeof input !== "object") return [];
2895
+ const o = input;
2896
+ const out = [];
2897
+ for (const f of PATH_FIELDS) {
2898
+ if (typeof o[f] === "string" && o[f]) out.push(o[f]);
2899
+ }
2900
+ if (o.changes && typeof o.changes === "object") {
2901
+ out.push(...Object.keys(o.changes));
2902
+ }
2903
+ return out;
2904
+ }
2905
+ function fsConfineRule(roots) {
2906
+ return {
2907
+ name: "fs_confine",
2908
+ evaluate(event) {
2909
+ if (event.kind !== "action") return null;
2910
+ const deny = (path, need2) => ({
2911
+ verdict: "deny",
2912
+ policy: "fs_confine",
2913
+ reason: `${need2 === "write" ? "write to" : "read of"} "${path}" is outside the run's worktree`
2914
+ });
2915
+ if (SHELL_TOOLS.has(event.tool)) {
2916
+ const result = scanCommand(commandOf(event.input), roots);
2917
+ if (result.kind === "escape") return deny(result.path, result.need);
2918
+ if (result.kind === "unparseable") {
2919
+ return {
2920
+ verdict: "deny",
2921
+ policy: "fs_confine",
2922
+ reason: `shell command could not be parsed for path confinement (${result.reason})`
2923
+ };
2924
+ }
2925
+ return null;
2926
+ }
2927
+ const need = PATH_WRITE_TOOLS.has(event.tool) ? "write" : "read";
2928
+ for (const p of pathsIn(event.input)) {
2929
+ if (!withinRoots(p, roots, need)) return deny(p, need);
2930
+ }
2931
+ return null;
2932
+ }
2933
+ };
2934
+ }
2478
2935
  function buildRules(policies, ctx) {
2479
2936
  const rules = [];
2480
2937
  let stageToolCalls = 0;
2938
+ if (ctx.fsRoots && process.env.DAHRK_FS_CONFINE !== "0") rules.push(fsConfineRule(ctx.fsRoots));
2481
2939
  for (const p of policies) {
2482
2940
  if ("read_only" in p && p.read_only) {
2483
2941
  rules.push({
@@ -2592,11 +3050,11 @@ async function startMcpGateway(opts) {
2592
3050
  if (upstream.body) await pipeline(Readable.fromWeb(upstream.body), res);
2593
3051
  else res.end();
2594
3052
  };
2595
- await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
3053
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
2596
3054
  const { port } = server.address();
2597
3055
  return {
2598
3056
  baseUrl: `http://127.0.0.1:${port}`,
2599
- stop: () => new Promise((resolve2) => server.close(() => resolve2()))
3057
+ stop: () => new Promise((resolve3) => server.close(() => resolve3()))
2600
3058
  };
2601
3059
  }
2602
3060
 
@@ -2639,7 +3097,7 @@ var attemptOf = (jobId) => {
2639
3097
  };
2640
3098
  var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
2641
3099
  function writeScratchState(ref, job, attempt, status) {
2642
- const statePath = join8(ref.scratchPath, "state.json");
3100
+ const statePath = join10(ref.scratchPath, "state.json");
2643
3101
  let state;
2644
3102
  try {
2645
3103
  state = JSON.parse(readFileSync4(statePath, "utf8"));
@@ -2654,17 +3112,17 @@ function writeIssueContext(ref, issueContext) {
2654
3112
  if (issueContext === void 0) return;
2655
3113
  try {
2656
3114
  mkdirSync6(ref.scratchPath, { recursive: true });
2657
- writeFileSync5(join8(ref.scratchPath, "issue.md"), issueContext);
3115
+ writeFileSync5(join10(ref.scratchPath, "issue.md"), issueContext);
2658
3116
  } catch {
2659
3117
  }
2660
3118
  }
2661
3119
  function writeAttachedDocuments(ref, docs) {
2662
3120
  if (!docs || docs.length === 0) return;
2663
3121
  try {
2664
- const dir = join8(ref.scratchPath, "docs");
3122
+ const dir = join10(ref.scratchPath, "docs");
2665
3123
  mkdirSync6(dir, { recursive: true });
2666
3124
  for (const doc of docs) {
2667
- writeFileSync5(join8(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
3125
+ writeFileSync5(join10(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
2668
3126
  }
2669
3127
  } catch {
2670
3128
  }
@@ -2683,7 +3141,7 @@ function writeGuidance(ref, guidance) {
2683
3141
  if (!guidance || guidance.length === 0) return;
2684
3142
  try {
2685
3143
  mkdirSync6(ref.scratchPath, { recursive: true });
2686
- writeFileSync5(join8(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
3144
+ writeFileSync5(join10(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
2687
3145
  } catch {
2688
3146
  }
2689
3147
  }
@@ -2693,11 +3151,11 @@ function capContent(raw) {
2693
3151
  return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
2694
3152
  }
2695
3153
  function resolveWorktreeRelativePath(ref, relPath) {
2696
- if (isAbsolute2(relPath)) return void 0;
2697
- const root = resolve(ref.worktreePath);
2698
- const target = resolve(root, relPath);
2699
- const fromRoot = relative2(root, target);
2700
- if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${sep2}`) || isAbsolute2(fromRoot)) {
3154
+ if (isAbsolute3(relPath)) return void 0;
3155
+ const root = resolve2(ref.worktreePath);
3156
+ const target = resolve2(root, relPath);
3157
+ const fromRoot = relative3(root, target);
3158
+ if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${sep3}`) || isAbsolute3(fromRoot)) {
2701
3159
  return void 0;
2702
3160
  }
2703
3161
  return target;
@@ -2717,13 +3175,13 @@ function readEmittedArtifact(ref, relPath) {
2717
3175
  }
2718
3176
  function scanScratchOutput(ref, preferRel) {
2719
3177
  try {
2720
- const names = readdirSync3(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
3178
+ const names = readdirSync3(join10(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
2721
3179
  (n) => n.toLowerCase().endsWith(".md")
2722
3180
  );
2723
3181
  if (names.length === 0) return void 0;
2724
3182
  const preferBase = preferRel?.split("/").pop();
2725
3183
  const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
2726
- const raw = readFileSync4(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
3184
+ const raw = readFileSync4(join10(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
2727
3185
  if (raw.trim().length === 0) return void 0;
2728
3186
  return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
2729
3187
  } catch {
@@ -2733,7 +3191,7 @@ function scanScratchOutput(ref, preferRel) {
2733
3191
  function scanChangedMarkdown(ref) {
2734
3192
  const git = (args) => {
2735
3193
  try {
2736
- return execFileSync4("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
3194
+ return execFileSync5("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
2737
3195
  } catch {
2738
3196
  return [];
2739
3197
  }
@@ -2743,7 +3201,7 @@ function scanChangedMarkdown(ref) {
2743
3201
  );
2744
3202
  for (const rel of rels) {
2745
3203
  try {
2746
- const raw = readFileSync4(join8(ref.worktreePath, rel), "utf8");
3204
+ const raw = readFileSync4(join10(ref.worktreePath, rel), "utf8");
2747
3205
  if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
2748
3206
  } catch {
2749
3207
  }
@@ -2866,9 +3324,9 @@ function createStageRunner(deps) {
2866
3324
  ...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
2867
3325
  });
2868
3326
  } else {
2869
- const base = deps.scratchRoot ?? join8(tmpdir2(), "dahrk", "scratch");
2870
- const worktreePath = join8(base, runId);
2871
- const scratchPath = join8(worktreePath, ".skakel", "scratch");
3327
+ const base = deps.scratchRoot ?? join10(tmpdir4(), "dahrk", "scratch");
3328
+ const worktreePath = join10(base, runId);
3329
+ const scratchPath = join10(worktreePath, ".skakel", "scratch");
2872
3330
  mkdirSync6(scratchPath, { recursive: true });
2873
3331
  ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
2874
3332
  scratchOnly.add(runId);
@@ -2884,7 +3342,7 @@ function createStageRunner(deps) {
2884
3342
  const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
2885
3343
  if (artifactDir && slash > 0) {
2886
3344
  try {
2887
- mkdirSync6(join8(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
3345
+ mkdirSync6(join10(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
2888
3346
  } catch {
2889
3347
  }
2890
3348
  }
@@ -2912,8 +3370,8 @@ function createStageRunner(deps) {
2912
3370
  if (!sink) return;
2913
3371
  const base = { tenantId: job.tenantId, runId, stageId, attempt };
2914
3372
  try {
2915
- for (const name of readdirSync3(join8(writer.dir, "blobs"))) {
2916
- const bytes = readFileSync4(join8(writer.dir, "blobs", name));
3373
+ for (const name of readdirSync3(join10(writer.dir, "blobs"))) {
3374
+ const bytes = readFileSync4(join10(writer.dir, "blobs", name));
2917
3375
  const { url } = await sink.requestBlobUrl({
2918
3376
  ...base,
2919
3377
  sha256: name,
@@ -2927,7 +3385,7 @@ function createStageRunner(deps) {
2927
3385
  }
2928
3386
  let archiveKey;
2929
3387
  try {
2930
- const bytes = readFileSync4(join8(writer.dir, "trace.jsonl"));
3388
+ const bytes = readFileSync4(join10(writer.dir, "trace.jsonl"));
2931
3389
  const sha = createHash3("sha256").update(bytes).digest("hex");
2932
3390
  const { key, url } = await sink.requestBlobUrl({
2933
3391
  ...base,
@@ -3011,7 +3469,11 @@ function createStageRunner(deps) {
3011
3469
  const jobRules = buildRules(job.policies ?? [], {
3012
3470
  worktreePath: ref.worktreePath,
3013
3471
  repoName: job.workspaceRef?.repo ?? "",
3014
- runToolCalls: counter
3472
+ runToolCalls: counter,
3473
+ // DHK-392: the stage is confined to the run's worktree (plus its scratch dir, the git object
3474
+ // store it depends on, and the toolchain). A node default, not a workflow policy. A run with
3475
+ // no repo still gets a box - just a smaller one, around its scratch dir.
3476
+ fsRoots: computeFsRoots({ worktreePath: ref.worktreePath, scratchPath: ref.scratchPath })
3015
3477
  });
3016
3478
  const rules = [...jobRules, ...deps.rules];
3017
3479
  const entry = evaluatePolicies({ kind: "stage-entry", stageId }, rules);
@@ -3020,6 +3482,7 @@ function createStageRunner(deps) {
3020
3482
  return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
3021
3483
  }
3022
3484
  let denied = false;
3485
+ let escapedUnblocked = false;
3023
3486
  const authorisedActions = [];
3024
3487
  const runtime = agentConfig.runtime;
3025
3488
  const actionKey = (tool3, input) => {
@@ -3062,6 +3525,7 @@ function createStageRunner(deps) {
3062
3525
  if (verdict.verdict === "deny") {
3063
3526
  streamEvent(writer.append(event));
3064
3527
  recordDeny(verdict, event.toolUseId);
3528
+ if (verdict.policy === "fs_confine" && runtime !== "claude-code") escapedUnblocked = true;
3065
3529
  return;
3066
3530
  }
3067
3531
  }
@@ -3124,10 +3588,16 @@ function createStageRunner(deps) {
3124
3588
  if (killTimer) clearTimeout(killTimer);
3125
3589
  }
3126
3590
  let status = timedOut ? "timeout" : result.status;
3591
+ if (status === "ok" && escapedUnblocked) {
3592
+ status = "fail";
3593
+ const msg = `stage reached outside the run's worktree and the ${runtime} runtime could not block it before it ran`;
3594
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "fs-confine-escape", message: msg });
3595
+ deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
3596
+ }
3127
3597
  if (status === "ok" && job.hooks && job.hooks.length > 0) {
3128
3598
  for (const cmd of job.hooks) {
3129
3599
  try {
3130
- execFileSync4("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
3600
+ execFileSync5("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
3131
3601
  } catch (e) {
3132
3602
  status = "fail";
3133
3603
  writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
@@ -3303,6 +3773,8 @@ async function startEdgeNode(opts) {
3303
3773
  const counters = new HealthCounters();
3304
3774
  const shipper = opts.shipper;
3305
3775
  const telemetryCeiling = ceilingFromEnv(process.env);
3776
+ let currentRuntimes = opts.runtimes;
3777
+ const runtimesKey = (r) => [...r].sort().join(",");
3306
3778
  const gitLog = log.child({ component: "git" });
3307
3779
  const gitLogger = {
3308
3780
  info: (msg) => gitLog.debug(msg),
@@ -3349,7 +3821,7 @@ async function startEdgeNode(opts) {
3349
3821
  health: collectHealth({
3350
3822
  counters,
3351
3823
  clientVersion: opts.clientVersion ?? "0.0.0",
3352
- runtimes: opts.runtimes,
3824
+ runtimes: currentRuntimes,
3353
3825
  worktreesDir: gitService.worktreesDir
3354
3826
  })
3355
3827
  } : { type: "heartbeat" }
@@ -3372,12 +3844,12 @@ async function startEdgeNode(opts) {
3372
3844
  const trace = {
3373
3845
  event: (frame) => send({ type: "trace-event", ...frame }),
3374
3846
  finalised: (frame) => send({ type: "trace-finalised", ...frame }),
3375
- requestBlobUrl: (req) => new Promise((resolve2) => {
3847
+ requestBlobUrl: (req) => new Promise((resolve3) => {
3376
3848
  const reqId = `${req.runId}:${req.stageId}:${req.attempt}:${blobReqCounter++}`;
3377
- pendingBlob.set(reqId, resolve2);
3849
+ pendingBlob.set(reqId, resolve3);
3378
3850
  send({ type: "blob-put-request", reqId, ...req });
3379
3851
  setTimeout(() => {
3380
- if (pendingBlob.delete(reqId)) resolve2({ key: "" });
3852
+ if (pendingBlob.delete(reqId)) resolve3({ key: "" });
3381
3853
  }, 3e4).unref?.();
3382
3854
  })
3383
3855
  };
@@ -3408,6 +3880,41 @@ async function startEdgeNode(opts) {
3408
3880
  for (const e of r.errors) log.warn({ reapError: e }, `EDGE_REAP_ERROR:${e}`);
3409
3881
  }).catch((e) => log.warn({ err: e }, `EDGE_REAP_ERROR:${e.message}`));
3410
3882
  const nodeId = opts.nodeId ?? randomUUID();
3883
+ const sendHello = () => {
3884
+ send({
3885
+ type: "hello",
3886
+ enrolToken: opts.enrolToken ?? "",
3887
+ detectedRuntimes: currentRuntimes,
3888
+ servesRepoIds: opts.servesRepoIds ?? [],
3889
+ ...opts.credentialModeExplicit && opts.credentialMode ? { credentialMode: opts.credentialMode } : {},
3890
+ nodeId,
3891
+ ...opts.name ? { name: opts.name } : {},
3892
+ os: osPlatform(),
3893
+ arch: osArch(),
3894
+ clientVersion: opts.clientVersion ?? "0.0.0",
3895
+ // Advertise the resolved worktree base so the hub records each run's real worktree location in
3896
+ // the projection instead of an advisory placeholder. Single-sourced from the git service so it
3897
+ // always matches where worktrees actually land.
3898
+ worktreesDir: gitService.worktreesDir
3899
+ });
3900
+ };
3901
+ let reprobeTimer;
3902
+ if (opts.reprobeRuntimes) {
3903
+ const reprobe = opts.reprobeRuntimes;
3904
+ reprobeTimer = setInterval(() => {
3905
+ void reprobe().then((detected) => {
3906
+ if (runtimesKey(detected) === runtimesKey(currentRuntimes)) return;
3907
+ const before = currentRuntimes;
3908
+ currentRuntimes = detected;
3909
+ log.warn(
3910
+ { before, after: detected },
3911
+ `EDGE_RUNTIMES_CHANGED:${before.join(",") || "none"} -> ${detected.join(",") || "none"}`
3912
+ );
3913
+ sendHello();
3914
+ }).catch((e) => log.warn({ err: e }, `EDGE_REPROBE_ERROR:${e.message}`));
3915
+ }, opts.runtimeRecheckMs ?? 6e4);
3916
+ reprobeTimer.unref?.();
3917
+ }
3411
3918
  const running = /* @__PURE__ */ new Set();
3412
3919
  const onMessage = async (raw) => {
3413
3920
  const msg = decode(raw);
@@ -3440,10 +3947,10 @@ async function startEdgeNode(opts) {
3440
3947
  return;
3441
3948
  }
3442
3949
  if (msg.type === "blob-put-url") {
3443
- const resolve2 = pendingBlob.get(msg.reqId);
3444
- if (resolve2) {
3950
+ const resolve3 = pendingBlob.get(msg.reqId);
3951
+ if (resolve3) {
3445
3952
  pendingBlob.delete(msg.reqId);
3446
- resolve2({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
3953
+ resolve3({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
3447
3954
  }
3448
3955
  return;
3449
3956
  }
@@ -3558,22 +4065,7 @@ async function startEdgeNode(opts) {
3558
4065
  connectCount++;
3559
4066
  counters.connectCount = connectCount;
3560
4067
  log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
3561
- send({
3562
- type: "hello",
3563
- enrolToken: opts.enrolToken ?? "",
3564
- detectedRuntimes: opts.runtimes,
3565
- servesRepoIds: opts.servesRepoIds ?? [],
3566
- ...opts.credentialModeExplicit && opts.credentialMode ? { credentialMode: opts.credentialMode } : {},
3567
- nodeId,
3568
- ...opts.name ? { name: opts.name } : {},
3569
- os: osPlatform(),
3570
- arch: osArch(),
3571
- clientVersion: opts.clientVersion ?? "0.0.0",
3572
- // Advertise the resolved worktree base so the hub records each run's real worktree location in
3573
- // the projection instead of an advisory placeholder. Single-sourced from the git service so it
3574
- // always matches where worktrees actually land.
3575
- worktreesDir: gitService.worktreesDir
3576
- });
4068
+ sendHello();
3577
4069
  for (const frame of lastResults.values()) send(frame);
3578
4070
  startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
3579
4071
  });
@@ -3607,16 +4099,18 @@ async function startEdgeNode(opts) {
3607
4099
  heartbeat = void 0;
3608
4100
  if (reconnectTimer) clearTimeout(reconnectTimer);
3609
4101
  reconnectTimer = void 0;
4102
+ if (reprobeTimer) clearInterval(reprobeTimer);
4103
+ reprobeTimer = void 0;
3610
4104
  ws?.close(1e3, "shutting down");
3611
4105
  },
3612
4106
  { once: true }
3613
4107
  );
3614
4108
  connect();
3615
- await new Promise((resolve2, reject) => {
4109
+ await new Promise((resolve3, reject) => {
3616
4110
  const t = setInterval(() => {
3617
4111
  if (ws && ws.readyState === WebSocket.OPEN) {
3618
4112
  clearInterval(t);
3619
- resolve2();
4113
+ resolve3();
3620
4114
  }
3621
4115
  }, 50);
3622
4116
  onFatal = (err) => {
@@ -3633,24 +4127,37 @@ var PROBES = [
3633
4127
  { runtime: "codex", cmd: "codex" },
3634
4128
  { runtime: "pi", cmd: "pi" }
3635
4129
  ];
3636
- function probe(cmd, timeoutMs) {
3637
- return new Promise((resolve2) => {
4130
+ var DEFAULT_TIMEOUT_MS = 5e3;
4131
+ var DEFAULT_ATTEMPTS = 2;
4132
+ function probeOnce(cmd, timeoutMs) {
4133
+ return new Promise((resolve3) => {
3638
4134
  execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
3639
- if (err) return resolve2(void 0);
4135
+ if (err) {
4136
+ const code = err.code;
4137
+ return resolve3({ retryable: code !== "ENOENT" });
4138
+ }
3640
4139
  const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
3641
- resolve2(line ?? "");
4140
+ resolve3({ version: line ?? "" });
3642
4141
  });
3643
4142
  });
3644
4143
  }
3645
- async function probeRuntimeStatuses(timeoutMs = 3e3) {
3646
- const versions = await Promise.all(PROBES.map((p) => probe(p.cmd, timeoutMs)));
4144
+ async function probe(cmd, timeoutMs, attempts) {
4145
+ for (let attempt = 1; attempt <= attempts; attempt++) {
4146
+ const result = await probeOnce(cmd, timeoutMs);
4147
+ if ("version" in result) return result.version;
4148
+ if (!result.retryable) return void 0;
4149
+ }
4150
+ return void 0;
4151
+ }
4152
+ async function probeRuntimeStatuses(timeoutMs = DEFAULT_TIMEOUT_MS, attempts = DEFAULT_ATTEMPTS) {
4153
+ const versions = await Promise.all(PROBES.map((p) => probe(p.cmd, timeoutMs, attempts)));
3647
4154
  return PROBES.map((p, i) => {
3648
4155
  const version = versions[i];
3649
4156
  return version === void 0 ? { runtime: p.runtime, cmd: p.cmd, installed: false } : { runtime: p.runtime, cmd: p.cmd, installed: true, version };
3650
4157
  });
3651
4158
  }
3652
- async function detectRuntimes(timeoutMs = 3e3) {
3653
- const statuses = await probeRuntimeStatuses(timeoutMs);
4159
+ async function detectRuntimes(timeoutMs = DEFAULT_TIMEOUT_MS, attempts = DEFAULT_ATTEMPTS) {
4160
+ const statuses = await probeRuntimeStatuses(timeoutMs, attempts);
3654
4161
  return statuses.filter((s) => s.installed).map((s) => s.runtime);
3655
4162
  }
3656
4163
 
@@ -3681,7 +4188,7 @@ function probeHub(opts) {
3681
4188
  clientVersion = "0.0.0",
3682
4189
  timeoutMs = 8e3
3683
4190
  } = opts;
3684
- return new Promise((resolve2) => {
4191
+ return new Promise((resolve3) => {
3685
4192
  let settled = false;
3686
4193
  let opened = false;
3687
4194
  let ws;
@@ -3693,7 +4200,7 @@ function probeHub(opts) {
3693
4200
  ws.terminate();
3694
4201
  } catch {
3695
4202
  }
3696
- resolve2(result);
4203
+ resolve3(result);
3697
4204
  };
3698
4205
  const timer = setTimeout(
3699
4206
  () => done({ ok: false, reason: "timeout", detail: `no welcome within ${timeoutMs}ms` }),
@@ -4153,8 +4660,8 @@ function usage(bin, command) {
4153
4660
  }
4154
4661
 
4155
4662
  // src/diagnose.ts
4156
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
4157
- import { join as join9 } from "path";
4663
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
4664
+ import { join as join11 } from "path";
4158
4665
  var BUNDLE_LOG_LINES = 2e3;
4159
4666
  function tailJsonl(raw, n) {
4160
4667
  const records = [];
@@ -4195,7 +4702,7 @@ async function buildBundle(deps) {
4195
4702
  if (deps.exists(deps.crashDir)) {
4196
4703
  for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
4197
4704
  try {
4198
- crashes.push(JSON.parse(deps.readFile(join9(deps.crashDir, name))));
4705
+ crashes.push(JSON.parse(deps.readFile(join11(deps.crashDir, name))));
4199
4706
  } catch (e) {
4200
4707
  warnings.push(`could not read crash record ${name} (${e.message})`);
4201
4708
  }
@@ -4254,17 +4761,17 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
4254
4761
  ...doctor ? { doctor } : {},
4255
4762
  readFile: (p) => readFileSync5(p, "utf8"),
4256
4763
  listDir: (p) => readdirSync4(p),
4257
- exists: (p) => existsSync6(p),
4764
+ exists: (p) => existsSync7(p),
4258
4765
  writeFile: (p, content) => {
4259
- const dir = join9(p, "..");
4260
- if (!existsSync6(dir)) mkdirSync7(dir, { recursive: true });
4766
+ const dir = join11(p, "..");
4767
+ if (!existsSync7(dir)) mkdirSync7(dir, { recursive: true });
4261
4768
  writeFileSync6(p, content, { mode: 384 });
4262
4769
  },
4263
4770
  out: (line) => void process.stdout.write(`${line}
4264
4771
  `)
4265
4772
  });
4266
4773
  function defaultBundlePath(cwd, now) {
4267
- return join9(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
4774
+ return join11(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
4268
4775
  }
4269
4776
 
4270
4777
  // src/doctor.ts
@@ -4399,8 +4906,8 @@ async function runDoctor(inputs, deps = {}) {
4399
4906
  }
4400
4907
 
4401
4908
  // src/lock.ts
4402
- import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
4403
- import { dirname as dirname4 } from "path";
4909
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
4910
+ import { dirname as dirname5 } from "path";
4404
4911
  function parseLock(content) {
4405
4912
  if (!content) return void 0;
4406
4913
  const pid = Number(content.trim());
@@ -4438,7 +4945,7 @@ var defaultLockDeps = (file) => ({
4438
4945
  }
4439
4946
  },
4440
4947
  writeFile: (path, content) => {
4441
- if (!existsSync7(dirname4(path))) mkdirSync8(dirname4(path), { recursive: true, mode: 448 });
4948
+ if (!existsSync8(dirname5(path))) mkdirSync8(dirname5(path), { recursive: true, mode: 448 });
4442
4949
  writeFileSync7(path, content);
4443
4950
  },
4444
4951
  removeFile: (path) => rmSync6(path, { force: true }),
@@ -4447,7 +4954,7 @@ var defaultLockDeps = (file) => ({
4447
4954
 
4448
4955
  // src/logs.ts
4449
4956
  import { spawn } from "child_process";
4450
- import { copyFileSync, existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
4957
+ import { copyFileSync, existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
4451
4958
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
4452
4959
  function logsCommand(inputs) {
4453
4960
  return [
@@ -4528,7 +5035,7 @@ async function runStructuredLogs(inputs, deps) {
4528
5035
  }
4529
5036
  function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
4530
5037
  try {
4531
- if (!existsSync8(file) || statSync4(file).size <= maxBytes) return;
5038
+ if (!existsSync9(file) || statSync4(file).size <= maxBytes) return;
4532
5039
  copyFileSync(file, `${file}.1`);
4533
5040
  truncateSync(file, 0);
4534
5041
  } catch {
@@ -4537,13 +5044,13 @@ function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
4537
5044
  var defaultLogsDeps = (files, jsonlFile) => ({
4538
5045
  files,
4539
5046
  jsonlFile,
4540
- fileExists: (path) => existsSync8(path),
5047
+ fileExists: (path) => existsSync9(path),
4541
5048
  readFile: (path) => readFileSync7(path, "utf8"),
4542
- run: (argv) => new Promise((resolve2) => {
5049
+ run: (argv) => new Promise((resolve3) => {
4543
5050
  const [cmd, ...args] = argv;
4544
5051
  const child = spawn(cmd, args, { stdio: "inherit" });
4545
- child.on("error", () => resolve2(1));
4546
- child.on("close", (code) => resolve2(code ?? 0));
5052
+ child.on("error", () => resolve3(1));
5053
+ child.on("close", (code) => resolve3(code ?? 0));
4547
5054
  }),
4548
5055
  out: (line) => void process.stdout.write(`${line}
4549
5056
  `)
@@ -4551,11 +5058,11 @@ var defaultLogsDeps = (files, jsonlFile) => ({
4551
5058
 
4552
5059
  // src/process-safety.ts
4553
5060
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
4554
- import { join as join10 } from "path";
5061
+ import { join as join12 } from "path";
4555
5062
  function writeCrashRecord(dir, record) {
4556
5063
  try {
4557
5064
  mkdirSync9(dir, { recursive: true, mode: 448 });
4558
- const file = join10(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
5065
+ const file = join12(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
4559
5066
  writeFileSync8(file, `${JSON.stringify(record, null, 2)}
4560
5067
  `, { mode: 384 });
4561
5068
  return file;
@@ -4609,11 +5116,11 @@ function installProcessSafetyNet(opts) {
4609
5116
  }
4610
5117
 
4611
5118
  // src/preflight.ts
4612
- import { execFileSync as execFileSync5 } from "child_process";
5119
+ import { execFileSync as execFileSync6 } from "child_process";
4613
5120
  import { randomUUID as randomUUID2 } from "crypto";
4614
- import { accessSync, constants as fsConstants, existsSync as existsSync9, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
4615
- import { homedir as homedir2 } from "os";
4616
- import { join as join11 } from "path";
5121
+ import { accessSync, constants as fsConstants, existsSync as existsSync10, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
5122
+ import { homedir as homedir4 } from "os";
5123
+ import { join as join13 } from "path";
4617
5124
  var REPORT_BASE_URL = "https://app.dahrk.ai/r";
4618
5125
  var LOW_DISK_BYTES = 512 * 1024 * 1024;
4619
5126
  var PREFLIGHT_STAGES = [
@@ -4742,7 +5249,7 @@ var defaultDeps2 = () => ({
4742
5249
  });
4743
5250
  function commandPresent(cmd) {
4744
5251
  try {
4745
- execFileSync5("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
5252
+ execFileSync6("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
4746
5253
  return true;
4747
5254
  } catch {
4748
5255
  return false;
@@ -4750,12 +5257,12 @@ function commandPresent(cmd) {
4750
5257
  }
4751
5258
  function sshKeyPresent() {
4752
5259
  try {
4753
- const dir = join11(homedir2(), ".ssh");
4754
- if (existsSync9(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
5260
+ const dir = join13(homedir4(), ".ssh");
5261
+ if (existsSync10(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
4755
5262
  } catch {
4756
5263
  }
4757
5264
  try {
4758
- execFileSync5("ssh-add", ["-l"], { stdio: "ignore" });
5265
+ execFileSync6("ssh-add", ["-l"], { stdio: "ignore" });
4759
5266
  return true;
4760
5267
  } catch {
4761
5268
  return false;
@@ -4770,12 +5277,12 @@ function writable(dir) {
4770
5277
  }
4771
5278
  }
4772
5279
  function worktreeRoot(env) {
4773
- return env.DAHRK_WORKTREES_DIR ?? join11(env.DAHRK_STATE_DIR ?? join11(homedir2(), ".dahrk"), "worktrees");
5280
+ return env.DAHRK_WORKTREES_DIR ?? join13(env.DAHRK_STATE_DIR ?? join13(homedir4(), ".dahrk"), "worktrees");
4774
5281
  }
4775
5282
  function nearestExisting(dir) {
4776
5283
  let cur = dir;
4777
- while (!existsSync9(cur)) {
4778
- const parent = join11(cur, "..");
5284
+ while (!existsSync10(cur)) {
5285
+ const parent = join13(cur, "..");
4779
5286
  if (parent === cur) break;
4780
5287
  cur = parent;
4781
5288
  }
@@ -4790,7 +5297,7 @@ function freeDiskBytes(dir) {
4790
5297
  }
4791
5298
  }
4792
5299
  function probeRepo(repoPath) {
4793
- const git = (args) => execFileSync5("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5300
+ const git = (args) => execFileSync6("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4794
5301
  try {
4795
5302
  if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") {
4796
5303
  return { path: repoPath, isGitRepo: false, headResolves: false, detail: "not inside a work tree" };
@@ -4825,49 +5332,51 @@ function gatherHostFacts(repoPath) {
4825
5332
  }
4826
5333
 
4827
5334
  // src/service.ts
4828
- import { execFileSync as execFileSync6 } from "child_process";
4829
- import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
4830
- import { homedir as homedir4, platform as osPlatform3, userInfo } from "os";
4831
- import { join as join13 } from "path";
5335
+ import { execFileSync as execFileSync7 } from "child_process";
5336
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
5337
+ import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
5338
+ import { join as join15 } from "path";
4832
5339
 
4833
5340
  // src/state.ts
4834
- import { chmodSync, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
4835
- import { homedir as homedir3 } from "os";
4836
- import { join as join12 } from "path";
5341
+ import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
5342
+ import { homedir as homedir5 } from "os";
5343
+ import { join as join14 } from "path";
5344
+ var RUNTIMES = ["claude-code", "codex", "pi"];
5345
+ var isRuntime = (v) => RUNTIMES.includes(v);
4837
5346
  var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
4838
5347
  var isDesired = (v) => v === "running" || v === "stopped";
4839
5348
  var FILE_MODE = 384;
4840
5349
  var DIR_MODE = 448;
4841
5350
  function stateDir(env) {
4842
- return env.DAHRK_STATE_DIR ?? join12(homedir3(), ".dahrk");
5351
+ return env.DAHRK_STATE_DIR ?? join14(homedir5(), ".dahrk");
4843
5352
  }
4844
5353
  function legacyStateDir(env) {
4845
- return env.DAHRK_STATE_DIR ? void 0 : join12(homedir3(), ".skakel");
5354
+ return env.DAHRK_STATE_DIR ? void 0 : join14(homedir5(), ".skakel");
4846
5355
  }
4847
5356
  function stateFile(env) {
4848
- return join12(stateDir(env), "node.json");
5357
+ return join14(stateDir(env), "node.json");
4849
5358
  }
4850
5359
  function logDir(env) {
4851
- return join12(stateDir(env), "logs");
5360
+ return join14(stateDir(env), "logs");
4852
5361
  }
4853
5362
  function logFiles(env) {
4854
5363
  const dir = logDir(env);
4855
- return { out: join12(dir, "node.out.log"), err: join12(dir, "node.err.log") };
5364
+ return { out: join14(dir, "node.out.log"), err: join14(dir, "node.err.log") };
4856
5365
  }
4857
5366
  function jsonlLogFile(env) {
4858
- return join12(logDir(env), "node.jsonl");
5367
+ return join14(logDir(env), "node.jsonl");
4859
5368
  }
4860
5369
  function crashDir(env) {
4861
- return join12(logDir(env), "crashes");
5370
+ return join14(logDir(env), "crashes");
4862
5371
  }
4863
5372
  function lockFile(env) {
4864
- return join12(stateDir(env), "node.pid");
5373
+ return join14(stateDir(env), "node.pid");
4865
5374
  }
4866
5375
  function setDesired(env, desired) {
4867
5376
  writeState(env, { desired });
4868
5377
  }
4869
5378
  function readState(file) {
4870
- if (!existsSync10(file)) return {};
5379
+ if (!existsSync11(file)) return {};
4871
5380
  try {
4872
5381
  const parsed = JSON.parse(readFileSync8(file, "utf8"));
4873
5382
  const state = {};
@@ -4876,6 +5385,10 @@ function readState(file) {
4876
5385
  if (typeof value === "string" && value) state[key] = value;
4877
5386
  }
4878
5387
  if (isDesired(parsed["desired"])) state.desired = parsed["desired"];
5388
+ if (Array.isArray(parsed["runtimes"])) {
5389
+ const runtimes = parsed["runtimes"].filter(isRuntime);
5390
+ if (runtimes.length) state.runtimes = runtimes;
5391
+ }
4879
5392
  return state;
4880
5393
  } catch {
4881
5394
  return {};
@@ -4968,9 +5481,9 @@ ${envEntries}
4968
5481
  <key>ThrottleInterval</key>
4969
5482
  <integer>10</integer>
4970
5483
  <key>StandardOutPath</key>
4971
- <string>${xmlEscape(join13(inputs.logDir, "node.out.log"))}</string>
5484
+ <string>${xmlEscape(join15(inputs.logDir, "node.out.log"))}</string>
4972
5485
  <key>StandardErrorPath</key>
4973
- <string>${xmlEscape(join13(inputs.logDir, "node.err.log"))}</string>
5486
+ <string>${xmlEscape(join15(inputs.logDir, "node.err.log"))}</string>
4974
5487
  </dict>
4975
5488
  </plist>
4976
5489
  `;
@@ -4993,8 +5506,8 @@ Type=simple
4993
5506
  ExecStart=${exec}
4994
5507
  ${envLines}
4995
5508
  WorkingDirectory=${inputs.homeDir}
4996
- StandardOutput=append:${join13(inputs.logDir, "node.out.log")}
4997
- StandardError=append:${join13(inputs.logDir, "node.err.log")}
5509
+ StandardOutput=append:${join15(inputs.logDir, "node.out.log")}
5510
+ StandardError=append:${join15(inputs.logDir, "node.err.log")}
4998
5511
  Restart=on-failure
4999
5512
  RestartSec=3
5000
5513
  RestartPreventExitStatus=78
@@ -5005,7 +5518,7 @@ WantedBy=default.target
5005
5518
  }
5006
5519
  function buildPlan(inputs) {
5007
5520
  if (inputs.manager === "launchd") {
5008
- const filePath2 = join13(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5521
+ const filePath2 = join15(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5009
5522
  return {
5010
5523
  manager: "launchd",
5011
5524
  label: LAUNCHD_LABEL,
@@ -5022,7 +5535,7 @@ function buildPlan(inputs) {
5022
5535
  logHint: "dahrk logs -f"
5023
5536
  };
5024
5537
  }
5025
- const filePath = join13(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5538
+ const filePath = join15(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5026
5539
  const user = userInfo().username;
5027
5540
  return {
5028
5541
  manager: "systemd",
@@ -5052,7 +5565,7 @@ function unitIsCurrent(plan, onDisk) {
5052
5565
  return onDisk === plan.content;
5053
5566
  }
5054
5567
  function unitPath(manager, homeDir) {
5055
- return manager === "launchd" ? join13(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join13(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5568
+ return manager === "launchd" ? join15(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join15(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5056
5569
  }
5057
5570
  function statusCommand(manager) {
5058
5571
  return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
@@ -5261,7 +5774,7 @@ function dirOf(path) {
5261
5774
  function resolveScriptPath() {
5262
5775
  const argv1 = process.argv[1] ?? "";
5263
5776
  try {
5264
- return realpathSync2(argv1);
5777
+ return realpathSync3(argv1);
5265
5778
  } catch {
5266
5779
  return argv1;
5267
5780
  }
@@ -5275,14 +5788,14 @@ function stableNodeBin(execPath, realpath) {
5275
5788
  }
5276
5789
  var realpathOrUndefined = (p) => {
5277
5790
  try {
5278
- return realpathSync2(p);
5791
+ return realpathSync3(p);
5279
5792
  } catch {
5280
5793
  return void 0;
5281
5794
  }
5282
5795
  };
5283
5796
  var defaultDeps3 = () => ({
5284
5797
  platform: osPlatform3(),
5285
- homeDir: homedir4(),
5798
+ homeDir: homedir6(),
5286
5799
  // Not `process.execPath` raw: that is the versioned Homebrew Cellar path, which the next
5287
5800
  // `brew upgrade node` deletes out from under the unit. See `stableNodeBin`.
5288
5801
  nodeBin: stableNodeBin(process.execPath, realpathOrUndefined),
@@ -5309,11 +5822,11 @@ var defaultDeps3 = () => ({
5309
5822
  }
5310
5823
  },
5311
5824
  removeFile: (path) => rmSync7(path, { force: true }),
5312
- fileExists: (path) => existsSync11(path),
5825
+ fileExists: (path) => existsSync12(path),
5313
5826
  run: (argv) => {
5314
5827
  const [cmd, ...args] = argv;
5315
5828
  try {
5316
- execFileSync6(cmd, args, { stdio: "inherit" });
5829
+ execFileSync7(cmd, args, { stdio: "inherit" });
5317
5830
  return 0;
5318
5831
  } catch (e) {
5319
5832
  const status = e.status;
@@ -5323,7 +5836,7 @@ var defaultDeps3 = () => ({
5323
5836
  capture: (argv) => {
5324
5837
  const [cmd, ...args] = argv;
5325
5838
  try {
5326
- const stdout = execFileSync6(cmd, args, {
5839
+ const stdout = execFileSync7(cmd, args, {
5327
5840
  encoding: "utf8",
5328
5841
  stdio: ["ignore", "pipe", "ignore"]
5329
5842
  });
@@ -5338,8 +5851,8 @@ var defaultDeps3 = () => ({
5338
5851
  });
5339
5852
 
5340
5853
  // src/update.ts
5341
- import { execFileSync as execFileSync7 } from "child_process";
5342
- import { realpathSync as realpathSync3 } from "fs";
5854
+ import { execFileSync as execFileSync8 } from "child_process";
5855
+ import { realpathSync as realpathSync4 } from "fs";
5343
5856
  var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
5344
5857
  var CHANNEL_COMMANDS = {
5345
5858
  npm: "npm install -g dahrk-node@latest",
@@ -5360,7 +5873,7 @@ function detectChannel(binPath) {
5360
5873
  if (!binPath) return "unknown";
5361
5874
  let resolved = binPath;
5362
5875
  try {
5363
- resolved = realpathSync3(binPath);
5876
+ resolved = realpathSync4(binPath);
5364
5877
  } catch {
5365
5878
  }
5366
5879
  if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
@@ -5448,7 +5961,7 @@ async function fetchLatestVersion(signal) {
5448
5961
  function spawnUpgrade(argv) {
5449
5962
  const [cmd, ...args] = argv;
5450
5963
  try {
5451
- execFileSync7(cmd, args, { stdio: "inherit" });
5964
+ execFileSync8(cmd, args, { stdio: "inherit" });
5452
5965
  return 0;
5453
5966
  } catch (e) {
5454
5967
  const status = e.status;
@@ -5590,11 +6103,11 @@ async function runStatus(inputs, deps) {
5590
6103
  }
5591
6104
 
5592
6105
  // src/main.ts
5593
- var CLIENT_VERSION = "0.1.10";
6106
+ var CLIENT_VERSION = "0.1.12";
5594
6107
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
5595
6108
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
5596
- var RUNTIMES = ["claude-code", "codex", "pi"];
5597
- var isRuntime = (r) => RUNTIMES.includes(r);
6109
+ var RUNTIMES2 = ["claude-code", "codex", "pi"];
6110
+ var isRuntime2 = (r) => RUNTIMES2.includes(r);
5598
6111
  function resolveNodeId(env, opts = {}) {
5599
6112
  if (env.DAHRK_NODE_ID) return env.DAHRK_NODE_ID;
5600
6113
  if (opts.ephemeral) return randomUUID3();
@@ -5602,7 +6115,7 @@ function resolveNodeId(env, opts = {}) {
5602
6115
  if (existing) return existing;
5603
6116
  const legacy = legacyStateDir(env);
5604
6117
  if (legacy) {
5605
- const legacyId = readState(join14(legacy, "node.json")).nodeId;
6118
+ const legacyId = readState(join16(legacy, "node.json")).nodeId;
5606
6119
  if (legacyId) return legacyId;
5607
6120
  }
5608
6121
  const nodeId = randomUUID3();
@@ -5610,7 +6123,7 @@ function resolveNodeId(env, opts = {}) {
5610
6123
  return nodeId;
5611
6124
  }
5612
6125
  async function resolveRuntimes(env) {
5613
- const override = list(env.DAHRK_RUNTIMES).filter(isRuntime);
6126
+ const override = list(env.DAHRK_RUNTIMES).filter(isRuntime2);
5614
6127
  if (override.length > 0) return override;
5615
6128
  if ((env.DAHRK_RUNNER ?? "real") === "mock") return ["claude-code"];
5616
6129
  return detectRuntimes();
@@ -5620,7 +6133,7 @@ function buildEdgeOptions(env, resolved) {
5620
6133
  const servesRepoIds = list(env.DAHRK_REPOS);
5621
6134
  const credentialModeExplicit = env.DAHRK_CREDENTIAL_MODE != null;
5622
6135
  const credentialMode = env.DAHRK_CREDENTIAL_MODE === "brokered" ? "brokered" : "ambient";
5623
- const envRuntimes = list(env.DAHRK_RUNTIMES).filter(isRuntime);
6136
+ const envRuntimes = list(env.DAHRK_RUNTIMES).filter(isRuntime2);
5624
6137
  const runtimes = resolved ? resolved.runtimes : envRuntimes.length > 0 ? envRuntimes : ["claude-code"];
5625
6138
  const nodeId = resolved?.nodeId ?? env.DAHRK_NODE_ID;
5626
6139
  const maxRuns = env.DAHRK_RETENTION_MAX_RUNS;
@@ -5715,6 +6228,18 @@ async function startForeground(env, flags) {
5715
6228
  const token = resolveEnrolToken(env, { ephemeral: flags.ephemeral });
5716
6229
  if (token) env.DAHRK_ENROL_TOKEN = token;
5717
6230
  const runtimes = await resolveRuntimes(env);
6231
+ if (!flags.ephemeral) {
6232
+ const prior = readState(stateFile(env)).runtimes ?? [];
6233
+ const missing = prior.filter((r) => !runtimes.includes(r));
6234
+ if (missing.length > 0) {
6235
+ logger.warn(
6236
+ { prior, detected: runtimes, missing },
6237
+ `RUNTIMES_DEGRADED:${missing.join(",")} advertised last run but not detected now - the node will serve no Jobs for it until it is re-probed or restarted. Run \`dahrk doctor\` to check.`
6238
+ );
6239
+ }
6240
+ writeState(env, { runtimes });
6241
+ }
6242
+ logger.info({ runtimes }, `RUNTIMES_DETECTED:${runtimes.join(",") || "none"}`);
5718
6243
  if (runtimes.length === 0) {
5719
6244
  console.warn(
5720
6245
  "no agent runtimes detected on this host (claude/codex/pi not on PATH); the node will advertise none and serve no Jobs. Install a runtime or set DAHRK_RUNTIMES to override. Run `dahrk doctor` to check."
@@ -5728,6 +6253,11 @@ async function startForeground(env, flags) {
5728
6253
  ...buildEdgeOptions(env, resolved),
5729
6254
  logger,
5730
6255
  shipper,
6256
+ // Self-heal a transiently-degraded boot without a manual restart: the edge re-probes on this seam
6257
+ // and re-advertises when the set changes. Reuses `resolveRuntimes`, so `DAHRK_RUNTIMES` still wins
6258
+ // (a pinned override never re-probes to something else) and the mock runner path stays stable.
6259
+ reprobeRuntimes: () => resolveRuntimes(env),
6260
+ ...env.DAHRK_RUNTIME_RECHECK_MS ? { runtimeRecheckMs: Number(env.DAHRK_RUNTIME_RECHECK_MS) } : {},
5731
6261
  ...persist ? {
5732
6262
  onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
5733
6263
  } : {}
@@ -5783,15 +6313,15 @@ function scheduleUpdateChecks(env) {
5783
6313
  function statusDeps(env) {
5784
6314
  return {
5785
6315
  platform: osPlatform4(),
5786
- homeDir: homedir5(),
6316
+ homeDir: homedir7(),
5787
6317
  env,
5788
6318
  binPath: process.argv[1],
5789
6319
  detectRuntimes: () => resolveRuntimes(env),
5790
- fileExists: (path) => existsSync12(path),
6320
+ fileExists: (path) => existsSync13(path),
5791
6321
  capture: (argv) => {
5792
6322
  const [cmd, ...args] = argv;
5793
6323
  try {
5794
- const stdout = execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
6324
+ const stdout = execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
5795
6325
  return { code: 0, stdout };
5796
6326
  } catch (e) {
5797
6327
  const status = e.status;
@@ -5932,7 +6462,7 @@ var invokedAsEntrypoint = (() => {
5932
6462
  const argv1 = process.argv[1];
5933
6463
  if (!argv1) return false;
5934
6464
  try {
5935
- return pathToFileURL(realpathSync4(argv1)).href === import.meta.url;
6465
+ return pathToFileURL(realpathSync5(argv1)).href === import.meta.url;
5936
6466
  } catch {
5937
6467
  return false;
5938
6468
  }