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.
- package/dist/main.js +613 -158
- 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
|
|
5
|
-
import { existsSync as
|
|
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
|
|
8
|
-
import { basename as basename2, join as
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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 :
|
|
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
|
|
1397
|
-
writeFileSync(excludePath, `${existing}${
|
|
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(
|
|
1423
|
-
const script =
|
|
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) =>
|
|
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:
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
1697
|
-
const bodyFile =
|
|
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
|
|
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 = [
|
|
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) =>
|
|
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(
|
|
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
|
|
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 =
|
|
1876
|
-
mkdirSync2(
|
|
1877
|
-
mkdirSync2(
|
|
1878
|
-
const tracePath =
|
|
1879
|
-
const metaPath =
|
|
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(
|
|
1892
|
-
return
|
|
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 =
|
|
1923
|
-
writeFileSync2(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
1976
|
-
const dest =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
2413
|
-
import { isAbsolute as
|
|
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
|
|
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((
|
|
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((
|
|
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 =
|
|
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(
|
|
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 =
|
|
3107
|
+
const dir = join10(ref.scratchPath, "docs");
|
|
2665
3108
|
mkdirSync6(dir, { recursive: true });
|
|
2666
3109
|
for (const doc of docs) {
|
|
2667
|
-
writeFileSync5(
|
|
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(
|
|
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 (
|
|
2697
|
-
const root =
|
|
2698
|
-
const target =
|
|
2699
|
-
const fromRoot =
|
|
2700
|
-
if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 ??
|
|
2870
|
-
const worktreePath =
|
|
2871
|
-
const scratchPath =
|
|
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(
|
|
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(
|
|
2916
|
-
const bytes = readFileSync4(
|
|
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(
|
|
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
|
-
|
|
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((
|
|
3830
|
+
requestBlobUrl: (req) => new Promise((resolve3) => {
|
|
3376
3831
|
const reqId = `${req.runId}:${req.stageId}:${req.attempt}:${blobReqCounter++}`;
|
|
3377
|
-
pendingBlob.set(reqId,
|
|
3832
|
+
pendingBlob.set(reqId, resolve3);
|
|
3378
3833
|
send({ type: "blob-put-request", reqId, ...req });
|
|
3379
3834
|
setTimeout(() => {
|
|
3380
|
-
if (pendingBlob.delete(reqId))
|
|
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
|
|
3444
|
-
if (
|
|
3898
|
+
const resolve3 = pendingBlob.get(msg.reqId);
|
|
3899
|
+
if (resolve3) {
|
|
3445
3900
|
pendingBlob.delete(msg.reqId);
|
|
3446
|
-
|
|
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((
|
|
4070
|
+
await new Promise((resolve3, reject) => {
|
|
3616
4071
|
const t = setInterval(() => {
|
|
3617
4072
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
3618
4073
|
clearInterval(t);
|
|
3619
|
-
|
|
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((
|
|
4092
|
+
return new Promise((resolve3) => {
|
|
3638
4093
|
execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
|
|
3639
|
-
if (err) return
|
|
4094
|
+
if (err) return resolve3(void 0);
|
|
3640
4095
|
const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
|
|
3641
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
4157
|
-
import { join as
|
|
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(
|
|
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) =>
|
|
4712
|
+
exists: (p) => existsSync7(p),
|
|
4258
4713
|
writeFile: (p, content) => {
|
|
4259
|
-
const dir =
|
|
4260
|
-
if (!
|
|
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
|
|
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
|
|
4403
|
-
import { dirname as
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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) =>
|
|
4995
|
+
fileExists: (path) => existsSync9(path),
|
|
4541
4996
|
readFile: (path) => readFileSync7(path, "utf8"),
|
|
4542
|
-
run: (argv) => new Promise((
|
|
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", () =>
|
|
4546
|
-
child.on("close", (code) =>
|
|
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
|
|
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 =
|
|
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
|
|
5067
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
4613
5068
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4614
|
-
import { accessSync, constants as fsConstants, existsSync as
|
|
4615
|
-
import { homedir as
|
|
4616
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
4754
|
-
if (
|
|
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
|
-
|
|
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 ??
|
|
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 (!
|
|
4778
|
-
const parent =
|
|
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) =>
|
|
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
|
|
4829
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
4830
|
-
import { homedir as
|
|
4831
|
-
import { join as
|
|
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
|
|
4835
|
-
import { homedir as
|
|
4836
|
-
import { join as
|
|
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 ??
|
|
5297
|
+
return env.DAHRK_STATE_DIR ?? join14(homedir5(), ".dahrk");
|
|
4843
5298
|
}
|
|
4844
5299
|
function legacyStateDir(env) {
|
|
4845
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
5300
|
+
return env.DAHRK_STATE_DIR ? void 0 : join14(homedir5(), ".skakel");
|
|
4846
5301
|
}
|
|
4847
5302
|
function stateFile(env) {
|
|
4848
|
-
return
|
|
5303
|
+
return join14(stateDir(env), "node.json");
|
|
4849
5304
|
}
|
|
4850
5305
|
function logDir(env) {
|
|
4851
|
-
return
|
|
5306
|
+
return join14(stateDir(env), "logs");
|
|
4852
5307
|
}
|
|
4853
5308
|
function logFiles(env) {
|
|
4854
5309
|
const dir = logDir(env);
|
|
4855
|
-
return { out:
|
|
5310
|
+
return { out: join14(dir, "node.out.log"), err: join14(dir, "node.err.log") };
|
|
4856
5311
|
}
|
|
4857
5312
|
function jsonlLogFile(env) {
|
|
4858
|
-
return
|
|
5313
|
+
return join14(logDir(env), "node.jsonl");
|
|
4859
5314
|
}
|
|
4860
5315
|
function crashDir(env) {
|
|
4861
|
-
return
|
|
5316
|
+
return join14(logDir(env), "crashes");
|
|
4862
5317
|
}
|
|
4863
5318
|
function lockFile(env) {
|
|
4864
|
-
return
|
|
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 (!
|
|
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(
|
|
5426
|
+
<string>${xmlEscape(join15(inputs.logDir, "node.out.log"))}</string>
|
|
4972
5427
|
<key>StandardErrorPath</key>
|
|
4973
|
-
<string>${xmlEscape(
|
|
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:${
|
|
4997
|
-
StandardError=append:${
|
|
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 =
|
|
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 =
|
|
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" ?
|
|
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
|
|
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
|
|
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:
|
|
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) =>
|
|
5767
|
+
fileExists: (path) => existsSync12(path),
|
|
5313
5768
|
run: (argv) => {
|
|
5314
5769
|
const [cmd, ...args] = argv;
|
|
5315
5770
|
try {
|
|
5316
|
-
|
|
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 =
|
|
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
|
|
5342
|
-
import { realpathSync as
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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:
|
|
6241
|
+
homeDir: homedir7(),
|
|
5787
6242
|
env,
|
|
5788
6243
|
binPath: process.argv[1],
|
|
5789
6244
|
detectRuntimes: () => resolveRuntimes(env),
|
|
5790
|
-
fileExists: (path) =>
|
|
6245
|
+
fileExists: (path) => existsSync13(path),
|
|
5791
6246
|
capture: (argv) => {
|
|
5792
6247
|
const [cmd, ...args] = argv;
|
|
5793
6248
|
try {
|
|
5794
|
-
const stdout =
|
|
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(
|
|
6390
|
+
return pathToFileURL(realpathSync5(argv1)).href === import.meta.url;
|
|
5936
6391
|
} catch {
|
|
5937
6392
|
return false;
|
|
5938
6393
|
}
|