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