dahrk-node 0.1.10 → 0.1.11

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 +613 -158
  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);
@@ -518,6 +520,22 @@ function buildBrokeredMcpServers(ctx) {
518
520
  for (const s of servers) entries[s.id] = { type: s.type, url: `${ctx.mcpProxyBaseUrl}/${s.id}` };
519
521
  return entries;
520
522
  }
523
+ function sandboxOptions(ctx) {
524
+ if (process.env.DAHRK_SANDBOX !== "1") return {};
525
+ const home = homedir();
526
+ return {
527
+ sandbox: {
528
+ enabled: true,
529
+ failIfUnavailable: false,
530
+ autoAllowBashIfSandboxed: false,
531
+ allowUnsandboxedCommands: false,
532
+ filesystem: {
533
+ allowWrite: [ctx.workspace.worktreePath, ctx.workspace.scratchPath, tmpdir()],
534
+ denyRead: [join2(home, ".ssh"), join2(home, ".aws"), join2(home, ".gnupg"), "/Volumes"]
535
+ }
536
+ }
537
+ };
538
+ }
521
539
  function createClaudeRunner() {
522
540
  const abortController = new AbortController();
523
541
  let cancelled = false;
@@ -533,7 +551,25 @@ function createClaudeRunner() {
533
551
  // is a Claude Code SETTINGS key (not a top-level Options field), so it rides the inline `settings`
534
552
  // object, which sits at the flag layer and overrides any project/local setting. Mirrors the cyrus
535
553
  // reference (EdgeWorker.ts writes the same key into .claude/settings.local.json).
536
- settings: { includeCoAuthoredBy: false },
554
+ settings: {
555
+ includeCoAuthoredBy: false,
556
+ // DHK-392, defence in depth. The stage runner's `fs_confine` builtin is the real block (it is
557
+ // what `canUseTool` below consults, and unlike these rules it covers Grep and Bash). These deny
558
+ // rules close the same door from inside Claude Code, for the tools its permission system does
559
+ // cover (Read/Glob/NotebookRead via `Read(...)`, Write/Edit via `Edit(...)`): credentials an
560
+ // agent has no business reading, and the mounted volumes whose scan started all this. Deny
561
+ // outranks allow, so a repo's own .claude/settings.json cannot undo them.
562
+ permissions: {
563
+ deny: [
564
+ "Read(//Volumes/**)",
565
+ "Read(~/.ssh/**)",
566
+ "Read(~/.aws/**)",
567
+ "Read(~/.gnupg/**)",
568
+ "Read(~/Library/Keychains/**)"
569
+ ]
570
+ }
571
+ },
572
+ ...sandboxOptions(ctx),
537
573
  // Inherit the REPO's .mcp.json / .claude settings (build spec section 9): do NOT set
538
574
  // strictMcpConfig. Policy enforcement around tools is M6; M4 allows tools to run and the
539
575
  // stage runner intercepts denied actions at the trace level.
@@ -1332,8 +1368,8 @@ var PROVIDER_BY_ENV = {
1332
1368
  // ../../packages/executor-worktree/src/git-service.ts
1333
1369
  import { execFileSync } from "child_process";
1334
1370
  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";
1371
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1372
+ import { basename, dirname, isAbsolute, join as join3 } from "path";
1337
1373
  var noopLogger = { info: () => {
1338
1374
  }, warn: () => {
1339
1375
  } };
@@ -1347,10 +1383,10 @@ function sanitizeBranchName(name) {
1347
1383
  return name.replace(/[`~^:?*[\]\\@{}\s]/g, "-").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/\.lock(\/|$)/g, "$1").replace(/^[.\-/]+/, "").replace(/[.\-/]+$/, "").replace(/-{2,}/g, "-");
1348
1384
  }
1349
1385
  function resolveWorktreesDir(override) {
1350
- return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join2(homedir(), ".dahrk", "worktrees");
1386
+ return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join3(homedir2(), ".dahrk", "worktrees");
1351
1387
  }
1352
1388
  function resolveMirrorsDir(override) {
1353
- return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join2(homedir(), ".dahrk", "mirrors");
1389
+ return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join3(homedir2(), ".dahrk", "mirrors");
1354
1390
  }
1355
1391
  function createGitService(opts = {}) {
1356
1392
  const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
@@ -1389,12 +1425,12 @@ function createGitService(opts = {}) {
1389
1425
  const entry = `${SCRATCH_DIR}/`;
1390
1426
  try {
1391
1427
  const rel = git(worktreePath, ["rev-parse", "--git-path", "info/exclude"]).trim();
1392
- const excludePath = isAbsolute(rel) ? rel : join2(worktreePath, rel);
1428
+ const excludePath = isAbsolute(rel) ? rel : join3(worktreePath, rel);
1393
1429
  const existing = existsSync(excludePath) ? readFileSync2(excludePath, "utf-8") : "";
1394
1430
  if (existing.split("\n").some((l) => l.trim() === entry)) return;
1395
1431
  mkdirSync(dirname(excludePath), { recursive: true });
1396
- const sep3 = existing && !existing.endsWith("\n") ? "\n" : "";
1397
- writeFileSync(excludePath, `${existing}${sep3}${entry}
1432
+ const sep4 = existing && !existing.endsWith("\n") ? "\n" : "";
1433
+ writeFileSync(excludePath, `${existing}${sep4}${entry}
1398
1434
  `);
1399
1435
  } catch (e) {
1400
1436
  log.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
@@ -1419,8 +1455,8 @@ function createGitService(opts = {}) {
1419
1455
  return { headSha: git(worktreePath, ["rev-parse", "HEAD"]).trim(), dirty };
1420
1456
  };
1421
1457
  const setupAuth = (token) => {
1422
- const dir = mkdtempSync(join2(tmpdir(), "dahrk-cred-"));
1423
- const script = join2(dir, "askpass.sh");
1458
+ const dir = mkdtempSync(join3(tmpdir2(), "dahrk-cred-"));
1459
+ const script = join3(dir, "askpass.sh");
1424
1460
  writeFileSync(script, '#!/bin/sh\nprintf "%s" "$DAHRK_GIT_TOKEN"\n', { mode: 448 });
1425
1461
  return {
1426
1462
  env: { ...process.env, GIT_ASKPASS: script, DAHRK_GIT_TOKEN: token, GIT_TERMINAL_PROMPT: "0" },
@@ -1429,7 +1465,7 @@ function createGitService(opts = {}) {
1429
1465
  };
1430
1466
  const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
1431
1467
  const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
1432
- const mirrorPathFor = (repoId) => join2(mirrorsDir, sanitizeBranchName(repoId));
1468
+ const mirrorPathFor = (repoId) => join3(mirrorsDir, sanitizeBranchName(repoId));
1433
1469
  const listWorktrees = (mirror) => {
1434
1470
  let out;
1435
1471
  try {
@@ -1540,18 +1576,18 @@ function createGitService(opts = {}) {
1540
1576
  repo: spec.repo ?? spec.repoId,
1541
1577
  baseBranch: spec.baseBranch,
1542
1578
  worktreePath,
1543
- scratchPath: join2(worktreePath, ".skakel", "scratch")
1579
+ scratchPath: join3(worktreePath, ".skakel", "scratch")
1544
1580
  });
1545
1581
  return {
1546
1582
  worktreesDir,
1547
1583
  async createWorktree(spec) {
1548
1584
  const { repoId, gitUrl, baseBranch, runId } = spec;
1549
1585
  const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
1550
- const worktreePath = join2(worktreesDir, runId);
1586
+ const worktreePath = join3(worktreesDir, runId);
1551
1587
  mkdirSync(worktreesDir, { recursive: true });
1552
1588
  if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
1553
1589
  log.info(`reusing existing worktree at ${worktreePath}`);
1554
- mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
1590
+ mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1555
1591
  return refFor(spec, worktreePath);
1556
1592
  }
1557
1593
  const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
@@ -1587,7 +1623,7 @@ function createGitService(opts = {}) {
1587
1623
  if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
1588
1624
  throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
1589
1625
  }
1590
- mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
1626
+ mkdirSync(join3(worktreePath, ".skakel", "scratch"), { recursive: true });
1591
1627
  return refFor(spec, worktreePath);
1592
1628
  },
1593
1629
  async commitAndPush(ref, opts2) {
@@ -1693,8 +1729,8 @@ function createGitService(opts = {}) {
1693
1729
  return void 0;
1694
1730
  }
1695
1731
  };
1696
- const tmp = mkdtempSync(join2(tmpdir(), "dahrk-pr-"));
1697
- const bodyFile = join2(tmp, "body.md");
1732
+ const tmp = mkdtempSync(join3(tmpdir2(), "dahrk-pr-"));
1733
+ const bodyFile = join3(tmp, "body.md");
1698
1734
  try {
1699
1735
  writeFileSync(bodyFile, opts2.body ?? "");
1700
1736
  try {
@@ -1730,7 +1766,7 @@ function createGitService(opts = {}) {
1730
1766
  // ../../packages/executor-worktree/src/worktree-reaper.ts
1731
1767
  import { execFileSync as execFileSync2 } from "child_process";
1732
1768
  import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync2, statSync } from "fs";
1733
- import { join as join3 } from "path";
1769
+ import { join as join4 } from "path";
1734
1770
  var MINUTE = 6e4;
1735
1771
  var HOUR = 60 * MINUTE;
1736
1772
  var DEFAULTS = {
@@ -1760,7 +1796,7 @@ var canonical = (p) => {
1760
1796
  }
1761
1797
  };
1762
1798
  function lastUsedMs(worktreePath) {
1763
- const candidates = [join3(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
1799
+ const candidates = [join4(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
1764
1800
  let newest = 0;
1765
1801
  for (const p of candidates) {
1766
1802
  try {
@@ -1783,7 +1819,7 @@ function createWorktreeReaper(opts) {
1783
1819
  const log = opts.logger ?? noop;
1784
1820
  const mirrors = () => {
1785
1821
  try {
1786
- return readdirSync(opts.mirrorsDir).map((d) => join3(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
1822
+ return readdirSync(opts.mirrorsDir).map((d) => join4(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
1787
1823
  } catch {
1788
1824
  return [];
1789
1825
  }
@@ -1807,7 +1843,7 @@ function createWorktreeReaper(opts) {
1807
1843
  }
1808
1844
  const onDisk = (() => {
1809
1845
  try {
1810
- return readdirSync(opts.worktreesDir).map((d) => canonical(join3(opts.worktreesDir, d)));
1846
+ return readdirSync(opts.worktreesDir).map((d) => canonical(join4(opts.worktreesDir, d)));
1811
1847
  } catch {
1812
1848
  return [];
1813
1849
  }
@@ -1868,15 +1904,15 @@ function createWorktreeReaper(opts) {
1868
1904
  // ../../packages/executor-worktree/src/trace-writer.ts
1869
1905
  import { createHash } from "crypto";
1870
1906
  import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1871
- import { join as join4 } from "path";
1907
+ import { join as join5 } from "path";
1872
1908
  var DEFAULT_SPILL_BYTES = 8192;
1873
1909
  function createTraceWriter(scratchPath, meta, opts = {}) {
1874
1910
  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");
1911
+ const dir = join5(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
1912
+ mkdirSync2(join5(dir, "blobs"), { recursive: true });
1913
+ mkdirSync2(join5(dir, "raw"), { recursive: true });
1914
+ const tracePath = join5(dir, "trace.jsonl");
1915
+ const metaPath = join5(dir, "meta.json");
1880
1916
  let current = { ...meta };
1881
1917
  let nextSeq = 0;
1882
1918
  let rawCount = 0;
@@ -1888,8 +1924,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1888
1924
  const spillValue = (value) => {
1889
1925
  const data = typeof value === "string" ? value : JSON.stringify(value);
1890
1926
  const sha = createHash("sha256").update(data).digest("hex");
1891
- writeFileSync2(join4(dir, "blobs", sha), data);
1892
- return join4("blobs", sha);
1927
+ writeFileSync2(join5(dir, "blobs", sha), data);
1928
+ return join5("blobs", sha);
1893
1929
  };
1894
1930
  const spill = (event) => {
1895
1931
  if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
@@ -1919,8 +1955,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1919
1955
  return written;
1920
1956
  },
1921
1957
  writeRaw(record) {
1922
- const rel = join4("raw", `${rawCount++}.json`);
1923
- writeFileSync2(join4(dir, rel), JSON.stringify(record, null, 2));
1958
+ const rel = join5("raw", `${rawCount++}.json`);
1959
+ writeFileSync2(join5(dir, rel), JSON.stringify(record, null, 2));
1924
1960
  return rel;
1925
1961
  },
1926
1962
  finalise(patch = {}) {
@@ -1936,12 +1972,12 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
1936
1972
  // ../../packages/executor-worktree/src/pack-cache.ts
1937
1973
  import { createHash as createHash2 } from "crypto";
1938
1974
  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";
1975
+ import { dirname as dirname2, join as join6, relative, sep } from "path";
1940
1976
  function readManifestFiles(dir) {
1941
1977
  const out = [];
1942
1978
  const walk = (cur) => {
1943
1979
  for (const entry of readdirSync2(cur, { withFileTypes: true })) {
1944
- const abs = join5(cur, entry.name);
1980
+ const abs = join6(cur, entry.name);
1945
1981
  if (entry.isDirectory()) walk(abs);
1946
1982
  else out.push(relative(dir, abs).split(sep).join("/"));
1947
1983
  }
@@ -1952,7 +1988,7 @@ function readManifestFiles(dir) {
1952
1988
 
1953
1989
  // ../../packages/executor-worktree/src/overlay.ts
1954
1990
  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";
1991
+ import { dirname as dirname3, join as join7 } from "path";
1956
1992
  function sameBytes(dest, bytes) {
1957
1993
  try {
1958
1994
  return readFileSync3(dest).equals(bytes);
@@ -1972,8 +2008,8 @@ async function overlayComponents(opts) {
1972
2008
  }
1973
2009
  const { dir } = await cache.materialise(ref);
1974
2010
  for (const relPath of readManifestFiles(dir)) {
1975
- const src = join6(dir, relPath);
1976
- const dest = join6(worktreePath, relPath);
2011
+ const src = join7(dir, relPath);
2012
+ const dest = join7(worktreePath, relPath);
1977
2013
  const bytes = readFileSync3(src);
1978
2014
  if (existsSync4(dest)) {
1979
2015
  if (sameBytes(dest, bytes)) continue;
@@ -2160,7 +2196,7 @@ function ceilingFromEnv(env) {
2160
2196
 
2161
2197
  // ../../packages/edge/src/logger.ts
2162
2198
  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";
2199
+ import { join as join8 } from "path";
2164
2200
  import pino from "pino";
2165
2201
 
2166
2202
  // ../../packages/edge/src/redact.ts
@@ -2350,7 +2386,7 @@ function createNodeLogger(opts = {}) {
2350
2386
  if (opts.dir && fileLevel !== "silent") {
2351
2387
  try {
2352
2388
  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);
2389
+ const file = new RotatingFile(join8(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
2354
2390
  streams.push({ level: fileLevel, stream: file });
2355
2391
  } catch (e) {
2356
2392
  process.stderr.write(`dahrk: file logging disabled (${e.message})
@@ -2406,15 +2442,377 @@ function denyToolRule(tool3) {
2406
2442
  }
2407
2443
 
2408
2444
  // ../../packages/edge/src/stage-runner.ts
2409
- import { execFileSync as execFileSync4 } from "child_process";
2445
+ import { execFileSync as execFileSync5 } from "child_process";
2410
2446
  import { createHash as createHash3 } from "crypto";
2411
2447
  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";
2448
+ import { tmpdir as tmpdir4 } from "os";
2449
+ import { isAbsolute as isAbsolute3, join as join10, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
2414
2450
  import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
2415
2451
 
2416
2452
  // ../../packages/edge/src/builtins.ts
2453
+ import { execFileSync as execFileSync4 } from "child_process";
2454
+
2455
+ // ../../packages/edge/src/fs-roots.ts
2417
2456
  import { execFileSync as execFileSync3 } from "child_process";
2457
+ import { existsSync as existsSync6, realpathSync as realpathSync2 } from "fs";
2458
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
2459
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve, sep as sep2 } from "path";
2460
+ function isUnder(root, target) {
2461
+ const rel = relative2(resolve(root), resolve(target));
2462
+ return rel === "" || !rel.startsWith(`..${sep2}`) && rel !== ".." && !isAbsolute2(rel);
2463
+ }
2464
+ function expandPath(raw, cwd) {
2465
+ const home = homedir3();
2466
+ const expanded = raw.replace(/^~(?=$|\/)/, home).replace(/^\$\{HOME\}(?=$|\/)/, home).replace(/^\$HOME(?=$|\/)/, home);
2467
+ return resolve(cwd, expanded);
2468
+ }
2469
+ function realish(p) {
2470
+ let head = p;
2471
+ const tail = [];
2472
+ while (head !== dirname4(head)) {
2473
+ if (existsSync6(head)) {
2474
+ try {
2475
+ return join9(realpathSync2.native(head), ...tail.reverse());
2476
+ } catch {
2477
+ return p;
2478
+ }
2479
+ }
2480
+ tail.push(head.slice(dirname4(head).length + 1));
2481
+ head = dirname4(head);
2482
+ }
2483
+ return p;
2484
+ }
2485
+ function withinRoots(raw, roots, need) {
2486
+ const literal = expandPath(raw, roots.cwd);
2487
+ const real = realish(literal);
2488
+ const hits = (list2) => list2.some((root) => isUnder(root, literal) || isUnder(root, real));
2489
+ if (hits(roots.deny)) return false;
2490
+ if (hits(roots.rw)) return true;
2491
+ return need === "read" && hits(roots.ro);
2492
+ }
2493
+ function gitCommonDir(worktreePath) {
2494
+ try {
2495
+ const out = execFileSync3("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
2496
+ cwd: worktreePath,
2497
+ stdio: ["ignore", "pipe", "ignore"]
2498
+ }).toString().trim();
2499
+ return out || void 0;
2500
+ } catch {
2501
+ return void 0;
2502
+ }
2503
+ }
2504
+ var pnpmStoreCache;
2505
+ function pnpmStore() {
2506
+ if (pnpmStoreCache) return pnpmStoreCache.path;
2507
+ try {
2508
+ const out = execFileSync3("pnpm", ["store", "path"], { stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).toString().trim();
2509
+ pnpmStoreCache = { path: out || void 0 };
2510
+ } catch {
2511
+ pnpmStoreCache = { path: void 0 };
2512
+ }
2513
+ return pnpmStoreCache.path;
2514
+ }
2515
+ var splitRoots = (v) => (v ?? "").split(":").map((s) => s.trim()).filter(Boolean);
2516
+ function computeFsRoots(opts) {
2517
+ const home = homedir3();
2518
+ const wt = realish(resolve(opts.worktreePath));
2519
+ const rw = [
2520
+ wt,
2521
+ ...opts.scratchPath ? [realish(resolve(opts.scratchPath))] : [],
2522
+ // Every git command in the worktree reads and writes here.
2523
+ ...[gitCommonDir(opts.worktreePath)].filter((p) => Boolean(p)).map(realish),
2524
+ // Scratch space: `mkdtemp`, git's temp files, and an agent tidying a throwaway dir it created.
2525
+ ...[tmpdir3(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].map(realish),
2526
+ // The safe I/O sinks. `2>/dev/null` is on a third of the shell commands real stages run; treating
2527
+ // it as a write outside the worktree would deny most of a normal build. The raw devices are NOT
2528
+ // here, so `> /dev/sda` still fails confinement (as well as shell_guard's device-write regex).
2529
+ ...["/dev/null", "/dev/stdout", "/dev/stderr", "/dev/tty", "/dev/fd"],
2530
+ ...[pnpmStore()].filter((p) => Boolean(p)).map(realish),
2531
+ ...splitRoots(process.env.DAHRK_FS_EXTRA_ROOTS).map((p) => realish(resolve(p)))
2532
+ ];
2533
+ const ro = [
2534
+ "/usr",
2535
+ "/bin",
2536
+ "/sbin",
2537
+ "/etc",
2538
+ "/opt",
2539
+ "/nix",
2540
+ "/Library",
2541
+ "/System",
2542
+ "/proc",
2543
+ "/sys",
2544
+ join9(home, ".gitconfig"),
2545
+ join9(home, ".config"),
2546
+ join9(home, ".npmrc"),
2547
+ join9(home, ".cache"),
2548
+ join9(home, "Library", "Caches"),
2549
+ join9(home, "Library", "pnpm"),
2550
+ join9(home, ".local", "share"),
2551
+ join9(home, ".nvm"),
2552
+ join9(home, ".volta"),
2553
+ join9(home, ".asdf"),
2554
+ join9(home, ".cargo"),
2555
+ join9(home, ".rustup"),
2556
+ ...splitRoots(process.env.DAHRK_FS_EXTRA_READ_ROOTS).map((p) => realish(resolve(p)))
2557
+ ].map(realish);
2558
+ const deny = [
2559
+ join9(home, ".ssh"),
2560
+ join9(home, ".aws"),
2561
+ join9(home, ".gnupg"),
2562
+ join9(home, ".config", "gcloud"),
2563
+ join9(home, "Library", "Keychains"),
2564
+ "/Volumes",
2565
+ "/etc/shadow",
2566
+ "/etc/sudoers"
2567
+ ];
2568
+ return { rw, ro, deny, cwd: wt };
2569
+ }
2570
+
2571
+ // ../../packages/edge/src/shell-scan.ts
2572
+ var PATTERN_FLAGS = /* @__PURE__ */ new Set([
2573
+ "-e",
2574
+ "--regexp",
2575
+ "-path",
2576
+ "-ipath",
2577
+ "-name",
2578
+ "-iname",
2579
+ "-wholename",
2580
+ "--glob",
2581
+ "-g",
2582
+ "--include",
2583
+ "--exclude",
2584
+ "--exclude-dir",
2585
+ "--type",
2586
+ "-m",
2587
+ "--message"
2588
+ ]);
2589
+ var PATTERN_FIRST = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "ag", "ack", "sed", "awk", "perl"]);
2590
+ var DIR_FLAGS = /* @__PURE__ */ new Set(["-C", "--git-dir", "--work-tree", "--cwd", "--directory"]);
2591
+ var NO_PATH_ARGV0 = /* @__PURE__ */ new Set(["echo", "printf", ":", "true", "false"]);
2592
+ var WRITE_ALL = /* @__PURE__ */ new Set(["rm", "rmdir", "mkdir", "touch", "chmod", "chown", "truncate", "mkfifo"]);
2593
+ var WRITE_LAST = /* @__PURE__ */ new Set(["cp", "mv", "ln", "tee", "dd", "install", "rsync"]);
2594
+ var SHELL_ARGV0 = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "eval", "xargs", "env", "nohup", "time", "sudo"]);
2595
+ var REDIRECTS = /* @__PURE__ */ new Set([">", ">>", "<", "2>", "2>>", "&>", "<<<"]);
2596
+ function tokenise(cmd) {
2597
+ const tokens = [];
2598
+ const subshells = [];
2599
+ let cur = "";
2600
+ let quoted = false;
2601
+ let started = false;
2602
+ let quote = null;
2603
+ const push = () => {
2604
+ if (started) tokens.push({ value: cur, quoted });
2605
+ cur = "";
2606
+ quoted = false;
2607
+ started = false;
2608
+ };
2609
+ for (let i = 0; i < cmd.length; i++) {
2610
+ const c = cmd[i];
2611
+ if (quote) {
2612
+ if (c === quote) {
2613
+ quote = null;
2614
+ } else if (c === "\\" && quote === '"' && i + 1 < cmd.length) {
2615
+ cur += cmd[++i];
2616
+ started = true;
2617
+ } else {
2618
+ cur += c;
2619
+ started = true;
2620
+ }
2621
+ continue;
2622
+ }
2623
+ if (c === '"' || c === "'") {
2624
+ quote = c;
2625
+ quoted = true;
2626
+ started = true;
2627
+ continue;
2628
+ }
2629
+ if (c === "\\" && i + 1 < cmd.length) {
2630
+ cur += cmd[++i];
2631
+ started = true;
2632
+ continue;
2633
+ }
2634
+ if (c === "$" && cmd[i + 1] === "(" || c === "`") {
2635
+ const open = c === "`" ? "`" : "(";
2636
+ const close = c === "`" ? "`" : ")";
2637
+ let depth = 1;
2638
+ let j = i + (c === "`" ? 1 : 2);
2639
+ let body = "";
2640
+ for (; j < cmd.length && depth > 0; j++) {
2641
+ const d = cmd[j];
2642
+ if (d === open && close !== "`") depth++;
2643
+ else if (d === close) {
2644
+ depth--;
2645
+ if (depth === 0) break;
2646
+ }
2647
+ body += d;
2648
+ }
2649
+ if (depth > 0) return null;
2650
+ subshells.push(body);
2651
+ cur += "$SUBSHELL";
2652
+ started = true;
2653
+ i = j;
2654
+ continue;
2655
+ }
2656
+ if (/\s/.test(c)) {
2657
+ push();
2658
+ continue;
2659
+ }
2660
+ const three = cmd.slice(i, i + 3);
2661
+ const two = cmd.slice(i, i + 2);
2662
+ if (three === "<<<") {
2663
+ push();
2664
+ tokens.push({ value: three, quoted: false, operator: three });
2665
+ i += 2;
2666
+ continue;
2667
+ }
2668
+ if (["&&", "||", ">>", "2>", "&>"].includes(two)) {
2669
+ push();
2670
+ tokens.push({ value: two, quoted: false, operator: two });
2671
+ i += 1;
2672
+ continue;
2673
+ }
2674
+ if (c === ";" || c === "|" || c === "&" || c === ">" || c === "<" || c === "\n") {
2675
+ push();
2676
+ tokens.push({ value: c, quoted: false, operator: c });
2677
+ continue;
2678
+ }
2679
+ cur += c;
2680
+ started = true;
2681
+ }
2682
+ if (quote) return null;
2683
+ push();
2684
+ return { tokens, subshells };
2685
+ }
2686
+ function looksLikePath(t) {
2687
+ if (t.value.startsWith("-")) return false;
2688
+ if (/\s/.test(t.value)) return false;
2689
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(t.value)) return false;
2690
+ const s = t.value.replace(/^@/, "");
2691
+ if (s.includes("$SUBSHELL")) return false;
2692
+ if (/^\$(?!\{?HOME\b)/.test(s)) return false;
2693
+ return /^(\/|~(?:$|\/)|\$\{?HOME\}?(?:$|\/)|\.\.(?:$|\/))/.test(s) || s.includes("/../") || s.endsWith("/..");
2694
+ }
2695
+ var bare = (v) => v.replace(/^@/, "");
2696
+ function scanSimple(tokens, roots, cwd) {
2697
+ const scoped = { ...roots, cwd };
2698
+ const check = (raw, need) => withinRoots(bare(raw), scoped, need) ? null : { kind: "escape", path: bare(raw), need };
2699
+ const argv0 = tokens.find((t) => !t.operator && !t.value.includes("="))?.value ?? "";
2700
+ const cmdName = argv0.split("/").pop() ?? argv0;
2701
+ if (SHELL_ARGV0.has(cmdName)) {
2702
+ for (const t of tokens) {
2703
+ if (t.operator || t.value === argv0) continue;
2704
+ if (t.quoted || t.value.includes(" ")) {
2705
+ const inner = scanCommand(t.value, roots, cwd);
2706
+ if (inner.kind !== "ok") return inner;
2707
+ } else if (looksLikePath(t)) {
2708
+ const bad = check(t.value, "read");
2709
+ if (bad) return bad;
2710
+ }
2711
+ }
2712
+ return { kind: "ok" };
2713
+ }
2714
+ if (NO_PATH_ARGV0.has(cmdName)) {
2715
+ return { kind: "ok" };
2716
+ }
2717
+ const writeAll = WRITE_ALL.has(cmdName);
2718
+ const writeLast = WRITE_LAST.has(cmdName);
2719
+ const operands = [];
2720
+ let patternPending = PATTERN_FIRST.has(cmdName);
2721
+ for (let i = 0; i < tokens.length; i++) {
2722
+ const t = tokens[i];
2723
+ if (t.operator) continue;
2724
+ if (t.value === argv0 && i === tokens.indexOf(t)) {
2725
+ if (looksLikePath(t)) {
2726
+ const bad = check(t.value, "read");
2727
+ if (bad) return bad;
2728
+ }
2729
+ continue;
2730
+ }
2731
+ if (DIR_FLAGS.has(t.value)) {
2732
+ const operand = tokens[i + 1];
2733
+ if (operand && !operand.operator) {
2734
+ const bad = check(operand.value, "read");
2735
+ if (bad) return bad;
2736
+ i++;
2737
+ }
2738
+ continue;
2739
+ }
2740
+ if (PATTERN_FLAGS.has(t.value)) {
2741
+ i++;
2742
+ continue;
2743
+ }
2744
+ if (t.value.startsWith("-")) continue;
2745
+ if (patternPending) {
2746
+ patternPending = false;
2747
+ continue;
2748
+ }
2749
+ if (looksLikePath(t)) operands.push({ token: t, index: i });
2750
+ }
2751
+ if (cmdName === "cd") {
2752
+ const target = tokens.find((t) => !t.operator && t.value !== argv0 && !t.value.startsWith("-"));
2753
+ if (!target) return { kind: "ok" };
2754
+ const bad = check(target.value, "read");
2755
+ if (bad) return bad;
2756
+ return { kind: "cd", to: target.value };
2757
+ }
2758
+ for (let n = 0; n < operands.length; n++) {
2759
+ const entry = operands[n];
2760
+ const isLast = n === operands.length - 1;
2761
+ const need = writeAll || writeLast && isLast ? "write" : "read";
2762
+ const bad = check(entry.token.value, need);
2763
+ if (bad) return bad;
2764
+ }
2765
+ return { kind: "ok" };
2766
+ }
2767
+ function scanCommand(cmd, roots, cwd = roots.cwd) {
2768
+ if (!cmd.trim()) return { kind: "ok" };
2769
+ const lexed = tokenise(cmd);
2770
+ if (!lexed) return { kind: "unparseable", reason: "unbalanced quotes or unterminated substitution" };
2771
+ for (const body of lexed.subshells) {
2772
+ const inner = scanCommand(body, roots, cwd);
2773
+ if (inner.kind !== "ok") return inner;
2774
+ }
2775
+ let current = [];
2776
+ let cwdNow = cwd;
2777
+ const segments = [];
2778
+ for (const t of lexed.tokens) {
2779
+ if (t.operator && [";", "|", "&", "&&", "||", "\n"].includes(t.operator)) {
2780
+ segments.push(current);
2781
+ current = [];
2782
+ continue;
2783
+ }
2784
+ current.push(t);
2785
+ }
2786
+ segments.push(current);
2787
+ for (const seg of segments) {
2788
+ if (seg.length === 0) continue;
2789
+ const plain = [];
2790
+ for (let i = 0; i < seg.length; i++) {
2791
+ const t = seg[i];
2792
+ if (t.operator && REDIRECTS.has(t.operator)) {
2793
+ const target = seg[i + 1];
2794
+ if (target && !target.operator) {
2795
+ const need = t.operator === "<" || t.operator === "<<<" ? "read" : "write";
2796
+ if (!withinRoots(bare(target.value), { ...roots, cwd: cwdNow }, need)) {
2797
+ return { kind: "escape", path: bare(target.value), need };
2798
+ }
2799
+ i++;
2800
+ }
2801
+ continue;
2802
+ }
2803
+ plain.push(t);
2804
+ }
2805
+ const out = scanSimple(plain, roots, cwdNow);
2806
+ if (out.kind === "cd") {
2807
+ cwdNow = expandPath(out.to, cwdNow);
2808
+ continue;
2809
+ }
2810
+ if (out.kind !== "ok") return out;
2811
+ }
2812
+ return { kind: "ok" };
2813
+ }
2814
+
2815
+ // ../../packages/edge/src/builtins.ts
2418
2816
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
2419
2817
  "Write",
2420
2818
  "Edit",
@@ -2456,7 +2854,7 @@ function isDangerousRm(cmd) {
2456
2854
  }
2457
2855
  function currentBranch(worktreePath) {
2458
2856
  try {
2459
- return execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
2857
+ return execFileSync4("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
2460
2858
  } catch {
2461
2859
  return "";
2462
2860
  }
@@ -2475,9 +2873,54 @@ function commandOf(input) {
2475
2873
  }
2476
2874
  return "";
2477
2875
  }
2876
+ var PATH_FIELDS = ["file_path", "notebook_path", "path", "filePath", "cwd", "directory"];
2877
+ var PATH_WRITE_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "apply_patch"]);
2878
+ function pathsIn(input) {
2879
+ if (!input || typeof input !== "object") return [];
2880
+ const o = input;
2881
+ const out = [];
2882
+ for (const f of PATH_FIELDS) {
2883
+ if (typeof o[f] === "string" && o[f]) out.push(o[f]);
2884
+ }
2885
+ if (o.changes && typeof o.changes === "object") {
2886
+ out.push(...Object.keys(o.changes));
2887
+ }
2888
+ return out;
2889
+ }
2890
+ function fsConfineRule(roots) {
2891
+ return {
2892
+ name: "fs_confine",
2893
+ evaluate(event) {
2894
+ if (event.kind !== "action") return null;
2895
+ const deny = (path, need2) => ({
2896
+ verdict: "deny",
2897
+ policy: "fs_confine",
2898
+ reason: `${need2 === "write" ? "write to" : "read of"} "${path}" is outside the run's worktree`
2899
+ });
2900
+ if (SHELL_TOOLS.has(event.tool)) {
2901
+ const result = scanCommand(commandOf(event.input), roots);
2902
+ if (result.kind === "escape") return deny(result.path, result.need);
2903
+ if (result.kind === "unparseable") {
2904
+ return {
2905
+ verdict: "deny",
2906
+ policy: "fs_confine",
2907
+ reason: `shell command could not be parsed for path confinement (${result.reason})`
2908
+ };
2909
+ }
2910
+ return null;
2911
+ }
2912
+ const need = PATH_WRITE_TOOLS.has(event.tool) ? "write" : "read";
2913
+ for (const p of pathsIn(event.input)) {
2914
+ if (!withinRoots(p, roots, need)) return deny(p, need);
2915
+ }
2916
+ return null;
2917
+ }
2918
+ };
2919
+ }
2478
2920
  function buildRules(policies, ctx) {
2479
2921
  const rules = [];
2480
2922
  let stageToolCalls = 0;
2923
+ if (ctx.fsRoots && process.env.DAHRK_FS_CONFINE !== "0") rules.push(fsConfineRule(ctx.fsRoots));
2481
2924
  for (const p of policies) {
2482
2925
  if ("read_only" in p && p.read_only) {
2483
2926
  rules.push({
@@ -2592,11 +3035,11 @@ async function startMcpGateway(opts) {
2592
3035
  if (upstream.body) await pipeline(Readable.fromWeb(upstream.body), res);
2593
3036
  else res.end();
2594
3037
  };
2595
- await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
3038
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
2596
3039
  const { port } = server.address();
2597
3040
  return {
2598
3041
  baseUrl: `http://127.0.0.1:${port}`,
2599
- stop: () => new Promise((resolve2) => server.close(() => resolve2()))
3042
+ stop: () => new Promise((resolve3) => server.close(() => resolve3()))
2600
3043
  };
2601
3044
  }
2602
3045
 
@@ -2639,7 +3082,7 @@ var attemptOf = (jobId) => {
2639
3082
  };
2640
3083
  var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
2641
3084
  function writeScratchState(ref, job, attempt, status) {
2642
- const statePath = join8(ref.scratchPath, "state.json");
3085
+ const statePath = join10(ref.scratchPath, "state.json");
2643
3086
  let state;
2644
3087
  try {
2645
3088
  state = JSON.parse(readFileSync4(statePath, "utf8"));
@@ -2654,17 +3097,17 @@ function writeIssueContext(ref, issueContext) {
2654
3097
  if (issueContext === void 0) return;
2655
3098
  try {
2656
3099
  mkdirSync6(ref.scratchPath, { recursive: true });
2657
- writeFileSync5(join8(ref.scratchPath, "issue.md"), issueContext);
3100
+ writeFileSync5(join10(ref.scratchPath, "issue.md"), issueContext);
2658
3101
  } catch {
2659
3102
  }
2660
3103
  }
2661
3104
  function writeAttachedDocuments(ref, docs) {
2662
3105
  if (!docs || docs.length === 0) return;
2663
3106
  try {
2664
- const dir = join8(ref.scratchPath, "docs");
3107
+ const dir = join10(ref.scratchPath, "docs");
2665
3108
  mkdirSync6(dir, { recursive: true });
2666
3109
  for (const doc of docs) {
2667
- writeFileSync5(join8(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
3110
+ writeFileSync5(join10(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
2668
3111
  }
2669
3112
  } catch {
2670
3113
  }
@@ -2683,7 +3126,7 @@ function writeGuidance(ref, guidance) {
2683
3126
  if (!guidance || guidance.length === 0) return;
2684
3127
  try {
2685
3128
  mkdirSync6(ref.scratchPath, { recursive: true });
2686
- writeFileSync5(join8(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
3129
+ writeFileSync5(join10(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
2687
3130
  } catch {
2688
3131
  }
2689
3132
  }
@@ -2693,11 +3136,11 @@ function capContent(raw) {
2693
3136
  return raw.length > ARTIFACT_CAP_BYTES ? raw.slice(0, ARTIFACT_CAP_BYTES) : raw;
2694
3137
  }
2695
3138
  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)) {
3139
+ if (isAbsolute3(relPath)) return void 0;
3140
+ const root = resolve2(ref.worktreePath);
3141
+ const target = resolve2(root, relPath);
3142
+ const fromRoot = relative3(root, target);
3143
+ if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${sep3}`) || isAbsolute3(fromRoot)) {
2701
3144
  return void 0;
2702
3145
  }
2703
3146
  return target;
@@ -2717,13 +3160,13 @@ function readEmittedArtifact(ref, relPath) {
2717
3160
  }
2718
3161
  function scanScratchOutput(ref, preferRel) {
2719
3162
  try {
2720
- const names = readdirSync3(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
3163
+ const names = readdirSync3(join10(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
2721
3164
  (n) => n.toLowerCase().endsWith(".md")
2722
3165
  );
2723
3166
  if (names.length === 0) return void 0;
2724
3167
  const preferBase = preferRel?.split("/").pop();
2725
3168
  const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
2726
- const raw = readFileSync4(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
3169
+ const raw = readFileSync4(join10(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
2727
3170
  if (raw.trim().length === 0) return void 0;
2728
3171
  return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
2729
3172
  } catch {
@@ -2733,7 +3176,7 @@ function scanScratchOutput(ref, preferRel) {
2733
3176
  function scanChangedMarkdown(ref) {
2734
3177
  const git = (args) => {
2735
3178
  try {
2736
- return execFileSync4("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
3179
+ return execFileSync5("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
2737
3180
  } catch {
2738
3181
  return [];
2739
3182
  }
@@ -2743,7 +3186,7 @@ function scanChangedMarkdown(ref) {
2743
3186
  );
2744
3187
  for (const rel of rels) {
2745
3188
  try {
2746
- const raw = readFileSync4(join8(ref.worktreePath, rel), "utf8");
3189
+ const raw = readFileSync4(join10(ref.worktreePath, rel), "utf8");
2747
3190
  if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
2748
3191
  } catch {
2749
3192
  }
@@ -2866,9 +3309,9 @@ function createStageRunner(deps) {
2866
3309
  ...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
2867
3310
  });
2868
3311
  } else {
2869
- const base = deps.scratchRoot ?? join8(tmpdir2(), "dahrk", "scratch");
2870
- const worktreePath = join8(base, runId);
2871
- const scratchPath = join8(worktreePath, ".skakel", "scratch");
3312
+ const base = deps.scratchRoot ?? join10(tmpdir4(), "dahrk", "scratch");
3313
+ const worktreePath = join10(base, runId);
3314
+ const scratchPath = join10(worktreePath, ".skakel", "scratch");
2872
3315
  mkdirSync6(scratchPath, { recursive: true });
2873
3316
  ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
2874
3317
  scratchOnly.add(runId);
@@ -2884,7 +3327,7 @@ function createStageRunner(deps) {
2884
3327
  const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
2885
3328
  if (artifactDir && slash > 0) {
2886
3329
  try {
2887
- mkdirSync6(join8(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
3330
+ mkdirSync6(join10(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
2888
3331
  } catch {
2889
3332
  }
2890
3333
  }
@@ -2912,8 +3355,8 @@ function createStageRunner(deps) {
2912
3355
  if (!sink) return;
2913
3356
  const base = { tenantId: job.tenantId, runId, stageId, attempt };
2914
3357
  try {
2915
- for (const name of readdirSync3(join8(writer.dir, "blobs"))) {
2916
- const bytes = readFileSync4(join8(writer.dir, "blobs", name));
3358
+ for (const name of readdirSync3(join10(writer.dir, "blobs"))) {
3359
+ const bytes = readFileSync4(join10(writer.dir, "blobs", name));
2917
3360
  const { url } = await sink.requestBlobUrl({
2918
3361
  ...base,
2919
3362
  sha256: name,
@@ -2927,7 +3370,7 @@ function createStageRunner(deps) {
2927
3370
  }
2928
3371
  let archiveKey;
2929
3372
  try {
2930
- const bytes = readFileSync4(join8(writer.dir, "trace.jsonl"));
3373
+ const bytes = readFileSync4(join10(writer.dir, "trace.jsonl"));
2931
3374
  const sha = createHash3("sha256").update(bytes).digest("hex");
2932
3375
  const { key, url } = await sink.requestBlobUrl({
2933
3376
  ...base,
@@ -3011,7 +3454,11 @@ function createStageRunner(deps) {
3011
3454
  const jobRules = buildRules(job.policies ?? [], {
3012
3455
  worktreePath: ref.worktreePath,
3013
3456
  repoName: job.workspaceRef?.repo ?? "",
3014
- runToolCalls: counter
3457
+ runToolCalls: counter,
3458
+ // DHK-392: the stage is confined to the run's worktree (plus its scratch dir, the git object
3459
+ // store it depends on, and the toolchain). A node default, not a workflow policy. A run with
3460
+ // no repo still gets a box - just a smaller one, around its scratch dir.
3461
+ fsRoots: computeFsRoots({ worktreePath: ref.worktreePath, scratchPath: ref.scratchPath })
3015
3462
  });
3016
3463
  const rules = [...jobRules, ...deps.rules];
3017
3464
  const entry = evaluatePolicies({ kind: "stage-entry", stageId }, rules);
@@ -3020,6 +3467,7 @@ function createStageRunner(deps) {
3020
3467
  return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
3021
3468
  }
3022
3469
  let denied = false;
3470
+ let escapedUnblocked = false;
3023
3471
  const authorisedActions = [];
3024
3472
  const runtime = agentConfig.runtime;
3025
3473
  const actionKey = (tool3, input) => {
@@ -3062,6 +3510,7 @@ function createStageRunner(deps) {
3062
3510
  if (verdict.verdict === "deny") {
3063
3511
  streamEvent(writer.append(event));
3064
3512
  recordDeny(verdict, event.toolUseId);
3513
+ if (verdict.policy === "fs_confine" && runtime !== "claude-code") escapedUnblocked = true;
3065
3514
  return;
3066
3515
  }
3067
3516
  }
@@ -3124,10 +3573,16 @@ function createStageRunner(deps) {
3124
3573
  if (killTimer) clearTimeout(killTimer);
3125
3574
  }
3126
3575
  let status = timedOut ? "timeout" : result.status;
3576
+ if (status === "ok" && escapedUnblocked) {
3577
+ status = "fail";
3578
+ const msg = `stage reached outside the run's worktree and the ${runtime} runtime could not block it before it ran`;
3579
+ writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "fs-confine-escape", message: msg });
3580
+ deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
3581
+ }
3127
3582
  if (status === "ok" && job.hooks && job.hooks.length > 0) {
3128
3583
  for (const cmd of job.hooks) {
3129
3584
  try {
3130
- execFileSync4("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
3585
+ execFileSync5("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
3131
3586
  } catch (e) {
3132
3587
  status = "fail";
3133
3588
  writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
@@ -3372,12 +3827,12 @@ async function startEdgeNode(opts) {
3372
3827
  const trace = {
3373
3828
  event: (frame) => send({ type: "trace-event", ...frame }),
3374
3829
  finalised: (frame) => send({ type: "trace-finalised", ...frame }),
3375
- requestBlobUrl: (req) => new Promise((resolve2) => {
3830
+ requestBlobUrl: (req) => new Promise((resolve3) => {
3376
3831
  const reqId = `${req.runId}:${req.stageId}:${req.attempt}:${blobReqCounter++}`;
3377
- pendingBlob.set(reqId, resolve2);
3832
+ pendingBlob.set(reqId, resolve3);
3378
3833
  send({ type: "blob-put-request", reqId, ...req });
3379
3834
  setTimeout(() => {
3380
- if (pendingBlob.delete(reqId)) resolve2({ key: "" });
3835
+ if (pendingBlob.delete(reqId)) resolve3({ key: "" });
3381
3836
  }, 3e4).unref?.();
3382
3837
  })
3383
3838
  };
@@ -3440,10 +3895,10 @@ async function startEdgeNode(opts) {
3440
3895
  return;
3441
3896
  }
3442
3897
  if (msg.type === "blob-put-url") {
3443
- const resolve2 = pendingBlob.get(msg.reqId);
3444
- if (resolve2) {
3898
+ const resolve3 = pendingBlob.get(msg.reqId);
3899
+ if (resolve3) {
3445
3900
  pendingBlob.delete(msg.reqId);
3446
- resolve2({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
3901
+ resolve3({ key: msg.key, ...msg.url ? { url: msg.url } : {} });
3447
3902
  }
3448
3903
  return;
3449
3904
  }
@@ -3612,11 +4067,11 @@ async function startEdgeNode(opts) {
3612
4067
  { once: true }
3613
4068
  );
3614
4069
  connect();
3615
- await new Promise((resolve2, reject) => {
4070
+ await new Promise((resolve3, reject) => {
3616
4071
  const t = setInterval(() => {
3617
4072
  if (ws && ws.readyState === WebSocket.OPEN) {
3618
4073
  clearInterval(t);
3619
- resolve2();
4074
+ resolve3();
3620
4075
  }
3621
4076
  }, 50);
3622
4077
  onFatal = (err) => {
@@ -3634,11 +4089,11 @@ var PROBES = [
3634
4089
  { runtime: "pi", cmd: "pi" }
3635
4090
  ];
3636
4091
  function probe(cmd, timeoutMs) {
3637
- return new Promise((resolve2) => {
4092
+ return new Promise((resolve3) => {
3638
4093
  execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
3639
- if (err) return resolve2(void 0);
4094
+ if (err) return resolve3(void 0);
3640
4095
  const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
3641
- resolve2(line ?? "");
4096
+ resolve3(line ?? "");
3642
4097
  });
3643
4098
  });
3644
4099
  }
@@ -3681,7 +4136,7 @@ function probeHub(opts) {
3681
4136
  clientVersion = "0.0.0",
3682
4137
  timeoutMs = 8e3
3683
4138
  } = opts;
3684
- return new Promise((resolve2) => {
4139
+ return new Promise((resolve3) => {
3685
4140
  let settled = false;
3686
4141
  let opened = false;
3687
4142
  let ws;
@@ -3693,7 +4148,7 @@ function probeHub(opts) {
3693
4148
  ws.terminate();
3694
4149
  } catch {
3695
4150
  }
3696
- resolve2(result);
4151
+ resolve3(result);
3697
4152
  };
3698
4153
  const timer = setTimeout(
3699
4154
  () => done({ ok: false, reason: "timeout", detail: `no welcome within ${timeoutMs}ms` }),
@@ -4153,8 +4608,8 @@ function usage(bin, command) {
4153
4608
  }
4154
4609
 
4155
4610
  // 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";
4611
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
4612
+ import { join as join11 } from "path";
4158
4613
  var BUNDLE_LOG_LINES = 2e3;
4159
4614
  function tailJsonl(raw, n) {
4160
4615
  const records = [];
@@ -4195,7 +4650,7 @@ async function buildBundle(deps) {
4195
4650
  if (deps.exists(deps.crashDir)) {
4196
4651
  for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
4197
4652
  try {
4198
- crashes.push(JSON.parse(deps.readFile(join9(deps.crashDir, name))));
4653
+ crashes.push(JSON.parse(deps.readFile(join11(deps.crashDir, name))));
4199
4654
  } catch (e) {
4200
4655
  warnings.push(`could not read crash record ${name} (${e.message})`);
4201
4656
  }
@@ -4254,17 +4709,17 @@ var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
4254
4709
  ...doctor ? { doctor } : {},
4255
4710
  readFile: (p) => readFileSync5(p, "utf8"),
4256
4711
  listDir: (p) => readdirSync4(p),
4257
- exists: (p) => existsSync6(p),
4712
+ exists: (p) => existsSync7(p),
4258
4713
  writeFile: (p, content) => {
4259
- const dir = join9(p, "..");
4260
- if (!existsSync6(dir)) mkdirSync7(dir, { recursive: true });
4714
+ const dir = join11(p, "..");
4715
+ if (!existsSync7(dir)) mkdirSync7(dir, { recursive: true });
4261
4716
  writeFileSync6(p, content, { mode: 384 });
4262
4717
  },
4263
4718
  out: (line) => void process.stdout.write(`${line}
4264
4719
  `)
4265
4720
  });
4266
4721
  function defaultBundlePath(cwd, now) {
4267
- return join9(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
4722
+ return join11(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
4268
4723
  }
4269
4724
 
4270
4725
  // src/doctor.ts
@@ -4399,8 +4854,8 @@ async function runDoctor(inputs, deps = {}) {
4399
4854
  }
4400
4855
 
4401
4856
  // 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";
4857
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
4858
+ import { dirname as dirname5 } from "path";
4404
4859
  function parseLock(content) {
4405
4860
  if (!content) return void 0;
4406
4861
  const pid = Number(content.trim());
@@ -4438,7 +4893,7 @@ var defaultLockDeps = (file) => ({
4438
4893
  }
4439
4894
  },
4440
4895
  writeFile: (path, content) => {
4441
- if (!existsSync7(dirname4(path))) mkdirSync8(dirname4(path), { recursive: true, mode: 448 });
4896
+ if (!existsSync8(dirname5(path))) mkdirSync8(dirname5(path), { recursive: true, mode: 448 });
4442
4897
  writeFileSync7(path, content);
4443
4898
  },
4444
4899
  removeFile: (path) => rmSync6(path, { force: true }),
@@ -4447,7 +4902,7 @@ var defaultLockDeps = (file) => ({
4447
4902
 
4448
4903
  // src/logs.ts
4449
4904
  import { spawn } from "child_process";
4450
- import { copyFileSync, existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
4905
+ import { copyFileSync, existsSync as existsSync9, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
4451
4906
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
4452
4907
  function logsCommand(inputs) {
4453
4908
  return [
@@ -4528,7 +4983,7 @@ async function runStructuredLogs(inputs, deps) {
4528
4983
  }
4529
4984
  function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
4530
4985
  try {
4531
- if (!existsSync8(file) || statSync4(file).size <= maxBytes) return;
4986
+ if (!existsSync9(file) || statSync4(file).size <= maxBytes) return;
4532
4987
  copyFileSync(file, `${file}.1`);
4533
4988
  truncateSync(file, 0);
4534
4989
  } catch {
@@ -4537,13 +4992,13 @@ function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
4537
4992
  var defaultLogsDeps = (files, jsonlFile) => ({
4538
4993
  files,
4539
4994
  jsonlFile,
4540
- fileExists: (path) => existsSync8(path),
4995
+ fileExists: (path) => existsSync9(path),
4541
4996
  readFile: (path) => readFileSync7(path, "utf8"),
4542
- run: (argv) => new Promise((resolve2) => {
4997
+ run: (argv) => new Promise((resolve3) => {
4543
4998
  const [cmd, ...args] = argv;
4544
4999
  const child = spawn(cmd, args, { stdio: "inherit" });
4545
- child.on("error", () => resolve2(1));
4546
- child.on("close", (code) => resolve2(code ?? 0));
5000
+ child.on("error", () => resolve3(1));
5001
+ child.on("close", (code) => resolve3(code ?? 0));
4547
5002
  }),
4548
5003
  out: (line) => void process.stdout.write(`${line}
4549
5004
  `)
@@ -4551,11 +5006,11 @@ var defaultLogsDeps = (files, jsonlFile) => ({
4551
5006
 
4552
5007
  // src/process-safety.ts
4553
5008
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
4554
- import { join as join10 } from "path";
5009
+ import { join as join12 } from "path";
4555
5010
  function writeCrashRecord(dir, record) {
4556
5011
  try {
4557
5012
  mkdirSync9(dir, { recursive: true, mode: 448 });
4558
- const file = join10(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
5013
+ const file = join12(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
4559
5014
  writeFileSync8(file, `${JSON.stringify(record, null, 2)}
4560
5015
  `, { mode: 384 });
4561
5016
  return file;
@@ -4609,11 +5064,11 @@ function installProcessSafetyNet(opts) {
4609
5064
  }
4610
5065
 
4611
5066
  // src/preflight.ts
4612
- import { execFileSync as execFileSync5 } from "child_process";
5067
+ import { execFileSync as execFileSync6 } from "child_process";
4613
5068
  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";
5069
+ import { accessSync, constants as fsConstants, existsSync as existsSync10, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
5070
+ import { homedir as homedir4 } from "os";
5071
+ import { join as join13 } from "path";
4617
5072
  var REPORT_BASE_URL = "https://app.dahrk.ai/r";
4618
5073
  var LOW_DISK_BYTES = 512 * 1024 * 1024;
4619
5074
  var PREFLIGHT_STAGES = [
@@ -4742,7 +5197,7 @@ var defaultDeps2 = () => ({
4742
5197
  });
4743
5198
  function commandPresent(cmd) {
4744
5199
  try {
4745
- execFileSync5("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
5200
+ execFileSync6("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
4746
5201
  return true;
4747
5202
  } catch {
4748
5203
  return false;
@@ -4750,12 +5205,12 @@ function commandPresent(cmd) {
4750
5205
  }
4751
5206
  function sshKeyPresent() {
4752
5207
  try {
4753
- const dir = join11(homedir2(), ".ssh");
4754
- if (existsSync9(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
5208
+ const dir = join13(homedir4(), ".ssh");
5209
+ if (existsSync10(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
4755
5210
  } catch {
4756
5211
  }
4757
5212
  try {
4758
- execFileSync5("ssh-add", ["-l"], { stdio: "ignore" });
5213
+ execFileSync6("ssh-add", ["-l"], { stdio: "ignore" });
4759
5214
  return true;
4760
5215
  } catch {
4761
5216
  return false;
@@ -4770,12 +5225,12 @@ function writable(dir) {
4770
5225
  }
4771
5226
  }
4772
5227
  function worktreeRoot(env) {
4773
- return env.DAHRK_WORKTREES_DIR ?? join11(env.DAHRK_STATE_DIR ?? join11(homedir2(), ".dahrk"), "worktrees");
5228
+ return env.DAHRK_WORKTREES_DIR ?? join13(env.DAHRK_STATE_DIR ?? join13(homedir4(), ".dahrk"), "worktrees");
4774
5229
  }
4775
5230
  function nearestExisting(dir) {
4776
5231
  let cur = dir;
4777
- while (!existsSync9(cur)) {
4778
- const parent = join11(cur, "..");
5232
+ while (!existsSync10(cur)) {
5233
+ const parent = join13(cur, "..");
4779
5234
  if (parent === cur) break;
4780
5235
  cur = parent;
4781
5236
  }
@@ -4790,7 +5245,7 @@ function freeDiskBytes(dir) {
4790
5245
  }
4791
5246
  }
4792
5247
  function probeRepo(repoPath) {
4793
- const git = (args) => execFileSync5("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
5248
+ const git = (args) => execFileSync6("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4794
5249
  try {
4795
5250
  if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") {
4796
5251
  return { path: repoPath, isGitRepo: false, headResolves: false, detail: "not inside a work tree" };
@@ -4825,49 +5280,49 @@ function gatherHostFacts(repoPath) {
4825
5280
  }
4826
5281
 
4827
5282
  // 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";
5283
+ import { execFileSync as execFileSync7 } from "child_process";
5284
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
5285
+ import { homedir as homedir6, platform as osPlatform3, userInfo } from "os";
5286
+ import { join as join15 } from "path";
4832
5287
 
4833
5288
  // 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";
5289
+ import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
5290
+ import { homedir as homedir5 } from "os";
5291
+ import { join as join14 } from "path";
4837
5292
  var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
4838
5293
  var isDesired = (v) => v === "running" || v === "stopped";
4839
5294
  var FILE_MODE = 384;
4840
5295
  var DIR_MODE = 448;
4841
5296
  function stateDir(env) {
4842
- return env.DAHRK_STATE_DIR ?? join12(homedir3(), ".dahrk");
5297
+ return env.DAHRK_STATE_DIR ?? join14(homedir5(), ".dahrk");
4843
5298
  }
4844
5299
  function legacyStateDir(env) {
4845
- return env.DAHRK_STATE_DIR ? void 0 : join12(homedir3(), ".skakel");
5300
+ return env.DAHRK_STATE_DIR ? void 0 : join14(homedir5(), ".skakel");
4846
5301
  }
4847
5302
  function stateFile(env) {
4848
- return join12(stateDir(env), "node.json");
5303
+ return join14(stateDir(env), "node.json");
4849
5304
  }
4850
5305
  function logDir(env) {
4851
- return join12(stateDir(env), "logs");
5306
+ return join14(stateDir(env), "logs");
4852
5307
  }
4853
5308
  function logFiles(env) {
4854
5309
  const dir = logDir(env);
4855
- return { out: join12(dir, "node.out.log"), err: join12(dir, "node.err.log") };
5310
+ return { out: join14(dir, "node.out.log"), err: join14(dir, "node.err.log") };
4856
5311
  }
4857
5312
  function jsonlLogFile(env) {
4858
- return join12(logDir(env), "node.jsonl");
5313
+ return join14(logDir(env), "node.jsonl");
4859
5314
  }
4860
5315
  function crashDir(env) {
4861
- return join12(logDir(env), "crashes");
5316
+ return join14(logDir(env), "crashes");
4862
5317
  }
4863
5318
  function lockFile(env) {
4864
- return join12(stateDir(env), "node.pid");
5319
+ return join14(stateDir(env), "node.pid");
4865
5320
  }
4866
5321
  function setDesired(env, desired) {
4867
5322
  writeState(env, { desired });
4868
5323
  }
4869
5324
  function readState(file) {
4870
- if (!existsSync10(file)) return {};
5325
+ if (!existsSync11(file)) return {};
4871
5326
  try {
4872
5327
  const parsed = JSON.parse(readFileSync8(file, "utf8"));
4873
5328
  const state = {};
@@ -4968,9 +5423,9 @@ ${envEntries}
4968
5423
  <key>ThrottleInterval</key>
4969
5424
  <integer>10</integer>
4970
5425
  <key>StandardOutPath</key>
4971
- <string>${xmlEscape(join13(inputs.logDir, "node.out.log"))}</string>
5426
+ <string>${xmlEscape(join15(inputs.logDir, "node.out.log"))}</string>
4972
5427
  <key>StandardErrorPath</key>
4973
- <string>${xmlEscape(join13(inputs.logDir, "node.err.log"))}</string>
5428
+ <string>${xmlEscape(join15(inputs.logDir, "node.err.log"))}</string>
4974
5429
  </dict>
4975
5430
  </plist>
4976
5431
  `;
@@ -4993,8 +5448,8 @@ Type=simple
4993
5448
  ExecStart=${exec}
4994
5449
  ${envLines}
4995
5450
  WorkingDirectory=${inputs.homeDir}
4996
- StandardOutput=append:${join13(inputs.logDir, "node.out.log")}
4997
- StandardError=append:${join13(inputs.logDir, "node.err.log")}
5451
+ StandardOutput=append:${join15(inputs.logDir, "node.out.log")}
5452
+ StandardError=append:${join15(inputs.logDir, "node.err.log")}
4998
5453
  Restart=on-failure
4999
5454
  RestartSec=3
5000
5455
  RestartPreventExitStatus=78
@@ -5005,7 +5460,7 @@ WantedBy=default.target
5005
5460
  }
5006
5461
  function buildPlan(inputs) {
5007
5462
  if (inputs.manager === "launchd") {
5008
- const filePath2 = join13(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5463
+ const filePath2 = join15(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
5009
5464
  return {
5010
5465
  manager: "launchd",
5011
5466
  label: LAUNCHD_LABEL,
@@ -5022,7 +5477,7 @@ function buildPlan(inputs) {
5022
5477
  logHint: "dahrk logs -f"
5023
5478
  };
5024
5479
  }
5025
- const filePath = join13(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5480
+ const filePath = join15(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5026
5481
  const user = userInfo().username;
5027
5482
  return {
5028
5483
  manager: "systemd",
@@ -5052,7 +5507,7 @@ function unitIsCurrent(plan, onDisk) {
5052
5507
  return onDisk === plan.content;
5053
5508
  }
5054
5509
  function unitPath(manager, homeDir) {
5055
- return manager === "launchd" ? join13(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join13(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5510
+ return manager === "launchd" ? join15(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join15(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
5056
5511
  }
5057
5512
  function statusCommand(manager) {
5058
5513
  return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
@@ -5261,7 +5716,7 @@ function dirOf(path) {
5261
5716
  function resolveScriptPath() {
5262
5717
  const argv1 = process.argv[1] ?? "";
5263
5718
  try {
5264
- return realpathSync2(argv1);
5719
+ return realpathSync3(argv1);
5265
5720
  } catch {
5266
5721
  return argv1;
5267
5722
  }
@@ -5275,14 +5730,14 @@ function stableNodeBin(execPath, realpath) {
5275
5730
  }
5276
5731
  var realpathOrUndefined = (p) => {
5277
5732
  try {
5278
- return realpathSync2(p);
5733
+ return realpathSync3(p);
5279
5734
  } catch {
5280
5735
  return void 0;
5281
5736
  }
5282
5737
  };
5283
5738
  var defaultDeps3 = () => ({
5284
5739
  platform: osPlatform3(),
5285
- homeDir: homedir4(),
5740
+ homeDir: homedir6(),
5286
5741
  // Not `process.execPath` raw: that is the versioned Homebrew Cellar path, which the next
5287
5742
  // `brew upgrade node` deletes out from under the unit. See `stableNodeBin`.
5288
5743
  nodeBin: stableNodeBin(process.execPath, realpathOrUndefined),
@@ -5309,11 +5764,11 @@ var defaultDeps3 = () => ({
5309
5764
  }
5310
5765
  },
5311
5766
  removeFile: (path) => rmSync7(path, { force: true }),
5312
- fileExists: (path) => existsSync11(path),
5767
+ fileExists: (path) => existsSync12(path),
5313
5768
  run: (argv) => {
5314
5769
  const [cmd, ...args] = argv;
5315
5770
  try {
5316
- execFileSync6(cmd, args, { stdio: "inherit" });
5771
+ execFileSync7(cmd, args, { stdio: "inherit" });
5317
5772
  return 0;
5318
5773
  } catch (e) {
5319
5774
  const status = e.status;
@@ -5323,7 +5778,7 @@ var defaultDeps3 = () => ({
5323
5778
  capture: (argv) => {
5324
5779
  const [cmd, ...args] = argv;
5325
5780
  try {
5326
- const stdout = execFileSync6(cmd, args, {
5781
+ const stdout = execFileSync7(cmd, args, {
5327
5782
  encoding: "utf8",
5328
5783
  stdio: ["ignore", "pipe", "ignore"]
5329
5784
  });
@@ -5338,8 +5793,8 @@ var defaultDeps3 = () => ({
5338
5793
  });
5339
5794
 
5340
5795
  // src/update.ts
5341
- import { execFileSync as execFileSync7 } from "child_process";
5342
- import { realpathSync as realpathSync3 } from "fs";
5796
+ import { execFileSync as execFileSync8 } from "child_process";
5797
+ import { realpathSync as realpathSync4 } from "fs";
5343
5798
  var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
5344
5799
  var CHANNEL_COMMANDS = {
5345
5800
  npm: "npm install -g dahrk-node@latest",
@@ -5360,7 +5815,7 @@ function detectChannel(binPath) {
5360
5815
  if (!binPath) return "unknown";
5361
5816
  let resolved = binPath;
5362
5817
  try {
5363
- resolved = realpathSync3(binPath);
5818
+ resolved = realpathSync4(binPath);
5364
5819
  } catch {
5365
5820
  }
5366
5821
  if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
@@ -5448,7 +5903,7 @@ async function fetchLatestVersion(signal) {
5448
5903
  function spawnUpgrade(argv) {
5449
5904
  const [cmd, ...args] = argv;
5450
5905
  try {
5451
- execFileSync7(cmd, args, { stdio: "inherit" });
5906
+ execFileSync8(cmd, args, { stdio: "inherit" });
5452
5907
  return 0;
5453
5908
  } catch (e) {
5454
5909
  const status = e.status;
@@ -5590,7 +6045,7 @@ async function runStatus(inputs, deps) {
5590
6045
  }
5591
6046
 
5592
6047
  // src/main.ts
5593
- var CLIENT_VERSION = "0.1.10";
6048
+ var CLIENT_VERSION = "0.1.11";
5594
6049
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
5595
6050
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
5596
6051
  var RUNTIMES = ["claude-code", "codex", "pi"];
@@ -5602,7 +6057,7 @@ function resolveNodeId(env, opts = {}) {
5602
6057
  if (existing) return existing;
5603
6058
  const legacy = legacyStateDir(env);
5604
6059
  if (legacy) {
5605
- const legacyId = readState(join14(legacy, "node.json")).nodeId;
6060
+ const legacyId = readState(join16(legacy, "node.json")).nodeId;
5606
6061
  if (legacyId) return legacyId;
5607
6062
  }
5608
6063
  const nodeId = randomUUID3();
@@ -5783,15 +6238,15 @@ function scheduleUpdateChecks(env) {
5783
6238
  function statusDeps(env) {
5784
6239
  return {
5785
6240
  platform: osPlatform4(),
5786
- homeDir: homedir5(),
6241
+ homeDir: homedir7(),
5787
6242
  env,
5788
6243
  binPath: process.argv[1],
5789
6244
  detectRuntimes: () => resolveRuntimes(env),
5790
- fileExists: (path) => existsSync12(path),
6245
+ fileExists: (path) => existsSync13(path),
5791
6246
  capture: (argv) => {
5792
6247
  const [cmd, ...args] = argv;
5793
6248
  try {
5794
- const stdout = execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
6249
+ const stdout = execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
5795
6250
  return { code: 0, stdout };
5796
6251
  } catch (e) {
5797
6252
  const status = e.status;
@@ -5932,7 +6387,7 @@ var invokedAsEntrypoint = (() => {
5932
6387
  const argv1 = process.argv[1];
5933
6388
  if (!argv1) return false;
5934
6389
  try {
5935
- return pathToFileURL(realpathSync4(argv1)).href === import.meta.url;
6390
+ return pathToFileURL(realpathSync5(argv1)).href === import.meta.url;
5936
6391
  } catch {
5937
6392
  return false;
5938
6393
  }