@tuent/sentinel 0.1.1 → 0.1.3
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/README.md +5 -1
- package/SECURITY_MODEL.md +94 -35
- package/dist/Sentinel-5CQ6HKXS.js +10 -0
- package/dist/{Sentinel-B_sv8Kiy.d.ts → Sentinel-BVoMEF3F.d.ts} +46 -1
- package/dist/{chunk-WPTJBRX5.js → chunk-2TJ5Z53T.js} +266 -73
- package/dist/{chunk-IYC5E7RL.js → chunk-G74MMDKA.js} +189 -43
- package/dist/{chunk-QHE56MEO.js → chunk-JTR2E7RD.js} +247 -60
- package/dist/{chunk-NS6ZLMDK.js → chunk-SSDIBY52.js} +84 -25
- package/dist/chunk-TKAKHSZ3.js +1 -0
- package/dist/{chunk-2FFMYSVC.js → chunk-WLIDSTS4.js} +18 -2
- package/dist/cli.js +22 -22
- package/dist/gateway/index.d.ts +33 -1
- package/dist/gateway/index.js +3 -3
- package/dist/gatewayDaemon.js +37 -15
- package/dist/index.d.ts +40 -9
- package/dist/index.js +8 -7
- package/dist/{policyLoader-6KR5VFVV.js → policyLoader-KZL2U4M2.js} +2 -2
- package/package.json +1 -1
- package/dist/Sentinel-QHMQ67W3.js +0 -10
|
@@ -6,8 +6,12 @@ import {
|
|
|
6
6
|
discoverPolicy
|
|
7
7
|
} from "./chunk-FMZWHT4M.js";
|
|
8
8
|
import {
|
|
9
|
+
FORBIDDEN_BASENAMES
|
|
10
|
+
} from "./chunk-JTR2E7RD.js";
|
|
11
|
+
import {
|
|
12
|
+
loadPolicy,
|
|
9
13
|
loadPolicyFromString
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-WLIDSTS4.js";
|
|
11
15
|
|
|
12
16
|
// src/setup/initClaudeCode.ts
|
|
13
17
|
import { access, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -17,13 +21,18 @@ import { createServer } from "http";
|
|
|
17
21
|
import { fileURLToPath } from "url";
|
|
18
22
|
|
|
19
23
|
// src/gateway/hookScriptSource.ts
|
|
24
|
+
var SCORER_CRITICAL_EXTRAS = ["shadow", "passwd"];
|
|
25
|
+
var HOOK_SENSITIVE_BASENAMES = [
|
|
26
|
+
.../* @__PURE__ */ new Set([...FORBIDDEN_BASENAMES, ...SCORER_CRITICAL_EXTRAS])
|
|
27
|
+
];
|
|
20
28
|
var HOOK_SCRIPT_SOURCE = `#!/usr/bin/env node
|
|
21
29
|
// Sentinel cc hook bridge \u2014 generated by sentinel init claude-code
|
|
22
30
|
// Do not edit manually; regenerate with: sentinel init claude-code --force
|
|
23
31
|
|
|
24
32
|
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
25
|
-
import { join } from "node:path";
|
|
33
|
+
import { join, resolve, sep } from "node:path";
|
|
26
34
|
import { spawn } from "node:child_process";
|
|
35
|
+
import { createRequire } from "node:module";
|
|
27
36
|
|
|
28
37
|
const GATEWAY_ENTRY_POINT = "__GATEWAY_ENTRY_POINT__";
|
|
29
38
|
const PORT = 7847;
|
|
@@ -74,6 +83,17 @@ function logFallback(entry) {
|
|
|
74
83
|
try { appendFileSync(FALLBACK_LOG, line + "\\n"); } catch { /* best effort */ }
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
// Sprint 26 Fix-2 Part 1 \u2014 hardcoded fail-closed FLOOR. tiers.json may ADD
|
|
87
|
+
// high-tier tools but can never DOWNGRADE a floor tool to low (defeats the
|
|
88
|
+
// two-step tier-config rewrite). Checked BEFORE the editable tiers below.
|
|
89
|
+
const FLOOR_HIGH = ["Bash", "Write", "Edit", "WebFetch", "NotebookEdit", "Task", "Skill"];
|
|
90
|
+
|
|
91
|
+
// Item D \u2014 operator allowUnknownTools escape hatch, baked at init from the
|
|
92
|
+
// launch policy so it survives gateway-down (the daemon allows these names
|
|
93
|
+
// unconditionally at the name level, so allowing them here keeps down \u2287 up).
|
|
94
|
+
// The marker default is [] \u2014 an un-substituted script stays strictest.
|
|
95
|
+
const ALLOW_UNKNOWN_TOOLS = /* __ALLOW_UNKNOWN_TOOLS__ */ [];
|
|
96
|
+
|
|
77
97
|
// Tier config uses flat format: { high, low, mcpDefault, unknownDefault } (plan v3.1 spec'd nested but simplified during 5a)
|
|
78
98
|
function loadTiers() {
|
|
79
99
|
try {
|
|
@@ -90,10 +110,52 @@ function loadTiers() {
|
|
|
90
110
|
}
|
|
91
111
|
|
|
92
112
|
function isHighSensitivity(toolName, tiers) {
|
|
113
|
+
if (FLOOR_HIGH.includes(toolName)) return true; // floor: tiers may add high, never downgrade
|
|
93
114
|
if (tiers.high && tiers.high.includes(toolName)) return true;
|
|
94
115
|
if (tiers.low && tiers.low.includes(toolName)) return false;
|
|
95
116
|
if (toolName.startsWith("mcp__")) return tiers.mcpDefault === "high";
|
|
96
|
-
|
|
117
|
+
// Item D \u2014 operator-allowlisted names pass: gateway-up they are translated
|
|
118
|
+
// as known and allowed at the name level, so the escape hatch must survive
|
|
119
|
+
// gateway-down too.
|
|
120
|
+
if (ALLOW_UNKNOWN_TOOLS.includes(toolName)) return false;
|
|
121
|
+
// Item D floor: all other unknown names are always high-tier gateway-down.
|
|
122
|
+
// Deliberately stricter than gateway-up's warn default: the hook cannot
|
|
123
|
+
// persist findings while the daemon is down, so a warn-equivalent here
|
|
124
|
+
// would be allow-UNLOGGED \u2014 the exact F-8 hole. Hard-deny keeps down \u2287 up
|
|
125
|
+
// in both knob modes (same fail-closed stance as Fix-2's read handling),
|
|
126
|
+
// and a tiers.json edit (unknownDefault: "low") cannot reopen it. Named
|
|
127
|
+
// tools can still be tiered low explicitly via tiers.low above.
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sprint 26 Fix-2 Part 2 \u2014 gateway-down safe-read exception.
|
|
132
|
+
// INVARIANT: on gateway-down a read is NEVER more permissive than gateway-up.
|
|
133
|
+
// Low-tier file reads (Read/Glob/Grep) fail CLOSED by default; only a
|
|
134
|
+
// structurally-provably-safe target is allowed. The guard is GENERATED from the
|
|
135
|
+
// canonical FORBIDDEN_BASENAMES (\u222A scorer extras), so it can't drift open.
|
|
136
|
+
const SENSITIVE_BASENAMES = ${JSON.stringify(HOOK_SENSITIVE_BASENAMES)};
|
|
137
|
+
const READ_TOOL_PATH_KEYS = { Read: "file_path", Glob: "pattern", Grep: "path" };
|
|
138
|
+
|
|
139
|
+
function isProvablySafeRead(toolName, toolInput, payloadCwd) {
|
|
140
|
+
const key = READ_TOOL_PATH_KEYS[toolName];
|
|
141
|
+
if (!key) return false; // not a path-bearing read tool \u2014 not provably safe
|
|
142
|
+
const raw = toolInput && toolInput[key];
|
|
143
|
+
if (typeof raw !== "string" || raw.length === 0) return false; // no target \u2192 not provable
|
|
144
|
+
if (raw[0] === "/" || raw[0] === "~") return false; // absolute / home \u2192 fail closed
|
|
145
|
+
const cwd = typeof payloadCwd === "string" && payloadCwd ? payloadCwd : process.cwd();
|
|
146
|
+
const cwdNorm = resolve(cwd);
|
|
147
|
+
const norm = resolve(cwd, raw);
|
|
148
|
+
// Must resolve strictly within cwd (rejects any .. escape).
|
|
149
|
+
if (norm !== cwdNorm && !norm.startsWith(cwdNorm + sep)) return false;
|
|
150
|
+
const segments = norm.slice(cwdNorm.length).split(sep).filter(Boolean);
|
|
151
|
+
for (const seg of segments) {
|
|
152
|
+
const low = seg.toLowerCase();
|
|
153
|
+
if (low[0] === ".") return false; // dotfile / dot-dir
|
|
154
|
+
for (const s of SENSITIVE_BASENAMES) {
|
|
155
|
+
if (low.includes(s.toLowerCase())) return false; // conservative sensitive-token match
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
97
159
|
}
|
|
98
160
|
|
|
99
161
|
// ---------------------------------------------------------------------------
|
|
@@ -111,15 +173,29 @@ if (mode === "pre") {
|
|
|
111
173
|
const result = JSON.parse(resp.body);
|
|
112
174
|
process.stdout.write(JSON.stringify(result), () => process.exit(0));
|
|
113
175
|
} catch {
|
|
114
|
-
// Gateway unreachable \u2014 tiered fail-closed
|
|
176
|
+
// Gateway unreachable \u2014 tiered fail-closed (Sprint 26 Fix-2).
|
|
115
177
|
const tiers = loadTiers();
|
|
178
|
+
const restore = "Restart your Claude Code session to relaunch the gateway; see ~/.dahlia/gateway-fallback.log.";
|
|
116
179
|
if (isHighSensitivity(toolName, tiers)) {
|
|
117
180
|
logFallback({ event: "fail-closed-block", tool: toolName, tier: "high" });
|
|
118
181
|
process.stderr.write(
|
|
119
|
-
\`Sentinel gateway unreachable; high-sensitivity tool "\${toolName}" blocked per fail-closed policy\`,
|
|
182
|
+
\`Sentinel gateway unreachable; high-sensitivity tool "\${toolName}" blocked per fail-closed policy. \${restore}\`,
|
|
183
|
+
() => process.exit(2),
|
|
184
|
+
);
|
|
185
|
+
} else if (
|
|
186
|
+
READ_TOOL_PATH_KEYS[toolName] &&
|
|
187
|
+
!isProvablySafeRead(toolName, payload.tool_input || {}, payload.cwd)
|
|
188
|
+
) {
|
|
189
|
+
// Low-tier read whose target is not provably safe \u2192 fail CLOSED. The hook
|
|
190
|
+
// carries no policy, so it cannot evaluate the forbid surface; a read it
|
|
191
|
+
// can't prove safe is treated as if the gateway would deny it.
|
|
192
|
+
logFallback({ event: "fail-closed-block", tool: toolName, tier: "low", reason: "read-not-provably-safe" });
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
\`Sentinel gateway unreachable; read by "\${toolName}" blocked (fail-closed: target not provably safe). \${restore}\`,
|
|
120
195
|
() => process.exit(2),
|
|
121
196
|
);
|
|
122
197
|
} else {
|
|
198
|
+
// Provably-safe read, or a non-read low-tier tool (e.g. WebSearch) \u2192 allow.
|
|
123
199
|
logFallback({ event: "fail-closed-allow", tool: toolName, tier: "low" });
|
|
124
200
|
process.stdout.write(JSON.stringify({
|
|
125
201
|
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" },
|
|
@@ -139,65 +215,37 @@ if (mode === "pre") {
|
|
|
139
215
|
process.exit(0);
|
|
140
216
|
|
|
141
217
|
} else if (mode === "session-start") {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const controller = new AbortController();
|
|
154
|
-
const timer = setTimeout(() => controller.abort(), 500);
|
|
155
|
-
try {
|
|
156
|
-
const res = await fetch(BASE_URL + "/api/sentinel/health", { signal: controller.signal });
|
|
157
|
-
if (res.ok) return; // evaluation-ready
|
|
158
|
-
} finally { clearTimeout(timer); }
|
|
159
|
-
} catch { /* not listening yet \u2014 retry until the bound */ }
|
|
160
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
161
|
-
}
|
|
162
|
-
// timed out \u2014 fall through; the first pre-tool-use uses the existing fail-closed.
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Check if gateway is already running (simple liveness; full PID validity is 5c)
|
|
166
|
-
if (existsSync(PID_PATH)) {
|
|
218
|
+
// Delegate the FULL lifecycle decision \u2014 liveness, build-version check, relaunch
|
|
219
|
+
// on stale build, and spawn \u2014 to runSessionStart via the dual-mode daemon entry.
|
|
220
|
+
// One source of truth: this generated file carries NO copy of that logic
|
|
221
|
+
// (anti-drift). We resolve the daemon entry from the PROJECT (cwd) at
|
|
222
|
+
// hook-runtime so a relocated install (pnpm / version bump) launches the CURRENT
|
|
223
|
+
// build rather than the init-time baked path; we fall back to the baked path
|
|
224
|
+
// (dev tree, or package not resolvable from cwd). The launcher itself performs
|
|
225
|
+
// the bounded readiness wait, so awaiting its exit preserves the cold-start
|
|
226
|
+
// guarantee (first PreToolUse hits a ready gateway, or the existing fail-closed).
|
|
227
|
+
function resolveDaemonEntry() {
|
|
167
228
|
try {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
} catch {
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Discover .sentinel.yaml by walking up from cwd
|
|
178
|
-
let dir = process.cwd();
|
|
179
|
-
let policyPath = null;
|
|
180
|
-
while (true) {
|
|
181
|
-
const candidate = join(dir, ".sentinel.yaml");
|
|
182
|
-
if (existsSync(candidate)) { policyPath = candidate; break; }
|
|
183
|
-
const parent = join(dir, "..");
|
|
184
|
-
if (parent === dir) break; // filesystem root
|
|
185
|
-
dir = parent;
|
|
229
|
+
const require = createRequire(join(process.cwd(), "package.json"));
|
|
230
|
+
const pkg = require.resolve("@tuent/sentinel/package.json");
|
|
231
|
+
const candidate = join(pkg, "..", "dist", "gatewayDaemon.js");
|
|
232
|
+
if (existsSync(candidate)) return candidate;
|
|
233
|
+
} catch { /* not resolvable from cwd \u2014 fall back */ }
|
|
234
|
+
return GATEWAY_ENTRY_POINT;
|
|
186
235
|
}
|
|
187
236
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await waitForGatewayReady(); // bounded wait for the just-spawned daemon to bind
|
|
237
|
+
const entry = resolveDaemonEntry();
|
|
238
|
+
const launchArgs = entry.endsWith(".ts")
|
|
239
|
+
? ["--import", "tsx/esm", entry, "--session-start", "--port", String(PORT), "--cwd", process.cwd()]
|
|
240
|
+
: [entry, "--session-start", "--port", String(PORT), "--cwd", process.cwd()];
|
|
241
|
+
await new Promise((res) => {
|
|
242
|
+
const child = spawn("node", launchArgs, { stdio: "ignore" });
|
|
243
|
+
// Safety bound: a wedged launcher must never hang the cc session. On timeout
|
|
244
|
+
// we return; the daemon may still be coming up and the first call fail-closes.
|
|
245
|
+
const safety = setTimeout(() => { try { child.kill(); } catch {} res(undefined); }, 15000);
|
|
246
|
+
child.on("exit", () => { clearTimeout(safety); res(undefined); });
|
|
247
|
+
child.on("error", () => { clearTimeout(safety); res(undefined); });
|
|
248
|
+
});
|
|
201
249
|
process.exit(0);
|
|
202
250
|
|
|
203
251
|
} else if (mode === "session-end") {
|
|
@@ -322,6 +370,11 @@ agent:
|
|
|
322
370
|
policy:
|
|
323
371
|
allow:
|
|
324
372
|
actions: [file_read, file_write, tool_invocation, network_request, command_exec]
|
|
373
|
+
# allow.targets is ADVISORY for file_read / file_write / tool_invocation: an
|
|
374
|
+
# access outside this list is logged as a MEDIUM scope_violation but still
|
|
375
|
+
# runs \u2014 Sentinel governs the agent's own tool calls, it is not a filesystem
|
|
376
|
+
# sandbox. forbid.targets below is the hard deny. network_request is the
|
|
377
|
+
# exception: unlisted hosts are DENIED by default (see the networkHosts note).
|
|
325
378
|
targets:
|
|
326
379
|
- "src/**"
|
|
327
380
|
- "test/**"
|
|
@@ -378,6 +431,28 @@ policy:
|
|
|
378
431
|
- "**/*.pem"
|
|
379
432
|
- "**/*.key"
|
|
380
433
|
- "/etc/**"
|
|
434
|
+
# Sprint 26 FIX 1 (A) \u2014 common credential stores. DRIFT: keep in sync with
|
|
435
|
+
# DEFAULT_FORBIDDEN_PATTERNS in defaults.ts (unification tracked separately).
|
|
436
|
+
- "**/.netrc"
|
|
437
|
+
- "**/.npmrc"
|
|
438
|
+
- "**/.git-credentials"
|
|
439
|
+
- "**/.pgpass"
|
|
440
|
+
- "**/.zsh_history"
|
|
441
|
+
- "**/.config/gh/**"
|
|
442
|
+
- "**/.docker/config.json"
|
|
443
|
+
- "**/.gnupg/**"
|
|
444
|
+
- "**/.config/gcloud/**"
|
|
445
|
+
- "**/.kube/**"
|
|
446
|
+
- "**/Library/Keychains/**"
|
|
447
|
+
# Sprint 26 FIX 1 (B) \u2014 Sentinel's own state dir (current path only).
|
|
448
|
+
- "**/.dahlia/**"
|
|
449
|
+
# Sprint 26 FIX 3 \u2014 this policy file and cc's hook-wiring settings files.
|
|
450
|
+
# Agent tool-writes are denied; reads stay allowed via a code-side ceiling
|
|
451
|
+
# exception (not authorable here \u2014 workspace-authored exceptions are
|
|
452
|
+
# dropped by the ceiling merge, by design). DRIFT: keep in sync with
|
|
453
|
+
# DEFAULT_FORBIDDEN_PATTERNS in defaults.ts.
|
|
454
|
+
- "**/.sentinel.yaml"
|
|
455
|
+
- "**/.claude/settings*.json"
|
|
381
456
|
enforcement:
|
|
382
457
|
restrictAfter: 3
|
|
383
458
|
quarantineAfter: 5
|
|
@@ -418,7 +493,16 @@ async function runInitClaudeCode(options) {
|
|
|
418
493
|
);
|
|
419
494
|
const hookPath = join(dahliaDir, "cc-hook.mjs");
|
|
420
495
|
const gatewayEntryPoint = resolveGatewayEntryPoint();
|
|
421
|
-
|
|
496
|
+
let allowUnknownTools = [];
|
|
497
|
+
try {
|
|
498
|
+
const effectivePolicy = await loadPolicy(policyPath);
|
|
499
|
+
allowUnknownTools = effectivePolicy.enforcement?.allowUnknownTools ?? [];
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
const hookContent = HOOK_SCRIPT_SOURCE.replace(
|
|
503
|
+
/__GATEWAY_ENTRY_POINT__/g,
|
|
504
|
+
gatewayEntryPoint
|
|
505
|
+
).replace("/* __ALLOW_UNKNOWN_TOOLS__ */ []", JSON.stringify(allowUnknownTools));
|
|
422
506
|
if (hookContent.includes("__GATEWAY_ENTRY_POINT__")) {
|
|
423
507
|
throw new Error("Failed to substitute all __GATEWAY_ENTRY_POINT__ placeholders");
|
|
424
508
|
}
|
|
@@ -477,9 +561,22 @@ async function writeIfAbsent(path, content, force, report, mode) {
|
|
|
477
561
|
report.created.push(path);
|
|
478
562
|
}
|
|
479
563
|
|
|
564
|
+
// src/setup/buildId.ts
|
|
565
|
+
import { createHash } from "crypto";
|
|
566
|
+
import { readFileSync } from "fs";
|
|
567
|
+
function computeBuildId(entryPath) {
|
|
568
|
+
try {
|
|
569
|
+
return createHash("sha256").update(readFileSync(entryPath)).digest("hex");
|
|
570
|
+
} catch {
|
|
571
|
+
return "unknown";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
480
575
|
// src/setup/sessionStart.ts
|
|
481
576
|
import { spawn } from "child_process";
|
|
482
577
|
import { homedir } from "os";
|
|
578
|
+
import { openSync, closeSync, unlinkSync, statSync, mkdirSync } from "fs";
|
|
579
|
+
import { join as join2 } from "path";
|
|
483
580
|
|
|
484
581
|
// src/setup/gatewayReadiness.ts
|
|
485
582
|
var GATEWAY_READY_TIMEOUT_MS = 5e3;
|
|
@@ -508,6 +605,87 @@ async function waitForGatewayReady(port, options) {
|
|
|
508
605
|
}
|
|
509
606
|
|
|
510
607
|
// src/setup/sessionStart.ts
|
|
608
|
+
var KILL_GRACE_MS = 3e3;
|
|
609
|
+
var KILL_POLL_MS = 100;
|
|
610
|
+
var RELAUNCH_LOCK_STALE_MS = 15e3;
|
|
611
|
+
function relaunchLockPath(home) {
|
|
612
|
+
return join2(home, ".dahlia", "gateway-relaunch.lock");
|
|
613
|
+
}
|
|
614
|
+
async function fetchHealthBuildId(port) {
|
|
615
|
+
try {
|
|
616
|
+
const controller = new AbortController();
|
|
617
|
+
const timer = setTimeout(() => controller.abort(), 500);
|
|
618
|
+
try {
|
|
619
|
+
const res = await fetch(`http://localhost:${port}/api/sentinel/health`, {
|
|
620
|
+
signal: controller.signal
|
|
621
|
+
});
|
|
622
|
+
if (!res.ok) return null;
|
|
623
|
+
const body = await res.json();
|
|
624
|
+
return typeof body.buildId === "string" ? body.buildId : null;
|
|
625
|
+
} finally {
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function isAlive(pid) {
|
|
633
|
+
try {
|
|
634
|
+
process.kill(pid, 0);
|
|
635
|
+
return true;
|
|
636
|
+
} catch {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function terminateDaemon(pid) {
|
|
641
|
+
try {
|
|
642
|
+
process.kill(pid, "SIGTERM");
|
|
643
|
+
} catch {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const deadline = Date.now() + KILL_GRACE_MS;
|
|
647
|
+
while (Date.now() < deadline) {
|
|
648
|
+
if (!isAlive(pid)) return;
|
|
649
|
+
await new Promise((r) => setTimeout(r, KILL_POLL_MS));
|
|
650
|
+
}
|
|
651
|
+
if (isAlive(pid)) {
|
|
652
|
+
try {
|
|
653
|
+
process.kill(pid, "SIGKILL");
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function acquireRelaunchLock(home) {
|
|
659
|
+
const path = relaunchLockPath(home);
|
|
660
|
+
try {
|
|
661
|
+
closeSync(openSync(path, "wx"));
|
|
662
|
+
return true;
|
|
663
|
+
} catch {
|
|
664
|
+
try {
|
|
665
|
+
const age = Date.now() - statSync(path).mtimeMs;
|
|
666
|
+
if (age > RELAUNCH_LOCK_STALE_MS) {
|
|
667
|
+
unlinkSync(path);
|
|
668
|
+
closeSync(openSync(path, "wx"));
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function releaseRelaunchLock(home) {
|
|
677
|
+
try {
|
|
678
|
+
unlinkSync(relaunchLockPath(home));
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function spawnDaemon(entry, policyPath, port, home) {
|
|
683
|
+
const args = entry.endsWith(".ts") ? ["--import", "tsx/esm", entry, "--policy", policyPath, "--port", String(port)] : [entry, "--policy", policyPath, "--port", String(port)];
|
|
684
|
+
const child = spawn("node", args, { detached: true, stdio: "ignore" });
|
|
685
|
+
child.unref();
|
|
686
|
+
if (child.pid) writePidFile(home, child.pid);
|
|
687
|
+
return child.pid;
|
|
688
|
+
}
|
|
511
689
|
async function runSessionStart(options) {
|
|
512
690
|
const cwd = options?.cwd ?? process.cwd();
|
|
513
691
|
const home = options?.home ?? homedir();
|
|
@@ -516,24 +694,39 @@ async function runSessionStart(options) {
|
|
|
516
694
|
if (!policyPath) {
|
|
517
695
|
return { action: "no-policy" };
|
|
518
696
|
}
|
|
697
|
+
mkdirSync(join2(home, ".dahlia"), { recursive: true });
|
|
698
|
+
const gatewayEntry = options?.gatewayEntry ?? resolveGatewayEntryPoint();
|
|
699
|
+
const localBuildId = computeBuildId(gatewayEntry);
|
|
519
700
|
const lock = acquireGatewayLock(home);
|
|
520
701
|
if (lock.reused) {
|
|
521
|
-
await
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
702
|
+
const remoteBuildId = await fetchHealthBuildId(port);
|
|
703
|
+
if (remoteBuildId !== null && remoteBuildId === localBuildId) {
|
|
704
|
+
await waitForGatewayReady(port);
|
|
705
|
+
return { action: "reused", pid: lock.pid, policyPath, relaunchReason: null };
|
|
706
|
+
}
|
|
707
|
+
const reason = remoteBuildId === null ? "health-unreachable-or-unknown" : "build-mismatch";
|
|
708
|
+
if (!acquireRelaunchLock(home)) {
|
|
709
|
+
await waitForGatewayReady(port);
|
|
710
|
+
const peerPid = acquireGatewayLock(home).pid ?? lock.pid;
|
|
711
|
+
return { action: "reused", pid: peerPid, policyPath, relaunchReason: null };
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
if (lock.pid) await terminateDaemon(lock.pid);
|
|
715
|
+
const pid2 = spawnDaemon(gatewayEntry, policyPath, port, home);
|
|
716
|
+
await waitForGatewayReady(port);
|
|
717
|
+
return { action: "relaunched", pid: pid2, policyPath, relaunchReason: reason };
|
|
718
|
+
} finally {
|
|
719
|
+
releaseRelaunchLock(home);
|
|
720
|
+
}
|
|
530
721
|
}
|
|
722
|
+
const pid = spawnDaemon(gatewayEntry, policyPath, port, home);
|
|
531
723
|
await waitForGatewayReady(port);
|
|
532
|
-
return { action: "spawned", pid
|
|
724
|
+
return { action: "spawned", pid, policyPath, relaunchReason: null };
|
|
533
725
|
}
|
|
534
726
|
|
|
535
727
|
export {
|
|
536
728
|
runInitClaudeCode,
|
|
729
|
+
computeBuildId,
|
|
537
730
|
runSessionStart
|
|
538
731
|
};
|
|
539
|
-
//# sourceMappingURL=chunk-
|
|
732
|
+
//# sourceMappingURL=chunk-2TJ5Z53T.js.map
|