@tuent/sentinel 0.1.2 → 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/SECURITY_MODEL.md +9 -0
- package/dist/Sentinel-5CQ6HKXS.js +10 -0
- package/dist/{Sentinel-xFCyXH45.d.ts → Sentinel-BVoMEF3F.d.ts} +15 -0
- package/dist/{chunk-FWIISAZZ.js → chunk-2TJ5Z53T.js} +150 -68
- package/dist/{chunk-L4R3LPJS.js → chunk-G74MMDKA.js} +47 -18
- package/dist/{chunk-QIYQWOLO.js → chunk-JTR2E7RD.js} +166 -57
- package/dist/{chunk-GRN5P3H2.js → chunk-SSDIBY52.js} +21 -6
- package/dist/chunk-TKAKHSZ3.js +1 -0
- package/dist/cli.js +22 -22
- package/dist/gateway/index.d.ts +11 -1
- package/dist/gateway/index.js +2 -2
- package/dist/gatewayDaemon.js +36 -14
- package/dist/index.d.ts +31 -9
- package/dist/index.js +7 -6
- package/package.json +1 -1
- package/dist/Sentinel-XMSJE4DZ.js +0 -10
package/SECURITY_MODEL.md
CHANGED
|
@@ -68,6 +68,15 @@ All target paths are normalized with `path.normalize()` before pattern matching.
|
|
|
68
68
|
- `src/../.env` is normalized to `.env` before checking against `**/.env`
|
|
69
69
|
- `project/subdir/../../.ssh/id_rsa` is normalized to `.ssh/id_rsa`
|
|
70
70
|
|
|
71
|
+
Conversely, `command_exec` targets (whole shell command strings) are **never glob-matched as if they were a path**. They are screened by a scanner: the command is tokenized, path-shaped tokens are resolved and matched against the forbidden patterns, and every argv token's basename is matched against each pattern's basename component with **component-boundary** semantics (the basename must match as a whole, not as a substring). So `process.env` and similarly-shaped code constructs are not treated as file access (the basename `process.env` is not the basename `.env`), while a real operand like `payroll.csv` against a `payroll.csv` forbid pattern is denied — in any command position, not only at the end. This screen is the single source of truth for command screening on **both** the gateway and the embedded-SDK (`wrap()`/`check()`) paths; the gateway additionally keeps its inline L1/L2 scan as defense-in-depth. Correspondingly, `command_exec` is **exempt from the allowed-target allowlist** (and session `SCOPE:` narrowing): those are path globs, and a command string is not a path, so matching it there only produced false `scope_violation`s — which the sensitivity scorer could escalate to a hard block when the command merely named a credential token (e.g. `process.env`). Whether an agent may run commands at all is governed by `allowedActions`; whether a given command is forbidden is the command screen above.
|
|
72
|
+
|
|
73
|
+
Two residuals of letting operators forbid bare basenames in commands (both pre-existing, narrower than the prior whole-string glob, and operator-self-inflicted):
|
|
74
|
+
|
|
75
|
+
- **R1 — generic-word custom basenames.** A custom forbid pattern whose basename is a common word (e.g. a pattern ending in `build`) will also match a command that uses that word as a non-file argument (e.g. `npm run build`). This is intrinsic to forbidding a bare common word; prefer specific basenames for custom command-forbid patterns. The same property holds for path-target matching.
|
|
76
|
+
- **R2 — positional-safety scope.** The echo-class positional-safety suppression (`isPositionallySafeMention`) is scoped to the built-in basename list, not to operator-custom patterns, so a custom basename mentioned in an otherwise-safe position (e.g. as an `echo` argument) is still denied.
|
|
77
|
+
|
|
78
|
+
SDK-path residual: obfuscated/encoded command operands are de-obfuscated only on the gateway path (the translator's resolver pipeline); a direct `wrap()`/`check()` caller gets tokenized + basename + substring screening but not the decode layer.
|
|
79
|
+
|
|
71
80
|
### Self-Protection: Policy and Hook-Wiring Files
|
|
72
81
|
|
|
73
82
|
The agent's own tool-writes to the live policy file (`.sentinel.yaml`) and to Claude Code's hook-wiring settings files (`.claude/settings.json` and `.claude/settings.local.json`, project- and user-level) are **denied by default**. Without this, an agent could rewrite its own policy or remove the hook entries that route its tool calls through the gateway with an ordinary file write. The corresponding forbid patterns ship in the defaults and the starter policy; the read carve-out is appended **code-side at ceiling-role construction** and is deliberately not authorable from the workspace yaml (workspace-authored exceptions are dropped by the ceiling merge, so a policy file can never widen its own protection away).
|
|
@@ -299,6 +299,21 @@ interface SecurityFinding {
|
|
|
299
299
|
* finding is its own distinct entry (never merged).
|
|
300
300
|
*/
|
|
301
301
|
dedupKey?: string;
|
|
302
|
+
/**
|
|
303
|
+
* Sprint 26B F-5a (corroboration-by-distinct-target). Identity of the RESOLVED
|
|
304
|
+
* forbidden target the deny fired on: the L1-resolved path when one exists,
|
|
305
|
+
* else a basename-family fallback (`l2:<sorted basenames>`) for L2-only hits
|
|
306
|
+
* that never produced a resolved path (unparseable / construct ambiguity).
|
|
307
|
+
* When present, getEffectiveBlockCount keys distinct-counting on it INSTEAD of
|
|
308
|
+
* dedupKey, so repeated denials against the same forbidden target — e.g. many
|
|
309
|
+
* differently-shaped commands all naming one token during self-hosted
|
|
310
|
+
* development — contribute ONE count toward the escalation ladder, while
|
|
311
|
+
* distinct forbidden targets still accumulate (a real multi-file sweep
|
|
312
|
+
* escalates as before). Additive and conditional (soft-signal discipline):
|
|
313
|
+
* absent on pre-F-5a entries, which keep counting by dedupKey / keyless
|
|
314
|
+
* exactly as before. NEVER changes the per-command deny disposition.
|
|
315
|
+
*/
|
|
316
|
+
targetKey?: string;
|
|
302
317
|
}
|
|
303
318
|
/** Computed behavioral baseline for an agent over a time window. */
|
|
304
319
|
/**
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./chunk-FMZWHT4M.js";
|
|
8
8
|
import {
|
|
9
9
|
FORBIDDEN_BASENAMES
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JTR2E7RD.js";
|
|
11
11
|
import {
|
|
12
12
|
loadPolicy,
|
|
13
13
|
loadPolicyFromString
|
|
@@ -32,6 +32,7 @@ var HOOK_SCRIPT_SOURCE = `#!/usr/bin/env node
|
|
|
32
32
|
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
33
33
|
import { join, resolve, sep } from "node:path";
|
|
34
34
|
import { spawn } from "node:child_process";
|
|
35
|
+
import { createRequire } from "node:module";
|
|
35
36
|
|
|
36
37
|
const GATEWAY_ENTRY_POINT = "__GATEWAY_ENTRY_POINT__";
|
|
37
38
|
const PORT = 7847;
|
|
@@ -214,65 +215,37 @@ if (mode === "pre") {
|
|
|
214
215
|
process.exit(0);
|
|
215
216
|
|
|
216
217
|
} else if (mode === "session-start") {
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const controller = new AbortController();
|
|
229
|
-
const timer = setTimeout(() => controller.abort(), 500);
|
|
230
|
-
try {
|
|
231
|
-
const res = await fetch(BASE_URL + "/api/sentinel/health", { signal: controller.signal });
|
|
232
|
-
if (res.ok) return; // evaluation-ready
|
|
233
|
-
} finally { clearTimeout(timer); }
|
|
234
|
-
} catch { /* not listening yet \u2014 retry until the bound */ }
|
|
235
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
236
|
-
}
|
|
237
|
-
// timed out \u2014 fall through; the first pre-tool-use uses the existing fail-closed.
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Check if gateway is already running (simple liveness; full PID validity is 5c)
|
|
241
|
-
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() {
|
|
242
228
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
} catch {
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Discover .sentinel.yaml by walking up from cwd
|
|
253
|
-
let dir = process.cwd();
|
|
254
|
-
let policyPath = null;
|
|
255
|
-
while (true) {
|
|
256
|
-
const candidate = join(dir, ".sentinel.yaml");
|
|
257
|
-
if (existsSync(candidate)) { policyPath = candidate; break; }
|
|
258
|
-
const parent = join(dir, "..");
|
|
259
|
-
if (parent === dir) break; // filesystem root
|
|
260
|
-
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;
|
|
261
235
|
}
|
|
262
236
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
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
|
+
});
|
|
276
249
|
process.exit(0);
|
|
277
250
|
|
|
278
251
|
} else if (mode === "session-end") {
|
|
@@ -588,9 +561,22 @@ async function writeIfAbsent(path, content, force, report, mode) {
|
|
|
588
561
|
report.created.push(path);
|
|
589
562
|
}
|
|
590
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
|
+
|
|
591
575
|
// src/setup/sessionStart.ts
|
|
592
576
|
import { spawn } from "child_process";
|
|
593
577
|
import { homedir } from "os";
|
|
578
|
+
import { openSync, closeSync, unlinkSync, statSync, mkdirSync } from "fs";
|
|
579
|
+
import { join as join2 } from "path";
|
|
594
580
|
|
|
595
581
|
// src/setup/gatewayReadiness.ts
|
|
596
582
|
var GATEWAY_READY_TIMEOUT_MS = 5e3;
|
|
@@ -619,6 +605,87 @@ async function waitForGatewayReady(port, options) {
|
|
|
619
605
|
}
|
|
620
606
|
|
|
621
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
|
+
}
|
|
622
689
|
async function runSessionStart(options) {
|
|
623
690
|
const cwd = options?.cwd ?? process.cwd();
|
|
624
691
|
const home = options?.home ?? homedir();
|
|
@@ -627,24 +694,39 @@ async function runSessionStart(options) {
|
|
|
627
694
|
if (!policyPath) {
|
|
628
695
|
return { action: "no-policy" };
|
|
629
696
|
}
|
|
697
|
+
mkdirSync(join2(home, ".dahlia"), { recursive: true });
|
|
698
|
+
const gatewayEntry = options?.gatewayEntry ?? resolveGatewayEntryPoint();
|
|
699
|
+
const localBuildId = computeBuildId(gatewayEntry);
|
|
630
700
|
const lock = acquireGatewayLock(home);
|
|
631
701
|
if (lock.reused) {
|
|
632
|
-
await
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
+
}
|
|
641
721
|
}
|
|
722
|
+
const pid = spawnDaemon(gatewayEntry, policyPath, port, home);
|
|
642
723
|
await waitForGatewayReady(port);
|
|
643
|
-
return { action: "spawned", pid
|
|
724
|
+
return { action: "spawned", pid, policyPath, relaunchReason: null };
|
|
644
725
|
}
|
|
645
726
|
|
|
646
727
|
export {
|
|
647
728
|
runInitClaudeCode,
|
|
729
|
+
computeBuildId,
|
|
648
730
|
runSessionStart
|
|
649
731
|
};
|
|
650
|
-
//# sourceMappingURL=chunk-
|
|
732
|
+
//# sourceMappingURL=chunk-2TJ5Z53T.js.map
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
scanContentForForbiddenBasenames,
|
|
16
16
|
scanGlobPattern,
|
|
17
17
|
tokenizePaths
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-JTR2E7RD.js";
|
|
19
19
|
import {
|
|
20
20
|
loadPolicy,
|
|
21
21
|
policyToConfig,
|
|
@@ -786,23 +786,39 @@ function mcpVerbIsMutating(toolName) {
|
|
|
786
786
|
const tokens = seg.split(/[_-]/).filter((t) => t.length > 0);
|
|
787
787
|
return tokens.some((t) => MCP_MUTATING_VERBS.some((v) => t.startsWith(v)));
|
|
788
788
|
}
|
|
789
|
+
var MCP_MAX_DEPTH = 6;
|
|
790
|
+
var MCP_MAX_NODES = 1e3;
|
|
791
|
+
function collectMcpStrings(value, depth, state) {
|
|
792
|
+
if (state.nodes >= MCP_MAX_NODES) {
|
|
793
|
+
state.truncated = true;
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
state.nodes++;
|
|
797
|
+
if (typeof value === "string") {
|
|
798
|
+
if (value.length > 0) state.strings.push(value);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (value === null || typeof value !== "object") return;
|
|
802
|
+
if (depth >= MCP_MAX_DEPTH) {
|
|
803
|
+
state.truncated = true;
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const children = Array.isArray(value) ? value : Object.values(value);
|
|
807
|
+
for (const child of children) collectMcpStrings(child, depth + 1, state);
|
|
808
|
+
}
|
|
789
809
|
function extractMcpTargets(toolName, toolInput) {
|
|
790
810
|
const targets = [toolName];
|
|
791
811
|
const keys = Object.keys(toolInput);
|
|
792
|
-
|
|
793
|
-
for (const key of keys)
|
|
794
|
-
|
|
795
|
-
if (typeof value !== "string" || value.length === 0) continue;
|
|
796
|
-
extractedStrings++;
|
|
812
|
+
const state = { strings: [], nodes: 0, truncated: false };
|
|
813
|
+
for (const key of keys) collectMcpStrings(toolInput[key], 1, state);
|
|
814
|
+
for (const value of state.strings) {
|
|
797
815
|
targets.push(value);
|
|
798
816
|
const resolved = runResolverPipeline(value);
|
|
799
|
-
for (const r of resolved)
|
|
800
|
-
targets.push(r);
|
|
801
|
-
}
|
|
817
|
+
for (const r of resolved) targets.push(r);
|
|
802
818
|
}
|
|
803
819
|
return {
|
|
804
820
|
targets,
|
|
805
|
-
unextractable: keys.length > 0 &&
|
|
821
|
+
unextractable: keys.length > 0 && (state.strings.length === 0 || state.truncated),
|
|
806
822
|
mutating: mcpVerbIsMutating(toolName)
|
|
807
823
|
};
|
|
808
824
|
}
|
|
@@ -1044,7 +1060,6 @@ function buildModifiedGrepInput(originalInput, exclusions) {
|
|
|
1044
1060
|
import { timingSafeEqual } from "crypto";
|
|
1045
1061
|
var DEFAULT_PORT = 7847;
|
|
1046
1062
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
1047
|
-
var GATEWAY_VERSION = "0.1.0";
|
|
1048
1063
|
function isLoopbackAddress(addr) {
|
|
1049
1064
|
if (!addr) return false;
|
|
1050
1065
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" || addr.startsWith("127.");
|
|
@@ -1101,6 +1116,9 @@ var SentinelGateway = class {
|
|
|
1101
1116
|
releaseToken;
|
|
1102
1117
|
/** Item D (F-8): disposition for unknown (non-MCP, unrecognized) tool names. */
|
|
1103
1118
|
unknownTools;
|
|
1119
|
+
/** Daemon-staleness build identity (content hash of the launched-from entry),
|
|
1120
|
+
* reported via /health. "unknown" when not supplied by the launcher. */
|
|
1121
|
+
buildId;
|
|
1104
1122
|
server = null;
|
|
1105
1123
|
running = false;
|
|
1106
1124
|
signalHandlersInstalled = false;
|
|
@@ -1116,6 +1134,7 @@ var SentinelGateway = class {
|
|
|
1116
1134
|
this.home = options.home ?? "";
|
|
1117
1135
|
this.releaseToken = options.releaseToken ?? null;
|
|
1118
1136
|
this.unknownTools = options.unknownTools ?? "warn";
|
|
1137
|
+
this.buildId = options.buildId ?? "unknown";
|
|
1119
1138
|
const internal = options;
|
|
1120
1139
|
if (internal.registry) {
|
|
1121
1140
|
this.registry = internal.registry;
|
|
@@ -1235,7 +1254,11 @@ var SentinelGateway = class {
|
|
|
1235
1254
|
const snap = this.telemetry.getSnapshot();
|
|
1236
1255
|
this.sendJson(res, 200, {
|
|
1237
1256
|
status: "running",
|
|
1238
|
-
|
|
1257
|
+
// Daemon-staleness build identity (content hash of the launched-from
|
|
1258
|
+
// entry). Replaces the former hardcoded GATEWAY_VERSION, which had
|
|
1259
|
+
// drifted from package.json and would have mismatched spuriously.
|
|
1260
|
+
// session-start compares this to the current on-disk entry hash.
|
|
1261
|
+
buildId: this.buildId,
|
|
1239
1262
|
uptime: snap.uptime_seconds
|
|
1240
1263
|
});
|
|
1241
1264
|
return;
|
|
@@ -1548,6 +1571,7 @@ var SentinelGateway = class {
|
|
|
1548
1571
|
unparseable: anyUnparseable,
|
|
1549
1572
|
hasDangerousConstruct: anyDangerousConstruct
|
|
1550
1573
|
});
|
|
1574
|
+
const targetKey = matchedPath ?? `l2:${[...new Set(allL2Hits)].sort().join(",")}`;
|
|
1551
1575
|
const finding = {
|
|
1552
1576
|
severity: "HIGH",
|
|
1553
1577
|
kind: "actionable",
|
|
@@ -1565,7 +1589,8 @@ var SentinelGateway = class {
|
|
|
1565
1589
|
timestamp: event.timestamp,
|
|
1566
1590
|
decision: "deny",
|
|
1567
1591
|
mentionOnly,
|
|
1568
|
-
dedupKey: event.primaryTarget
|
|
1592
|
+
dedupKey: event.primaryTarget,
|
|
1593
|
+
targetKey
|
|
1569
1594
|
};
|
|
1570
1595
|
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1571
1596
|
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
@@ -1592,7 +1617,9 @@ var SentinelGateway = class {
|
|
|
1592
1617
|
decision: "deny",
|
|
1593
1618
|
// A resolved path-glob hit is a file target — never a mention.
|
|
1594
1619
|
mentionOnly: false,
|
|
1595
|
-
dedupKey: event.primaryTarget
|
|
1620
|
+
dedupKey: event.primaryTarget,
|
|
1621
|
+
// F-5a: the resolved forbidden path IS the target identity here.
|
|
1622
|
+
targetKey: matchedPath
|
|
1596
1623
|
};
|
|
1597
1624
|
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1598
1625
|
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
@@ -2022,9 +2049,10 @@ var SentinelGateway = class {
|
|
|
2022
2049
|
};
|
|
2023
2050
|
async function runGatewayDaemon({
|
|
2024
2051
|
policyPath,
|
|
2025
|
-
port = DEFAULT_PORT
|
|
2052
|
+
port = DEFAULT_PORT,
|
|
2053
|
+
buildId
|
|
2026
2054
|
}) {
|
|
2027
|
-
const { Sentinel: SentinelClass } = await import("./Sentinel-
|
|
2055
|
+
const { Sentinel: SentinelClass } = await import("./Sentinel-5CQ6HKXS.js");
|
|
2028
2056
|
const { writePidFile, writeReleaseToken } = await import("./pidManager-DOGVN6ZT.js");
|
|
2029
2057
|
const { homedir } = await import("os");
|
|
2030
2058
|
const { randomBytes } = await import("crypto");
|
|
@@ -2046,7 +2074,8 @@ async function runGatewayDaemon({
|
|
|
2046
2074
|
home: homedir(),
|
|
2047
2075
|
releaseToken,
|
|
2048
2076
|
unknownTools: operatorConfig.enforcement?.unknownTools,
|
|
2049
|
-
allowUnknownTools: operatorConfig.enforcement?.allowUnknownTools
|
|
2077
|
+
allowUnknownTools: operatorConfig.enforcement?.allowUnknownTools,
|
|
2078
|
+
buildId
|
|
2050
2079
|
});
|
|
2051
2080
|
await gateway.start();
|
|
2052
2081
|
const home = homedir();
|
|
@@ -2059,4 +2088,4 @@ export {
|
|
|
2059
2088
|
SentinelGateway,
|
|
2060
2089
|
runGatewayDaemon
|
|
2061
2090
|
};
|
|
2062
|
-
//# sourceMappingURL=chunk-
|
|
2091
|
+
//# sourceMappingURL=chunk-G74MMDKA.js.map
|
|
@@ -550,7 +550,7 @@ function tokenizePaths(command) {
|
|
|
550
550
|
if (dispatch.unparseable) {
|
|
551
551
|
result.unparseable = true;
|
|
552
552
|
}
|
|
553
|
-
} else if (isPathShaped(token)) {
|
|
553
|
+
} else if (isPathShaped(token) && !isEnvIdentifierChain(token)) {
|
|
554
554
|
const resolved = resolvePathToken(token);
|
|
555
555
|
result.paths.push(resolved);
|
|
556
556
|
}
|
|
@@ -558,6 +558,21 @@ function tokenizePaths(command) {
|
|
|
558
558
|
}
|
|
559
559
|
return result;
|
|
560
560
|
}
|
|
561
|
+
function isEnvIdentifierChain(token) {
|
|
562
|
+
if (token.includes("/") || token.startsWith(".")) return false;
|
|
563
|
+
if (!allEnvOccurrencesIdentifierEmbedded(token)) return false;
|
|
564
|
+
return !SENSITIVE_BASENAME_RE.test(token.replace(/\.env/gi, " "));
|
|
565
|
+
}
|
|
566
|
+
function allEnvOccurrencesIdentifierEmbedded(s) {
|
|
567
|
+
const envHits = /\.env/gi;
|
|
568
|
+
let m;
|
|
569
|
+
let any = false;
|
|
570
|
+
while ((m = envHits.exec(s)) !== null) {
|
|
571
|
+
any = true;
|
|
572
|
+
if (!/[A-Za-z0-9_$]/.test(s[m.index - 1] ?? "")) return false;
|
|
573
|
+
}
|
|
574
|
+
return any;
|
|
575
|
+
}
|
|
561
576
|
function isPathShaped(token) {
|
|
562
577
|
if (token.includes("/")) return true;
|
|
563
578
|
if (token.startsWith(".")) return true;
|
|
@@ -816,6 +831,7 @@ function classifyDeny(command, signals) {
|
|
|
816
831
|
}
|
|
817
832
|
|
|
818
833
|
// src/roleValidator.ts
|
|
834
|
+
import { parse as shellParse2 } from "shell-quote";
|
|
819
835
|
var SUSPICIOUS_BASENAME_RE = /^\.|(\.env|secret|credential|key|config|token)/i;
|
|
820
836
|
function resolveSymlinks(normalizedPath) {
|
|
821
837
|
if (!normalizedPath || normalizedPath.includes("://")) return normalizedPath;
|
|
@@ -1148,10 +1164,26 @@ var RoleValidator = class {
|
|
|
1148
1164
|
}
|
|
1149
1165
|
}
|
|
1150
1166
|
}
|
|
1151
|
-
|
|
1167
|
+
if (event.action === "command_exec") {
|
|
1168
|
+
const cmd = this.screenCommandTarget(event);
|
|
1169
|
+
if (cmd) {
|
|
1170
|
+
const finding = this.buildForbiddenFinding(
|
|
1171
|
+
event,
|
|
1172
|
+
cmd.matchedTarget,
|
|
1173
|
+
cmd.matchedPattern,
|
|
1174
|
+
activeTask ?? null
|
|
1175
|
+
);
|
|
1176
|
+
if (finding) {
|
|
1177
|
+
finding.targetKey = cmd.targetKey;
|
|
1178
|
+
finding.dedupKey = event.primaryTarget;
|
|
1179
|
+
finding.mentionOnly = false;
|
|
1180
|
+
return finding;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1152
1184
|
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
1153
1185
|
let matchedTargetValue = null;
|
|
1154
|
-
if (
|
|
1186
|
+
if (event.action !== "command_exec" && primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
|
|
1155
1187
|
matchedTargetValue = normalizedPrimaryTarget;
|
|
1156
1188
|
}
|
|
1157
1189
|
if (!matchedTargetValue) {
|
|
@@ -1169,57 +1201,13 @@ var RoleValidator = class {
|
|
|
1169
1201
|
}
|
|
1170
1202
|
}
|
|
1171
1203
|
if (matchedTargetValue) {
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
"unauthorized_target",
|
|
1180
|
-
matchedTargetValue,
|
|
1181
|
-
event.action
|
|
1182
|
-
),
|
|
1183
|
-
matchedTarget: matchedTargetValue
|
|
1184
|
-
});
|
|
1185
|
-
if (!isCritical) {
|
|
1186
|
-
const exception = findMatchingException(this.role.exceptions, event, activeTask ?? null);
|
|
1187
|
-
if (exception) {
|
|
1188
|
-
this.onAuditEntry?.({
|
|
1189
|
-
type: "exception_applied",
|
|
1190
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1191
|
-
agentId: event.agentId,
|
|
1192
|
-
target: matchedTargetValue,
|
|
1193
|
-
action: event.action,
|
|
1194
|
-
exceptionTarget: exception.target,
|
|
1195
|
-
taskId: activeTask?.taskId ?? null
|
|
1196
|
-
});
|
|
1197
|
-
if (exception.requiresApproval) {
|
|
1198
|
-
const ctx = {
|
|
1199
|
-
finding,
|
|
1200
|
-
exception,
|
|
1201
|
-
activeTask: activeTask ?? null,
|
|
1202
|
-
expiresAt: exception.expiresAfter != null && activeTask ? new Date(new Date(activeTask.startedAt).getTime() + exception.expiresAfter) : null
|
|
1203
|
-
};
|
|
1204
|
-
const approved = this.approvalFn?.(ctx) === true;
|
|
1205
|
-
if (approved) {
|
|
1206
|
-
this.onAuditEntry?.({
|
|
1207
|
-
type: "exception_approved",
|
|
1208
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1209
|
-
agentId: event.agentId,
|
|
1210
|
-
target: matchedTargetValue,
|
|
1211
|
-
action: event.action,
|
|
1212
|
-
exceptionTarget: exception.target,
|
|
1213
|
-
taskId: activeTask?.taskId ?? null
|
|
1214
|
-
});
|
|
1215
|
-
continue;
|
|
1216
|
-
}
|
|
1217
|
-
} else {
|
|
1218
|
-
continue;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
return this.enhanceWithSensitivity(finding, event);
|
|
1204
|
+
const finding = this.buildForbiddenFinding(
|
|
1205
|
+
event,
|
|
1206
|
+
matchedTargetValue,
|
|
1207
|
+
pattern,
|
|
1208
|
+
activeTask ?? null
|
|
1209
|
+
);
|
|
1210
|
+
if (finding) return finding;
|
|
1223
1211
|
}
|
|
1224
1212
|
}
|
|
1225
1213
|
if (this.sensitivityScorer && event.metadata?._mcpTool === "true") {
|
|
@@ -1238,7 +1226,7 @@ var RoleValidator = class {
|
|
|
1238
1226
|
});
|
|
1239
1227
|
}
|
|
1240
1228
|
}
|
|
1241
|
-
if (this.role.allowedTargetPatterns.length > 0) {
|
|
1229
|
+
if (this.role.allowedTargetPatterns.length > 0 && event.action !== "command_exec") {
|
|
1242
1230
|
const anchor = this.workspaceRoot !== "" && PATH_TARGET_ACTIONS.has(event.action);
|
|
1243
1231
|
const matched = this.role.allowedTargetPatterns.some((pattern) => {
|
|
1244
1232
|
const candidates = anchor ? [pattern, anchorAllowedPattern(pattern, this.workspaceRoot)] : [pattern];
|
|
@@ -1276,7 +1264,7 @@ var RoleValidator = class {
|
|
|
1276
1264
|
}
|
|
1277
1265
|
}
|
|
1278
1266
|
const scopePatterns = activeTask?.scopePatterns;
|
|
1279
|
-
if (scopePatterns && scopePatterns.length > 0) {
|
|
1267
|
+
if (scopePatterns && scopePatterns.length > 0 && event.action !== "command_exec") {
|
|
1280
1268
|
const anchor = this.workspaceRoot !== "" && PATH_TARGET_ACTIONS.has(event.action);
|
|
1281
1269
|
const inScope = scopePatterns.some((pattern) => {
|
|
1282
1270
|
const candidates = anchor ? [pattern, anchorAllowedPattern(pattern, this.workspaceRoot)] : [pattern];
|
|
@@ -1340,6 +1328,127 @@ var RoleValidator = class {
|
|
|
1340
1328
|
}
|
|
1341
1329
|
return finding;
|
|
1342
1330
|
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Sprint 26B B′ — scanner-authoritative forbidden screen for a command_exec
|
|
1333
|
+
* PRIMARY target (the command string). Three layers, first match wins:
|
|
1334
|
+
*
|
|
1335
|
+
* L1 — tokenizePaths(command): resolved path-shaped tokens (symlink-resolved,
|
|
1336
|
+
* dir-globs, F-5b's isEnvIdentifierChain guard inside) matched against the
|
|
1337
|
+
* FULL role patterns via matchGlob.
|
|
1338
|
+
* Boundary — every argv token's basename vs each pattern's basename component
|
|
1339
|
+
* via the SAME matchGlob (its `^…$` anchoring → true boundary, so
|
|
1340
|
+
* `^\.env$` rejects `process.env` and `^payroll\.csv$` rejects
|
|
1341
|
+
* `mypayroll.csv`, while a `payroll.csv` token matches a `payroll.csv`
|
|
1342
|
+
* pattern basename). Patterns whose basename is pure-wildcard (a bare
|
|
1343
|
+
* star or globstar) are dir-globs and are skipped here (L1 handles them
|
|
1344
|
+
* on resolved paths).
|
|
1345
|
+
* L2 — scanBashCommand substring net (fixed FORBIDDEN_BASENAMES) gated by
|
|
1346
|
+
* isPositionallySafeMention, for heredoc/substitution shapes tokenization
|
|
1347
|
+
* can't cleanly split.
|
|
1348
|
+
*
|
|
1349
|
+
* Returns the matched target + the F-5a targetKey (resolved path / token /
|
|
1350
|
+
* `l2:<basenames>`, mirroring the gateway emit) + a pattern label for the
|
|
1351
|
+
* finding description, or null when nothing matches.
|
|
1352
|
+
*/
|
|
1353
|
+
screenCommandTarget(event) {
|
|
1354
|
+
const command = event.primaryTarget;
|
|
1355
|
+
if (isPositionallySafeMention(command)) return null;
|
|
1356
|
+
const tr = tokenizePaths(command);
|
|
1357
|
+
for (const p of tr.paths) {
|
|
1358
|
+
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
1359
|
+
if (matchGlobInsensitive(pattern, p)) {
|
|
1360
|
+
return { matchedTarget: p, targetKey: p, matchedPattern: pattern };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
let tokens;
|
|
1365
|
+
try {
|
|
1366
|
+
tokens = shellParse2(command);
|
|
1367
|
+
} catch {
|
|
1368
|
+
tokens = [];
|
|
1369
|
+
}
|
|
1370
|
+
for (const tok of tokens) {
|
|
1371
|
+
if (typeof tok !== "string") continue;
|
|
1372
|
+
const tokBase = basename2(tok);
|
|
1373
|
+
if (tokBase.length === 0) continue;
|
|
1374
|
+
for (const pattern of this.role.forbiddenTargetPatterns) {
|
|
1375
|
+
const patBase = basename2(pattern);
|
|
1376
|
+
if (patBase === "*" || patBase === "**") continue;
|
|
1377
|
+
if (matchGlobInsensitive(patBase, tokBase)) {
|
|
1378
|
+
return { matchedTarget: tok, targetKey: tok, matchedPattern: pattern };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const scan = scanBashCommand(command, FORBIDDEN_BASENAMES);
|
|
1383
|
+
if (scan.matched) {
|
|
1384
|
+
const hits = [...new Set(scan.hits)].sort();
|
|
1385
|
+
return {
|
|
1386
|
+
matchedTarget: scan.hits[0],
|
|
1387
|
+
targetKey: `l2:${hits.join(",")}`,
|
|
1388
|
+
matchedPattern: hits.join(", ")
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Build the unauthorized_target finding for a matched forbidden target, applying
|
|
1395
|
+
* the same exception/approval/sensitivity flow shared by Check 2's per-pattern
|
|
1396
|
+
* loop and the B′ command screen. Returns the finding to emit, or null when an
|
|
1397
|
+
* exception (auto, or approved) suppresses it (caller continues / falls through).
|
|
1398
|
+
*/
|
|
1399
|
+
buildForbiddenFinding(event, matchedTargetValue, pattern, activeTask) {
|
|
1400
|
+
const sensitivity = this.sensitivityScorer?.scoreTarget(matchedTargetValue, event.action);
|
|
1401
|
+
const isCritical = sensitivity ? sensitivity.effectiveScore >= 0.9 : false;
|
|
1402
|
+
const finding = this.makeFinding(event, {
|
|
1403
|
+
severity: "HIGH",
|
|
1404
|
+
type: "unauthorized_target",
|
|
1405
|
+
description: `Agent accessed '${matchedTargetValue}' which matches forbidden pattern '${pattern}'`,
|
|
1406
|
+
recommendation: getTargetRecommendation(
|
|
1407
|
+
"unauthorized_target",
|
|
1408
|
+
matchedTargetValue,
|
|
1409
|
+
event.action
|
|
1410
|
+
),
|
|
1411
|
+
matchedTarget: matchedTargetValue
|
|
1412
|
+
});
|
|
1413
|
+
if (!isCritical) {
|
|
1414
|
+
const exception = findMatchingException(this.role.exceptions, event, activeTask);
|
|
1415
|
+
if (exception) {
|
|
1416
|
+
this.onAuditEntry?.({
|
|
1417
|
+
type: "exception_applied",
|
|
1418
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1419
|
+
agentId: event.agentId,
|
|
1420
|
+
target: matchedTargetValue,
|
|
1421
|
+
action: event.action,
|
|
1422
|
+
exceptionTarget: exception.target,
|
|
1423
|
+
taskId: activeTask?.taskId ?? null
|
|
1424
|
+
});
|
|
1425
|
+
if (exception.requiresApproval) {
|
|
1426
|
+
const ctx = {
|
|
1427
|
+
finding,
|
|
1428
|
+
exception,
|
|
1429
|
+
activeTask,
|
|
1430
|
+
expiresAt: exception.expiresAfter != null && activeTask ? new Date(new Date(activeTask.startedAt).getTime() + exception.expiresAfter) : null
|
|
1431
|
+
};
|
|
1432
|
+
const approved = this.approvalFn?.(ctx) === true;
|
|
1433
|
+
if (approved) {
|
|
1434
|
+
this.onAuditEntry?.({
|
|
1435
|
+
type: "exception_approved",
|
|
1436
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1437
|
+
agentId: event.agentId,
|
|
1438
|
+
target: matchedTargetValue,
|
|
1439
|
+
action: event.action,
|
|
1440
|
+
exceptionTarget: exception.target,
|
|
1441
|
+
taskId: activeTask?.taskId ?? null
|
|
1442
|
+
});
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
} else {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return this.enhanceWithSensitivity(finding, event);
|
|
1451
|
+
}
|
|
1343
1452
|
makeFinding(event, details) {
|
|
1344
1453
|
return {
|
|
1345
1454
|
severity: details.severity,
|
|
@@ -1788,4 +1897,4 @@ export {
|
|
|
1788
1897
|
findMatchingException,
|
|
1789
1898
|
RoleValidator
|
|
1790
1899
|
};
|
|
1791
|
-
//# sourceMappingURL=chunk-
|
|
1900
|
+
//# sourceMappingURL=chunk-JTR2E7RD.js.map
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
unionWithDefaultForbiddenPatterns,
|
|
19
19
|
walkForbiddenInodeRoots,
|
|
20
20
|
withPolicyReadExceptions
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-JTR2E7RD.js";
|
|
22
22
|
import {
|
|
23
23
|
loadPolicy,
|
|
24
24
|
policyToConfig,
|
|
@@ -315,6 +315,9 @@ var ProfileStore = class {
|
|
|
315
315
|
this.profile = await this.backend.read();
|
|
316
316
|
this.engine = new DataEngine({ petalCount: this.petalCount });
|
|
317
317
|
if (this.profile) {
|
|
318
|
+
if (!Array.isArray(this.profile.dataPoints)) {
|
|
319
|
+
this.profile.dataPoints = [];
|
|
320
|
+
}
|
|
318
321
|
for (const point of this.profile.dataPoints) {
|
|
319
322
|
this.engine.addPoint(point);
|
|
320
323
|
}
|
|
@@ -618,6 +621,7 @@ var AgentActivityClassifier = class {
|
|
|
618
621
|
|
|
619
622
|
// src/auditTrail.ts
|
|
620
623
|
import { createHash } from "crypto";
|
|
624
|
+
import { existsSync } from "fs";
|
|
621
625
|
var AuditTrail = class _AuditTrail {
|
|
622
626
|
static GENESIS_HASH = "0".repeat(64);
|
|
623
627
|
/** S21-P4: honest label for cumulative-stats.json's totalEntries. */
|
|
@@ -651,6 +655,11 @@ var AuditTrail = class _AuditTrail {
|
|
|
651
655
|
this.agentId = agentId;
|
|
652
656
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
653
657
|
const logDir = options?.logDir ?? `${home}/.dahlia/agents/${agentId}`;
|
|
658
|
+
if (options?.logDir && !existsSync(`${logDir}/audit.log`) && existsSync(`${logDir}/${agentId}/audit.log`)) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`AuditTrail logDir must be the agent's own directory, not the agents root: "${logDir}" contains no audit.log, but "${logDir}/${agentId}/audit.log" exists. Pass join(agentsRoot, "${agentId}") instead.`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
654
663
|
this.logPath = `${logDir}/audit.log`;
|
|
655
664
|
this.statsPath = `${logDir}/cumulative-stats.json`;
|
|
656
665
|
this.manifestPath = `${logDir}/audit.manifest`;
|
|
@@ -749,6 +758,9 @@ var AuditTrail = class _AuditTrail {
|
|
|
749
758
|
if (finding.dedupKey !== void 0) {
|
|
750
759
|
entry.dedupKey = finding.dedupKey;
|
|
751
760
|
}
|
|
761
|
+
if (finding.targetKey !== void 0) {
|
|
762
|
+
entry.targetKey = finding.targetKey;
|
|
763
|
+
}
|
|
752
764
|
await this.writeLine(entry);
|
|
753
765
|
}
|
|
754
766
|
/**
|
|
@@ -2978,6 +2990,7 @@ var SentinelRunner = class {
|
|
|
2978
2990
|
}
|
|
2979
2991
|
async runGapCheck() {
|
|
2980
2992
|
if (!this._deviationDetector || !this.auditTrail) return;
|
|
2993
|
+
if (this._eventCount === 0) return;
|
|
2981
2994
|
const finding = await this._deviationDetector.checkActivityGap(this.auditTrail);
|
|
2982
2995
|
if (finding) {
|
|
2983
2996
|
this.findings.push(finding);
|
|
@@ -3529,7 +3542,7 @@ var SentinelManager = class {
|
|
|
3529
3542
|
};
|
|
3530
3543
|
|
|
3531
3544
|
// src/Sentinel.ts
|
|
3532
|
-
import { lstatSync, existsSync } from "fs";
|
|
3545
|
+
import { lstatSync, existsSync as existsSync2 } from "fs";
|
|
3533
3546
|
import { resolve as resolve2, dirname, sep as sep2 } from "path";
|
|
3534
3547
|
|
|
3535
3548
|
// src/baselineBuilder.ts
|
|
@@ -6418,7 +6431,7 @@ var Sentinel = class _Sentinel {
|
|
|
6418
6431
|
);
|
|
6419
6432
|
}
|
|
6420
6433
|
}
|
|
6421
|
-
if (!
|
|
6434
|
+
if (!existsSync2(root)) {
|
|
6422
6435
|
console.warn(
|
|
6423
6436
|
`[Sentinel] Workspace root '${root}' does not exist on disk \u2014 anchored allowed patterns will match nothing and every allowed read may be denied.`
|
|
6424
6437
|
);
|
|
@@ -7421,8 +7434,10 @@ var Sentinel = class _Sentinel {
|
|
|
7421
7434
|
const distinctKeys = /* @__PURE__ */ new Set();
|
|
7422
7435
|
let keyless = 0;
|
|
7423
7436
|
for (const f of survivors) {
|
|
7424
|
-
const
|
|
7425
|
-
|
|
7437
|
+
const tk = f.targetKey;
|
|
7438
|
+
const dk = f.dedupKey;
|
|
7439
|
+
if (typeof tk === "string" && tk.length > 0) distinctKeys.add(`t:${tk}`);
|
|
7440
|
+
else if (typeof dk === "string" && dk.length > 0) distinctKeys.add(`c:${dk}`);
|
|
7426
7441
|
else keyless++;
|
|
7427
7442
|
}
|
|
7428
7443
|
return distinctKeys.size + keyless;
|
|
@@ -7470,4 +7485,4 @@ export {
|
|
|
7470
7485
|
createCliApproval,
|
|
7471
7486
|
Sentinel
|
|
7472
7487
|
};
|
|
7473
|
-
//# sourceMappingURL=chunk-
|
|
7488
|
+
//# sourceMappingURL=chunk-SSDIBY52.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-TKAKHSZ3.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
`];for(const t of e){const a=Se(t.layer),r=t.isRichData?"":" [filler]";n.push(`- ${t.label}${r}`),n.push(` Category: ${t.category}`),n.push(` Layer zone: ${a} (${(t.layer*100).toFixed(0)}%)`),n.push(` Openness: ${(t.openness*100).toFixed(0)}%`),n.push(` Description: ${t.description}`),n.push(` Last active: ${t.lastActive}`);const
|
|
4
|
-
`).map(
|
|
2
|
+
import"./chunk-TKAKHSZ3.js";import{AgentProfileManager as H,AlertManager as de,AuditTrail as A,BaselineBuilder as N,CorrelationDetector as ge,DeviationDetector as B,FileStorageBackend as ue,ProfileStore as fe,ReportGenerator as pe,Sentinel as R,SentinelRunner as me,generateFleetReport as he}from"./chunk-SSDIBY52.js";import{runInitClaudeCode as ye}from"./chunk-2TJ5Z53T.js";import{readReleaseToken as we}from"./chunk-LATQNIRW.js";import{deriveAgentId as $e}from"./chunk-B5QKJHSV.js";import"./chunk-FMZWHT4M.js";import"./chunk-JTR2E7RD.js";import{loadPolicy as ve}from"./chunk-WLIDSTS4.js";import{getOrCreateKeyPair as M}from"./chunk-NUXSUSYY.js";import{join as u}from"path";import{homedir as m}from"os";import{readFile as j,writeFile as C,access as K,mkdir as W}from"fs/promises";function Ae(e){const o=new Date(e);if(isNaN(o.getTime()))return"unknown";const t=Date.now()-o.getTime();if(t<0)return"just now";const a=Math.floor(t/6e4);if(a<1)return"just now";if(a<60)return`${a} minute${a===1?"":"s"} ago`;const r=Math.floor(a/60);if(r<24)return`${r} hour${r===1?"":"s"} ago`;const s=Math.floor(r/24);if(s<14)return`${s} day${s===1?"":"s"} ago`;const d=Math.floor(s/7);if(s<60)return`${d} week${d===1?"":"s"} ago`;if(s>=365)return"over a year ago";const c=Math.floor(s/30);return`${c} month${c===1?"":"s"} ago`}function Ie(e,o){const n=new Date(o),t=isNaN(n.getTime())?1/0:Math.floor((Date.now()-n.getTime())/864e5);return e<.3||t>30?"declining":e>.7&&t<7?"rising":"stable"}function Se(e){return e<.3?"inner":e<.65?"middle":"outer"}function G(e){if(e.length===0)return"No petals selected.";const o=new Map;for(const t of e)o.set(t.id,t.label);const n=[`Selected petals (${e.length}):
|
|
3
|
+
`];for(const t of e){const a=Se(t.layer),r=t.isRichData?"":" [filler]";n.push(`- ${t.label}${r}`),n.push(` Category: ${t.category}`),n.push(` Layer zone: ${a} (${(t.layer*100).toFixed(0)}%)`),n.push(` Openness: ${(t.openness*100).toFixed(0)}%`),n.push(` Description: ${t.description}`),n.push(` Last active: ${t.lastActive}`);const s=Ae(t.lastActive),d=t.weight!=null?Ie(t.weight,t.lastActive):"stable";if(n.push(` Temporal: Last active ${s} | Weight trend: ${d}`),t.source){const c={seed:"seed data",agent:"observed from activity",manual:"filesystem scan",diary:"personal diary entry",conversation:"created from conversation","agent-monitor":"monitored agent activity"};n.push(` Source: ${c[t.source]??t.source}`)}if(t.weight!=null&&n.push(` Weight: ${t.weight.toFixed(2)}`),t.connections.length>0){const c=t.connections.map(i=>o.get(i)??De(i));n.push(` Connections: ${c.join(", ")}`)}if(t.files&&t.files.length>0){const c=t.files.slice(0,10).map(i=>i.split("/").pop()??i);n.push(` Key files: ${c.join(", ")}`)}if(t.fileContents&&t.fileContents.length>0){n.push(" File contents:");for(const c of t.fileContents)n.push(` --- ${c.name} ---`),n.push(c.content.split(`
|
|
4
|
+
`).map(i=>` ${i}`).join(`
|
|
5
5
|
`))}n.push("")}return n.join(`
|
|
6
6
|
`)}function De(e){return e.split("-").map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join(" ")}function be(e){const o=[Te(e.agentName,e.agentId,e.role),Re(e.baseline),Ce(e.e),Ee(e.findings),Me()],n=je(e.t);return n&&o.push(n),o.join(`
|
|
7
7
|
|
|
@@ -14,20 +14,20 @@ ${G(e)}`}function Ee(e){const o="=== Security Findings ===";if(e.length===0)retu
|
|
|
14
14
|
No security findings detected. Agent behavior is within expected parameters.`;const n=[o];for(const t of e)n.push(`[${t.severity}] ${t.type}`),n.push(` Description: ${t.description}`),n.push(` Evidence: ${t.evidence.action} \u2192 ${t.evidence.target} at ${t.evidence.timestamp}`),t.evidence.baselineComparison&&n.push(` Baseline comparison: ${t.evidence.baselineComparison}`),n.push(` Recommendation: ${t.recommendation}`),n.push("");return n.join(`
|
|
15
15
|
`)}function Me(){return["=== Analysis Request ===","Based on the data above, provide:","1. OVERALL HEALTH ASSESSMENT: Is this agent operating normally?","2. RISK SUMMARY: What is the overall risk level?","3. FINDING REVIEW: For each finding, confirm or adjust its severity with reasoning.","4. RECOMMENDATIONS: Specific actions the owner should take.","5. MONITORING GUIDANCE: What patterns to watch for going forward."].join(`
|
|
16
16
|
`)}function je(e){return!e||e.length===0?null:`=== Owner Context ===
|
|
17
|
-
${G(e)}`}import{request as J}from"http";var f=process.argv.slice(2),p=w(f,"--agent"),U=w(f,"--name"),ke=w(f,"--role"),Ne=f.includes("--create"),Fe=f.includes("--compute-baseline"),xe=f.includes("--monitor"),Pe=f.includes("--init-config"),Oe=f.includes("--init-policy"),Le=f.includes("init")&&f.includes("claude-code"),qe=f.includes("--force"),_=w(f,"--from-policy"),He=f.includes("--report"),Be=f.includes("--report-all"),Ke=f.includes("--correlations"),We=f.includes("--verify-audit"),Ge=f.includes("--enroll-manifest"),Je=f.includes("--recompute-stats"),Ue=f.includes("--health"),_e=f.includes("--restrict"),Qe=f.includes("--quarantine"),Ve=f.includes("--release")||f[0]==="release",ze=f.includes("--status"),Ye=f.includes("--start-task"),Xe=f.includes("--end-task"),Ze=f.includes("--intent-status"),et=f.includes("--intent-report"),Q=w(f,"--task-id"),V=w(f,"--description"),z=w(f,"--relaxed-actions"),Y=w(f,"--phases"),X=w(f,"--reason"),F=w(f,"--quarantine"),x=w(f,"--quarantine-reason"),
|
|
18
|
-
`)}function te(e){const o=["Agent ID","Mode","Reason","Changed"],n=e.map(
|
|
19
|
-
`)}function ne(e){const o=["Agent ID","Sessions","Last Active","Role","Baseline","Mode","Status"],n=e.map(
|
|
20
|
-
`)}function P(e,o){return e<10?"New":o.some(n=>n.severity==="HIGH"||n.severity==="CRITICAL")?"At Risk":o.some(n=>n.severity==="MEDIUM")?"Caution":"Healthy"}function O(e,o){const n=Date.now()-o*24*60*60*1e3;return e.filter(t=>new Date(t.lastActive).getTime()>=n)}function oe(e){return{id:e.id,label:e.label,category:e.category,description:e.description,layer:e.layer,lastActive:e.lastActive,openness:e.openness,connections:e.connections,isRichData:!0,source:e.source,core:e.core,sharable:e.sharable,weight:e.weight,files:e.files}}function L(e,o){const n=["Sentinel \u2014 Live Monitoring","\u2500".repeat(26)];for(const[t,a]of e){const r=(o.get(t)??"manual").toUpperCase(),
|
|
21
|
-
`)}function ae(e){const o=["Monitoring stopped. Summary:"];for(const[n,t]of e){const a=t.getFindings();let r=`${a.length} findings`;if(a.length>0){const
|
|
22
|
-
`)}async function tt(e,o){const n=u(m(),".dahlia","agents"),t=await
|
|
23
|
-
`),console.log(te(n));const t=n.filter(a=>a.mode!=="normal");if(t.length>0){console.log("");for(const a of t){const r=a.mode==="quarantined"?"QUARANTINED":"RESTRICTED";console.log(`\u26A0 ${a.agentId}: ${r}`)}}}async function rt(){const e=await S.listAgents();if(e.length===0){console.log("No monitored agents found. Create one with --create --agent=ID --name=NAME");return}const o=[];for(const t of e)try{const
|
|
24
|
-
`),console.log(ne(o))}async function lt(e){const o=await S.loadAgentProfile(e),n=await S.loadRole(e),a=await new N(e).loadBaseline(e),
|
|
17
|
+
${G(e)}`}import{request as J}from"http";var f=process.argv.slice(2),p=w(f,"--agent"),U=w(f,"--name"),ke=w(f,"--role"),Ne=f.includes("--create"),Fe=f.includes("--compute-baseline"),xe=f.includes("--monitor"),Pe=f.includes("--init-config"),Oe=f.includes("--init-policy"),Le=f.includes("init")&&f.includes("claude-code"),qe=f.includes("--force"),_=w(f,"--from-policy"),He=f.includes("--report"),Be=f.includes("--report-all"),Ke=f.includes("--correlations"),We=f.includes("--verify-audit"),Ge=f.includes("--enroll-manifest"),Je=f.includes("--recompute-stats"),Ue=f.includes("--health"),_e=f.includes("--restrict"),Qe=f.includes("--quarantine"),Ve=f.includes("--release")||f[0]==="release",ze=f.includes("--status"),Ye=f.includes("--start-task"),Xe=f.includes("--end-task"),Ze=f.includes("--intent-status"),et=f.includes("--intent-report"),Q=w(f,"--task-id"),V=w(f,"--description"),z=w(f,"--relaxed-actions"),Y=w(f,"--phases"),X=w(f,"--reason"),F=w(f,"--quarantine"),x=w(f,"--quarantine-reason"),b=w(f,"--period"),Z=w(f,"--format"),S=new H;function w(e,o){const n=e.find(a=>a.startsWith(o+"="));if(!n)return;const t=n.indexOf("=");return n.slice(t+1)}async function T(e,o){try{const n=await j(u(e,o,"mode.json"),"utf-8"),t=JSON.parse(n);return t.mode==="restricted"||t.mode==="quarantined"||t.mode==="normal"?t:{mode:"normal"}}catch{return{mode:"normal"}}}function ee(e){const o=["Agent","Score","Status","Mode","C/H/M/L","Last Event","Baseline"],n=e.map(i=>[i.agentId,String(i.score),i.status,i.mode,i.findings,i.lastEvent,i.baseline?"\u2713":"\u2717"]),t=[o,...n],a=o.map((i,l)=>Math.max(...t.map(g=>g[l].length))),r=(i,l)=>i+" ".repeat(l-i.length),s=o.map((i,l)=>r(i,a[l])).join(" "),d=a.map(i=>"-".repeat(i)).join(" "),c=n.map(i=>i.map((l,g)=>r(l,a[g])).join(" "));return[s,d,...c].join(`
|
|
18
|
+
`)}function te(e){const o=["Agent ID","Mode","Reason","Changed"],n=e.map(i=>[i.agentId,i.mode.toUpperCase(),i.reason,i.changed]),t=[o,...n],a=o.map((i,l)=>Math.max(...t.map(g=>g[l].length))),r=(i,l)=>i+" ".repeat(l-i.length),s=o.map((i,l)=>r(i,a[l])).join(" "),d=a.map(i=>"-".repeat(i)).join(" "),c=n.map(i=>i.map((l,g)=>r(l,a[g])).join(" "));return[s,d,...c].join(`
|
|
19
|
+
`)}function ne(e){const o=["Agent ID","Sessions","Last Active","Role","Baseline","Mode","Status"],n=e.map(i=>[i.agentId,String(i.sessions),i.lastActive,i.roleDefined?"\u2713":"\u2717",i.baselineDefined?"\u2713":"\u2717",i.mode??"normal",i.status]),t=[o,...n],a=o.map((i,l)=>Math.max(...t.map(g=>g[l].length))),r=(i,l)=>i+" ".repeat(l-i.length),s=o.map((i,l)=>r(i,a[l])).join(" "),d=a.map(i=>"-".repeat(i)).join(" "),c=n.map(i=>i.map((l,g)=>r(l,a[g])).join(" "));return[s,d,...c].join(`
|
|
20
|
+
`)}function P(e,o){return e<10?"New":o.some(n=>n.severity==="HIGH"||n.severity==="CRITICAL")?"At Risk":o.some(n=>n.severity==="MEDIUM")?"Caution":"Healthy"}function O(e,o){const n=Date.now()-o*24*60*60*1e3;return e.filter(t=>new Date(t.lastActive).getTime()>=n)}function oe(e){return{id:e.id,label:e.label,category:e.category,description:e.description,layer:e.layer,lastActive:e.lastActive,openness:e.openness,connections:e.connections,isRichData:!0,source:e.source,core:e.core,sharable:e.sharable,weight:e.weight,files:e.files}}function L(e,o){const n=["Sentinel \u2014 Live Monitoring","\u2500".repeat(26)];for(const[t,a]of e){const r=(o.get(t)??"manual").toUpperCase(),s=a.getFindings(),d=s.filter(y=>y.severity==="HIGH"||y.severity==="CRITICAL").length,c=s.filter(y=>y.severity==="MEDIUM").length;let i=`${s.length} findings`;d>0?i=`${d} HIGH`:c>0&&(i=`${c} MEDIUM`);const l=P(a.sessionCount,s),g=t.padEnd(18),h=r.padEnd(5);n.push(`[${g}] ${h}| ${a.eventCount} events | ${a.sessionCount} sessions | ${i} | ${l}`)}return n.push(""),n.push("Press Ctrl+C to stop."),n.join(`
|
|
21
|
+
`)}function ae(e){const o=["Monitoring stopped. Summary:"];for(const[n,t]of e){const a=t.getFindings();let r=`${a.length} findings`;if(a.length>0){const s={};for(const c of a)s[c.severity]=(s[c.severity]??0)+1;const d=Object.entries(s).map(([c,i])=>`${i} ${c}`);r=`${a.length} finding${a.length>1?"s":""} (${d.join(", ")})`}o.push(`- ${n}: ${t.eventCount} events, ${t.sessionCount} sessions, ${r}`)}return o.join(`
|
|
22
|
+
`)}async function tt(e,o){const n=u(m(),".dahlia","agents"),t=await T(n,e);if(t.mode==="quarantined"){console.log(`Agent ${e} is quarantined \u2014 cannot downgrade to restricted.`);return}const a=t.mode;await W(u(n,e),{recursive:!0}),await C(u(n,e,"mode.json"),JSON.stringify({mode:"restricted",reason:o,timestamp:new Date().toISOString(),previousMode:a}),"utf-8");const r=await M(u(n,e)),s=new A(e,{logDir:u(n,e)});await s.open(),s.setSigningKey(r.privateKey,r.publicKey),await s.logModeChange("restricted",o,a),await s.close(),console.log(`Agent ${e} RESTRICTED: ${o}`)}async function nt(e,o){const n=u(m(),".dahlia","agents"),a=(await T(n,e)).mode;await W(u(n,e),{recursive:!0}),await C(u(n,e,"mode.json"),JSON.stringify({mode:"quarantined",reason:o,timestamp:new Date().toISOString(),previousMode:a}),"utf-8");const r=await M(u(n,e)),s=new A(e,{logDir:u(n,e)});await s.open(),s.setSigningKey(r.privateKey,r.publicKey),await s.logModeChange("quarantined",o,a),await s.close(),console.log(`Agent ${e} QUARANTINED: ${o}`)}function ot(e,o=1500){return new Promise(n=>{const t=J({host:"127.0.0.1",port:e,path:"/api/sentinel/health",method:"GET"},a=>{a.resume(),n(a.statusCode===200)});t.on("error",()=>n(!1)),t.setTimeout(o,()=>{t.destroy(),n(!1)}),t.end()})}function at(e,o,n){return new Promise((t,a)=>{const r=JSON.stringify({agentId:n,reason:"operator release (sentinel release)"}),s=J({host:"127.0.0.1",port:e,path:"/api/sentinel/release",method:"POST",headers:{"content-type":"application/json","content-length":Buffer.byteLength(r),"x-sentinel-token":o}},d=>{let c="";d.on("data",i=>c+=i),d.on("end",()=>{try{t({status:d.statusCode??0,body:JSON.parse(c)})}catch{t({status:d.statusCode??0,body:c})}})});s.on("error",a),s.setTimeout(3e3,()=>s.destroy(new Error("timeout"))),s.write(r),s.end()})}function se(e){const{id:o,selfDerived:n,previousMode:t,mode:a}=e;return n&&t==="normal"?`Agent ${o} was already normal \u2014 no change. If you are still locked out, your workspace id may differ (e.g. a symlinked root). Run: sentinel release --agent=<id>`:`[live] Agent ${o} RELEASED \u2014 was ${t}, now ${a}. Applied to the running daemon (no restart).`}async function st(e){const o=u(m(),".dahlia","agents"),n=e===void 0,t=e??$e(process.cwd());n&&console.log(`Resolved agent-id from cwd: ${t}`);const a=we(m());if(a&&await ot(a.port))try{const i=await at(a.port,a.token,t);if(i.status===200&&typeof i.body=="object"&&i.body.ok){console.log(se({id:t,selfDerived:n,previousMode:String(i.body.previousMode),mode:String(i.body.mode)}));return}const l=typeof i.body=="object"?JSON.stringify(i.body):i.body;console.error(`[live] daemon refused release (HTTP ${i.status}): ${l}`);return}catch(i){console.error(`[live] daemon release call failed: ${i.message}. Falling back to mode.json.`)}const r=await T(o,t);if(r.mode==="normal"){console.log(`Agent ${t} is already in normal mode.`);return}const s=r.mode;await C(u(o,t,"mode.json"),JSON.stringify({mode:"normal",reason:"manual release",timestamp:new Date().toISOString(),previousMode:s}),"utf-8");const d=await M(u(o,t)),c=new A(t,{logDir:u(o,t)});await c.open(),c.setSigningKey(d.privateKey,d.publicKey),await c.logModeChange("normal","manual release",s),await c.close(),console.log(`[fallback] Agent ${t} RELEASED (was ${s}) \u2014 no running daemon; mode.json written, applies on the daemon's next start.`)}async function it(){const e=await S.listAgents();if(e.length===0){console.log("No monitored agents found.");return}const o=u(m(),".dahlia","agents"),n=[];for(const a of e){const r=await T(o,a);n.push({agentId:a,mode:r.mode,reason:r.reason??"-",changed:r.timestamp?r.timestamp.split("T")[0]:"-"})}console.log(`Agent Mode Status
|
|
23
|
+
`),console.log(te(n));const t=n.filter(a=>a.mode!=="normal");if(t.length>0){console.log("");for(const a of t){const r=a.mode==="quarantined"?"QUARANTINED":"RESTRICTED";console.log(`\u26A0 ${a.agentId}: ${r}`)}}}async function rt(){const e=await S.listAgents();if(e.length===0){console.log("No monitored agents found. Create one with --create --agent=ID --name=NAME");return}const o=[];for(const t of e)try{const s=(await S.loadAgentProfile(t)).build().filter(I=>I.source==="agent-monitor"),d=s.length,c=s.length>0?s.map(I=>I.lastActive).sort().reverse()[0].split("T")[0]:"never",l=await new N(t).loadBaseline(t),g=await S.loadRole(t),h=[];if(l){const I=O(s,7),k=new B(l,g);for(const $ of I){const D={label:$.label,category:$.category,lastActive:$.lastActive,description:$.description,weight:$.weight,source:$.source,files:$.files,connections:$.connections};h.push(...k.analyzeSession(D))}}const y=u(m(),".dahlia","agents"),E=await T(y,t);o.push({agentId:t,sessions:d,lastActive:c,roleDefined:g!==null,baselineDefined:l!==null,status:P(d,h),mode:E.mode})}catch(a){console.warn(`Error loading agent ${t}:`,a),o.push({agentId:t,sessions:0,lastActive:"error",roleDefined:!1,baselineDefined:!1,status:"Error",mode:"normal"})}const n=o.filter(t=>t.mode&&t.mode!=="normal");if(n.length>0){for(const t of n){const a=t.mode==="quarantined"?"QUARANTINED":"RESTRICTED";console.log(`\u26A0 ${t.agentId}: ${a}`)}console.log("")}console.log(`Sentinel Agent Overview
|
|
24
|
+
`),console.log(ne(o))}async function lt(e){const o=await S.loadAgentProfile(e),n=await S.loadRole(e),a=await new N(e).loadBaseline(e),s=o.build().filter(D=>D.source==="agent-monitor"),d=O(s,7),c=d.map(oe),i=[];if(a){const D=new B(a,n);for(const v of d){const ce={label:v.label,category:v.category,lastActive:v.lastActive,description:v.description,weight:v.weight,source:v.source,files:v.files,connections:v.connections};i.push(...D.analyzeSession(ce))}}const l=u(m(),".dahlia","profile.json"),g=new ue(l),h=new fe({backend:g});let y=[];await h.load()&&(y=h.build().filter(v=>v.core===!0).map(oe));const I=be({agentId:e,agentName:e,role:n,baseline:a,e:c,findings:i,t:y}),k=new Date().toISOString().split("T")[0],$=`Sentinel Agent Report \u2014 ${e} \u2014 ${d.length} recent sessions \u2014 generated ${k}
|
|
25
25
|
|
|
26
|
-
`;process.stdout.write($+I)}async function ct(e){const o=u(m(),".dahlia","agents"),n=new A(e,{logDir:u(o,e)});await n.open();const t=new N(e),a=await t.computeBaseline(n);await n.close(),await t.saveBaseline(a),console.log(`Baseline computed for agent: ${e}`),console.log(` Sessions: ${a.totalSessions}`),console.log(` Period: ${a.periodDays} days`),console.log(" Action distribution:");for(const[r,
|
|
27
|
-
`),console.log("Template config created at ~/.dahlia/sentinel.json \u2014 edit it with your agent details then run: npm run sentinel -- --monitor")}async function q(){const e=u(m(),".dahlia","sentinel.json");try{const o=await
|
|
28
|
-
\u26A0 [${h.agentId}] ${h.severity}: ${h.description}`)}),
|
|
29
|
-
`+ae(t)),process.exit(0)},
|
|
30
|
-
`);for(const
|
|
26
|
+
`;process.stdout.write($+I)}async function ct(e){const o=u(m(),".dahlia","agents"),n=new A(e,{logDir:u(o,e)});await n.open();const t=new N(e),a=await t.computeBaseline(n);await n.close(),await t.saveBaseline(a),console.log(`Baseline computed for agent: ${e}`),console.log(` Sessions: ${a.totalSessions}`),console.log(` Period: ${a.periodDays} days`),console.log(" Action distribution:");for(const[r,s]of Object.entries(a.actionDistribution))console.log(` ${r}: ${s}%`);console.log(` Saved to: ~/.dahlia/agents/${e}/baseline.json`)}async function dt(e,o,n){let t;if(n){let a;try{a=await j(n,"utf-8")}catch(r){console.error(`Failed to read role file "${n}":`,r);return}try{t=JSON.parse(a)}catch(r){console.error(`Invalid JSON in role file "${n}":`,r.message);return}}await S.createAgent(e,o,t),console.log(`Agent created: ${e} (${o})`),t&&console.log(` Role loaded from: ${n}`)}async function gt(){const e=u(m(),".dahlia","sentinel.json");try{await K(e),console.log(`Config already exists at ${e}`);return}catch{}await C(e,JSON.stringify({agents:[{agentId:"example-agent",name:"Example Agent",adapterType:"log",logPath:"/path/to/agent/activity.log",logFormat:"json-lines",roleDefinitionPath:"~/.dahlia/agents/example-agent/role.json"}],baselineWindowDays:30},null,2)+`
|
|
27
|
+
`),console.log("Template config created at ~/.dahlia/sentinel.json \u2014 edit it with your agent details then run: npm run sentinel -- --monitor")}async function q(){const e=u(m(),".dahlia","sentinel.json");try{const o=await j(e,"utf-8");return JSON.parse(o)}catch(o){if(o.code==="ENOENT")return null;if(o instanceof SyntaxError)return console.error(`Invalid JSON in sentinel config: ${o.message}`),null;throw o}}async function ut(e){const o=await q();if(!o){console.log("No sentinel config found. Create ~/.dahlia/sentinel.json or use --init-config to add agents. See docs for config format.");return}let n=o.agents;if(e&&(n=n.filter(l=>l.agentId===e),n.length===0)){console.log(`Agent "${e}" not found in sentinel.json`);return}const t=new Map,a=new Map,r=l=>l&&l.startsWith("~/")?m()+l.slice(1):l;let s;if(o.alerts){const l={...o.alerts.minSeverity&&{minSeverity:o.alerts.minSeverity},...o.alerts.dedupeWindowMs!==void 0&&{dedupeWindowMs:o.alerts.dedupeWindowMs},...o.alerts.quietHoursEnabled!==void 0&&{quietHoursEnabled:o.alerts.quietHoursEnabled},...o.alerts.quietHours&&{quietHours:o.alerts.quietHours},...o.alerts.channels&&{channels:o.alerts.channels}};s=new de(l)}for(const l of n)try{const g=new me(l.agentId,void 0,{type:l.adapterType,logPath:r(l.logPath),logFormat:l.logFormat,fieldMapping:l.fieldMapping,mcpLogDir:r(l.mcpLogDir),webhookPort:l.webhookPort,webhookApiKey:l.webhookApiKey,readExisting:l.readExisting});g.setAuditLogDir(u(m(),".dahlia","agents",l.agentId)),g.setFindingCallback(h=>{(h.severity==="HIGH"||h.severity==="CRITICAL")&&console.log(`
|
|
28
|
+
\u26A0 [${h.agentId}] ${h.severity}: ${h.description}`)}),s&&g.setAlertManager(s),await g.start(),t.set(l.agentId,g),a.set(l.agentId,l.adapterType)}catch(g){console.error(`Failed to start monitoring for agent ${l.agentId}:`,g)}console.log(L(t,a));const d=setInterval(()=>{console.clear(),console.log(L(t,a))},5e3),c=async()=>{clearInterval(d);for(const[l,g]of t)try{await g.stop()}catch(h){console.warn(`Error stopping runner ${l}:`,h)}console.log(`
|
|
29
|
+
`+ae(t)),process.exit(0)},i=()=>{c().catch(l=>{console.error("Error during shutdown:",l),process.exit(1)})};process.on("SIGINT",i),process.on("SIGTERM",i)}async function ft(e,o,n){const a=await new pe(e).generateReport({periodDays:o,format:n});process.stdout.write(a)}async function pt(e,o){const n=await he({periodDays:e,format:o});process.stdout.write(n)}async function mt(e=30){const o=await q();if(!o){console.log("No sentinel config found. Create ~/.dahlia/sentinel.json or use --init-config.");return}const n=u(m(),".dahlia","agents"),t=new Map;try{for(const s of o.agents){const d=new A(s.agentId,{logDir:u(n,s.agentId)});await d.open(),t.set(s.agentId,d)}const r=await new ge().detect(t,e*24*60*60*1e3);if(r.length===0){console.log("No cross-agent correlations detected.");return}console.log(`Found ${r.length} cross-agent correlation(s):
|
|
30
|
+
`);for(const s of r)console.log(`[${s.severity}] ${s.rule}`),console.log(` Agent A: ${s.agentA.agentId} \u2014 ${s.agentA.action} on ${s.agentA.target}`),console.log(` Agent B: ${s.agentB.agentId} \u2014 ${s.agentB.action} on ${s.agentB.target}`),console.log(` Time delta: ${Math.round(s.timeDeltaMs/1e3)}s`),console.log(` Recommendation: ${s.recommendation}`),console.log("")}finally{for(const a of t.values())await a.close()}}async function ht(e){const o=u(m(),".dahlia","agents"),n=u(o,e,"cumulative-stats.json");let t="(none)";try{t=JSON.parse(await j(n,"utf-8")).totalEntries}catch{}const r=await new A(e,{logDir:u(o,e)}).recomputeCumulativeStats();console.log(`Recomputed cumulative-stats for ${e}:`),console.log(` totalEntries: ${t} \u2192 ${r.totalEntries}`),console.log(` scope: ${r.countScope}`),console.log(" (Read-only over the signed trail; only cumulative-stats.json rewritten.)")}async function yt(e){const o=u(m(),".dahlia","agents"),n=u(o,e),t=await M(n),a=new A(e,{logDir:n});a.setSigningKey(t.privateKey,t.publicKey);const r=F&&x?[{file:F,reason:x}]:void 0,s=await a.enrollManifest(r?{quarantine:r}:void 0);console.log(`Manifest enrolled for ${e}: ${s.records} file record(s) signed + chained`+(s.quarantined?`, ${s.quarantined} quarantine record(s)`:"")+"."),r&&console.log(` Quarantined: ${F} \u2014 ${x}`),console.log(" (Read-only over audit entries; protects from enrollment forward.)")}async function wt(e){const o=u(m(),".dahlia","agents"),n=new A(e,{logDir:u(o,e)});await n.open();const t=await n.verify();if(t.valid?console.log(`Audit trail for ${e}: VALID (${t.totalEntries} entries verified)`):t.brokenAt!==void 0?console.log(`Audit trail for ${e}: BROKEN at entry ${t.brokenAt} (${t.totalEntries} entries checked)`):console.log(`Audit trail for ${e}: INVALID \u2014 file-level manifest integrity failure (${t.totalEntries} entries chain-verified)`),t.manifest){const r=t.manifest;console.log(` Manifest: ${r.recordCount} file record(s) \u2014 ${r.ok?"OK":`${r.issues.length} issue(s)`}`);for(const s of r.issues)console.log(` [${s.type}] ${s.detail}`);for(const s of r.quarantined)console.log(` [QUARANTINED] ${s.file} \u2014 ${s.reason} (retained on disk, excluded from verdict)`)}const a=await n.query({type:"finding",limit:1e4});if(a.length>0){let r=0,s=0,d=0;for(const i of a){const l=i.decision;l==="deny"?s++:l==="modify"?d++:r++}console.log(` Findings: ${a.length} total \u2014 ${r} allowed, ${s} denied, ${d} modified`);const c=a.slice(0,5);for(const i of c){const l=i,g=l.decision,h=l.modification,y=l.description,E=l._decisionDefaulted===!0;g==="modify"&&h?.type==="append_args"?console.log(` [modified] ${y} \u2014 appended args: [${h.args.join(", ")}]`):console.log(g==="deny"?` [denied] ${y}`:E?` [allowed (legacy)] ${y}`:` [allowed] ${y}`)}}await n.close()}async function $t(){const e=u(m(),".dahlia","agents"),o=new R({agentsDir:e}),t=await new H(e).listAgents();if(t.length===0){console.log("No monitored agents found."),await o.stop();return}const a=[];for(const r of t){const s=await o.getHealthScore(r);a.push({agentId:r,score:s.score,status:s.status,mode:s.mode,findings:`${s.findings.critical}/${s.findings.high}/${s.findings.medium}/${s.findings.low}`,lastEvent:s.lastEvent?s.lastEvent.split("T")[0]:"-",baseline:s.baselineEstablished})}await o.stop(),console.log(`Agent Health Dashboard
|
|
31
31
|
`),console.log(ee(a))}var ie=`# Sentinel \u2014 Agent Security Policy
|
|
32
32
|
version: "1.0"
|
|
33
33
|
|
|
@@ -72,9 +72,9 @@ policy:
|
|
|
72
72
|
# repoRoot: . # scan this directory for sensitive files
|
|
73
73
|
# mapPath: ~/.dahlia/repo-sensitivity.json
|
|
74
74
|
# overlayPath: ~/.dahlia/repo-sensitivity.review.json
|
|
75
|
-
`;async function re(){const e=u(process.cwd(),".sentinel.yaml");try{await K(e),console.log(`.sentinel.yaml already exists in ${process.cwd()}`);return}catch{}await
|
|
76
|
-
`),console.log(` Task ID: ${t.taskId}`),console.log(` Description: ${t.description}`),console.log(` Keywords: ${t.keywords.join(", ")}`),console.log(` Status: ${t.status}`),console.log(` Active for: ${(a/6e4).toFixed(1)} minutes`),console.log(` TTL remain: ${r>0?(r/6e4).toFixed(1)+" minutes":"EXPIRED"}`),t.relaxedActions?.length&&console.log(` Relaxed: ${t.relaxedActions.join(", ")}`),t.phases?.length&&console.log(` Phases: ${t.phases.join(", ")}`),t.acceptableActions.length>0&&console.log(` Acceptable: ${t.acceptableActions.map(
|
|
77
|
-
`),console.log(` Intent starts: ${t.intentStartCount}`),console.log(` Intent ends: ${t.intentEndCount}`),console.log(` Intent checks: ${t.intentCheckCount}`),console.log(` Drift findings: ${t.intentDriftCount}`),t.averageAlignmentScore!==null&&console.log(` Avg alignment: ${t.averageAlignmentScore.toFixed(2)}`);const d=a.filter(c=>{const
|
|
78
|
-
Last ${d.length} misaligned action(s):`);for(const c of d){const
|
|
79
|
-
Recent drift findings: ${
|
|
80
|
-
Sentinel + Claude Code integration ready.`)}catch(n){console.error(`Init failed: ${n.message}`),process.exit(1)}}if(Le){const e=w(f,"--port");await Dt(qe,e?parseInt(e,10):void 0)}else if(Ye&&p&&Q&&V)await vt(p,Q,V);else if(Xe&&p)await At(p);else if(Ze&&p)await It(p);else if(et&&p)await St(p);else if(Oe)await re();else if(_)await le(_);else if(Ge&&p)await yt(p);else if(Je&&p)await ht(p);else if(We&&p)await wt(p);else if(Ue)await $t();else if(_e&&p)await tt(p,X??"No reason provided");else if(Qe&&p)await nt(p,X??"No reason provided");else if(Ve)await st(p);else if(ze)await it();else if(Pe)await gt();else if(xe)await ut(p);else if(Ne&&p&&U)await dt(p,U,ke);else if(Fe&&p)await ct(p);else if(Be){const e=
|
|
75
|
+
`;async function re(){const e=u(process.cwd(),".sentinel.yaml");try{await K(e),console.log(`.sentinel.yaml already exists in ${process.cwd()}`);return}catch{}await C(e,ie,"utf-8"),console.log("Created .sentinel.yaml \u2014 edit then run: npm run sentinel -- --from-policy .sentinel.yaml")}async function le(e){const o=await ve(e),n=await R.fromPolicy(e);console.log(`Loaded policy for agent ${o.agent.id}. Monitoring active.`);const t=async()=>{await n.stop(),console.log("Monitoring stopped."),process.exit(0)},a=()=>{t().catch(r=>{console.error("Error during shutdown:",r),process.exit(1)})};process.on("SIGINT",a),process.on("SIGTERM",a)}async function vt(e,o,n){const t=u(m(),".dahlia","agents"),a=new R({agentsDir:t});await a.addAgent(e,e);const r=z?z.split(","):void 0,s=Y?Y.split(",").map(c=>c.trim()):void 0,d=a.startTask(e,o,n,{relaxedActions:r,phases:s});if(!d){console.log(`Failed to start task for agent ${e}.`),await a.stop();return}console.log(`Task started for agent ${e}:`),console.log(` Task ID: ${d.taskId}`),console.log(` Description: ${d.description}`),console.log(` Keywords: ${d.keywords.join(", ")}`),d.relaxedActions?.length&&console.log(` Relaxed: ${d.relaxedActions.join(", ")}`),d.phases?.length&&console.log(` Phases: ${d.phases.join(", ")}`),console.log(` TTL: ${(d.ttlMs/6e4).toFixed(0)} minutes`),await a.stop()}async function At(e){const o=u(m(),".dahlia","agents"),n=new R({agentsDir:o});await n.addAgent(e,e);const t=n.endTask(e);t?(console.log(`Task ended for agent ${e}:`),console.log(` Task ID: ${t.taskId}`),console.log(` Description: ${t.description}`),console.log(` Status: ${t.status}`)):console.log(`No active task found for agent ${e}.`),await n.stop()}async function It(e){const o=u(m(),".dahlia","agents"),n=new R({agentsDir:o});await n.addAgent(e,e);const t=n.getActiveTask(e);if(!t){console.log(`No active task for agent ${e}.`),await n.stop();return}const a=Date.now()-new Date(t.startedAt).getTime(),r=t.ttlMs-a;console.log(`Active task for agent ${e}:
|
|
76
|
+
`),console.log(` Task ID: ${t.taskId}`),console.log(` Description: ${t.description}`),console.log(` Keywords: ${t.keywords.join(", ")}`),console.log(` Status: ${t.status}`),console.log(` Active for: ${(a/6e4).toFixed(1)} minutes`),console.log(` TTL remain: ${r>0?(r/6e4).toFixed(1)+" minutes":"EXPIRED"}`),t.relaxedActions?.length&&console.log(` Relaxed: ${t.relaxedActions.join(", ")}`),t.phases?.length&&console.log(` Phases: ${t.phases.join(", ")}`),t.acceptableActions.length>0&&console.log(` Acceptable: ${t.acceptableActions.map(s=>`${s.action}:${s.targetPattern}`).join(", ")}`),await n.stop()}async function St(e){const o=u(m(),".dahlia","agents"),n=new A(e,{logDir:u(o,e)});await n.open();const t=await n.getStats(),a=await n.query({type:"intent_check"}),s=(await n.query({type:"finding"})).filter(c=>typeof c.data=="object"&&c.data!==null&&"type"in c.data&&c.data.type==="intent_drift");console.log(`Intent Alignment Report \u2014 ${e}
|
|
77
|
+
`),console.log(` Intent starts: ${t.intentStartCount}`),console.log(` Intent ends: ${t.intentEndCount}`),console.log(` Intent checks: ${t.intentCheckCount}`),console.log(` Drift findings: ${t.intentDriftCount}`),t.averageAlignmentScore!==null&&console.log(` Avg alignment: ${t.averageAlignmentScore.toFixed(2)}`);const d=a.filter(c=>{const i=c.data;return i&&typeof i=="object"&&"aligned"in i&&!i.aligned}).slice(-5);if(d.length>0){console.log(`
|
|
78
|
+
Last ${d.length} misaligned action(s):`);for(const c of d){const i=c.data,l=typeof i.score=="number"?i.score.toFixed(2):"?",g=i.event,h=g?.action??"?",y=g?.primaryTarget??g?.targets?.[0]??g?.target??"?";console.log(` score: ${l} ${h} \u2192 ${y} (${c.timestamp.split("T")[0]})`)}}if(s.length>0){console.log(`
|
|
79
|
+
Recent drift findings: ${s.length}`);for(const c of s.slice(-3)){const i=c.data,l=i.severity??"?",g=i.description??"";console.log(` [${l}] ${typeof g=="string"?g.slice(0,80):g}`)}}await n.close()}async function Dt(e,o){try{const n=await ye({force:e,port:o});if(n.created.length>0){console.log("Created:");for(const t of n.created)console.log(` ${t}`)}if(n.skipped.length>0){console.log("Skipped (already exists \u2014 use --force to overwrite):");for(const t of n.skipped)console.log(` ${t}`)}if(n.merged.length>0){console.log("Merged:");for(const t of n.merged)console.log(` ${t}`)}if(n.errors.length>0){console.log("Errors:");for(const t of n.errors)console.log(` ${t}`)}n.errors.length===0&&console.log(`
|
|
80
|
+
Sentinel + Claude Code integration ready.`)}catch(n){console.error(`Init failed: ${n.message}`),process.exit(1)}}if(Le){const e=w(f,"--port");await Dt(qe,e?parseInt(e,10):void 0)}else if(Ye&&p&&Q&&V)await vt(p,Q,V);else if(Xe&&p)await At(p);else if(Ze&&p)await It(p);else if(et&&p)await St(p);else if(Oe)await re();else if(_)await le(_);else if(Ge&&p)await yt(p);else if(Je&&p)await ht(p);else if(We&&p)await wt(p);else if(Ue)await $t();else if(_e&&p)await tt(p,X??"No reason provided");else if(Qe&&p)await nt(p,X??"No reason provided");else if(Ve)await st(p);else if(ze)await it();else if(Pe)await gt();else if(xe)await ut(p);else if(Ne&&p&&U)await dt(p,U,ke);else if(Fe&&p)await ct(p);else if(Be){const e=b?parseInt(b,10):30;await pt(e,Z==="json"?"json":"markdown")}else if(He&&p){const e=b?parseInt(b,10):30;await ft(p,e,Z==="json"?"json":"markdown")}else if(Ke){const e=b?parseInt(b,10):30;await mt(e)}else p?await lt(p):await rt();
|
package/dist/gateway/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v as Sentinel, e as AgentRole, S as SecurityFinding } from '../Sentinel-
|
|
1
|
+
import { v as Sentinel, e as AgentRole, S as SecurityFinding } from '../Sentinel-BVoMEF3F.js';
|
|
2
2
|
import 'node:crypto';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -75,6 +75,13 @@ interface SentinelGatewayOptions {
|
|
|
75
75
|
* Operator-only, same channel as unknownTools.
|
|
76
76
|
*/
|
|
77
77
|
allowUnknownTools?: string[];
|
|
78
|
+
/**
|
|
79
|
+
* Build identity reported via /health (daemon-staleness fix). Content hash of
|
|
80
|
+
* the daemon entry file this process was launched from; session-start compares
|
|
81
|
+
* it to the hash of the current on-disk entry and relaunches on mismatch.
|
|
82
|
+
* Absent → /health reports "unknown" → next session-start relaunches (safe side).
|
|
83
|
+
*/
|
|
84
|
+
buildId?: string;
|
|
78
85
|
}
|
|
79
86
|
declare class SentinelGateway {
|
|
80
87
|
private readonly configuredPort;
|
|
@@ -90,6 +97,9 @@ declare class SentinelGateway {
|
|
|
90
97
|
private readonly releaseToken;
|
|
91
98
|
/** Item D (F-8): disposition for unknown (non-MCP, unrecognized) tool names. */
|
|
92
99
|
private readonly unknownTools;
|
|
100
|
+
/** Daemon-staleness build identity (content hash of the launched-from entry),
|
|
101
|
+
* reported via /health. "unknown" when not supplied by the launcher. */
|
|
102
|
+
private readonly buildId;
|
|
93
103
|
private server;
|
|
94
104
|
private running;
|
|
95
105
|
private signalHandlersInstalled;
|
package/dist/gateway/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SentinelGateway
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-G74MMDKA.js";
|
|
4
4
|
import "../chunk-B5QKJHSV.js";
|
|
5
5
|
import "../chunk-FMZWHT4M.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-JTR2E7RD.js";
|
|
7
7
|
import "../chunk-WLIDSTS4.js";
|
|
8
8
|
export {
|
|
9
9
|
SentinelGateway
|
package/dist/gatewayDaemon.js
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runGatewayDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-G74MMDKA.js";
|
|
5
|
+
import {
|
|
6
|
+
computeBuildId,
|
|
7
|
+
runSessionStart
|
|
8
|
+
} from "./chunk-2TJ5Z53T.js";
|
|
9
|
+
import "./chunk-LATQNIRW.js";
|
|
5
10
|
import "./chunk-B5QKJHSV.js";
|
|
6
11
|
import "./chunk-FMZWHT4M.js";
|
|
7
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-JTR2E7RD.js";
|
|
8
13
|
import "./chunk-WLIDSTS4.js";
|
|
9
14
|
|
|
10
15
|
// src/gatewayDaemon.ts
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
11
17
|
var args = process.argv.slice(2);
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
for (let i = 0; i < args.length; i++) {
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
var selfPath = fileURLToPath(import.meta.url);
|
|
19
|
+
function getFlag(name) {
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
if (args[i] === name && args[i + 1]) return args[i + 1];
|
|
22
|
+
}
|
|
23
|
+
return void 0;
|
|
17
24
|
}
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
if (args.includes("--session-start")) {
|
|
26
|
+
const portArg = getFlag("--port");
|
|
27
|
+
runSessionStart({
|
|
28
|
+
cwd: getFlag("--cwd"),
|
|
29
|
+
port: portArg ? parseInt(portArg, 10) : void 0,
|
|
30
|
+
gatewayEntry: selfPath
|
|
31
|
+
}).catch((err) => {
|
|
32
|
+
console.error("[SENTINEL GATEWAY] session-start error:", err);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
const policyPath = getFlag("--policy");
|
|
37
|
+
const portArg = getFlag("--port");
|
|
38
|
+
const port = portArg ? parseInt(portArg, 10) : void 0;
|
|
39
|
+
if (!policyPath) {
|
|
40
|
+
console.error("[SENTINEL GATEWAY] --policy <path> is required");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
runGatewayDaemon({ policyPath, port, buildId: computeBuildId(selfPath) }).catch((err) => {
|
|
44
|
+
console.error("[SENTINEL GATEWAY] Fatal:", err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
21
47
|
}
|
|
22
|
-
runGatewayDaemon({ policyPath, port }).catch((err) => {
|
|
23
|
-
console.error("[SENTINEL GATEWAY] Fatal:", err);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
});
|
|
26
48
|
//# sourceMappingURL=gatewayDaemon.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AgentActivityEvent, S as SecurityFinding } from './Sentinel-
|
|
2
|
-
export { a as AcceptableAction, b as AdapterConfig, c as AgentBaseline, d as AgentMode, e as AgentRole, f as AlertChannel, g as AlertConfig, h as AllowResponse, i as AuditEntry, j as AuditQueryOptions, B as BlockResponse, C as CorrelationFinding, E as ExceptionApprovalContext, k as ExceptionApprovalFn, G as GuideResponse, H as HookCheckpoint, l as HookContext, m as HookHandler, n as HookRegistration, o as HookResponse, I as IntentAlignmentConfig, p as IntentAlignmentResult, M as ModifiableEventFields, q as MonitorOptions, O as OverlayDecisionType, R as RepoSensitivityMap, r as ReportOptions, s as RoleException, t as SecuritySeverity, u as SensitivityOverlay, v as Sentinel, w as SentinelConfig, T as TaskIntent } from './Sentinel-
|
|
1
|
+
import { A as AgentActivityEvent, S as SecurityFinding } from './Sentinel-BVoMEF3F.js';
|
|
2
|
+
export { a as AcceptableAction, b as AdapterConfig, c as AgentBaseline, d as AgentMode, e as AgentRole, f as AlertChannel, g as AlertConfig, h as AllowResponse, i as AuditEntry, j as AuditQueryOptions, B as BlockResponse, C as CorrelationFinding, E as ExceptionApprovalContext, k as ExceptionApprovalFn, G as GuideResponse, H as HookCheckpoint, l as HookContext, m as HookHandler, n as HookRegistration, o as HookResponse, I as IntentAlignmentConfig, p as IntentAlignmentResult, M as ModifiableEventFields, q as MonitorOptions, O as OverlayDecisionType, R as RepoSensitivityMap, r as ReportOptions, s as RoleException, t as SecuritySeverity, u as SensitivityOverlay, v as Sentinel, w as SentinelConfig, T as TaskIntent } from './Sentinel-BVoMEF3F.js';
|
|
3
3
|
import 'node:crypto';
|
|
4
4
|
|
|
5
5
|
interface SentinelPolicy {
|
|
@@ -128,23 +128,45 @@ declare function runInitClaudeCode(options: {
|
|
|
128
128
|
declare function discoverPolicy(startDir: string, home: string): string | null;
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* `session-start` entry point — called by the hook script on SessionStart
|
|
131
|
+
* `session-start` entry point — called by the hook script on SessionStart, and
|
|
132
|
+
* the single source of truth for the gateway lifecycle decision (anti-drift: the
|
|
133
|
+
* cc hook delegates here via the dual-mode daemon entry rather than carrying its
|
|
134
|
+
* own copy of this logic).
|
|
132
135
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
136
|
+
* Flow:
|
|
137
|
+
* 1. Discover .sentinel.yaml (walk up from cwd to $HOME); none → no gateway.
|
|
138
|
+
* 2. Resolve the daemon entry AT LAUNCH (not a baked path) and hash it — the
|
|
139
|
+
* current build identity.
|
|
140
|
+
* 3. Liveness: is a daemon already running (PID file)?
|
|
141
|
+
* - none → cold spawn (today's path).
|
|
142
|
+
* - running → GET /health, compare its build id to the current entry hash:
|
|
143
|
+
* match → reuse (unchanged fast path).
|
|
144
|
+
* mismatch → RELAUNCH: SIGTERM (→ SIGKILL fallback) the stale daemon,
|
|
145
|
+
* spawn the current build, wait ready. This is pure process
|
|
146
|
+
* replacement: it writes NO mode_change / release / trail
|
|
147
|
+
* state — the new daemon rebuilds mode (mode.json) and count
|
|
148
|
+
* (the append-only trail) lazily via _initWorkspaceAgent, so
|
|
149
|
+
* a restricted/quarantined agent stays restricted/quarantined
|
|
150
|
+
* with the same escalation count across the relaunch.
|
|
151
|
+
* Concurrency: a relaunch holds an atomic lock so two simultaneous session-
|
|
152
|
+
* starts converge on ONE daemon (the loser waits for ready and reuses) instead
|
|
153
|
+
* of racing two daemons onto the port.
|
|
138
154
|
*/
|
|
139
155
|
interface SessionStartResult {
|
|
140
|
-
action: "reused" | "spawned" | "no-policy";
|
|
156
|
+
action: "reused" | "spawned" | "relaunched" | "no-policy";
|
|
141
157
|
pid?: number;
|
|
142
158
|
policyPath?: string;
|
|
159
|
+
/** Why a relaunch happened (build mismatch) or null — for telemetry/tests. */
|
|
160
|
+
relaunchReason?: string | null;
|
|
143
161
|
}
|
|
144
162
|
declare function runSessionStart(options?: {
|
|
145
163
|
cwd?: string;
|
|
146
164
|
home?: string;
|
|
147
165
|
port?: number;
|
|
166
|
+
/** Daemon entry to launch. Defaults to resolveGatewayEntryPoint() (resolved at
|
|
167
|
+
* launch, NOT a baked path — closes the pnpm-relocation / dangling cases). The
|
|
168
|
+
* dual-mode entry passes its own path so the launcher spawns the current build. */
|
|
169
|
+
gatewayEntry?: string;
|
|
148
170
|
}): Promise<SessionStartResult>;
|
|
149
171
|
|
|
150
172
|
export { AgentActivityEvent, type CliApprovalOptions, type InitReport, SecurityFinding, type SentinelPolicy, type SessionStartResult, createCliApproval, discoverPolicy, loadPolicy, loadPolicyFromString, runInitClaudeCode, runSessionStart };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
runInitClaudeCode,
|
|
3
|
-
runSessionStart
|
|
4
|
-
} from "./chunk-FWIISAZZ.js";
|
|
1
|
+
import "./chunk-TKAKHSZ3.js";
|
|
5
2
|
import {
|
|
6
3
|
Sentinel,
|
|
7
4
|
createCliApproval
|
|
8
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SSDIBY52.js";
|
|
6
|
+
import {
|
|
7
|
+
runInitClaudeCode,
|
|
8
|
+
runSessionStart
|
|
9
|
+
} from "./chunk-2TJ5Z53T.js";
|
|
9
10
|
import "./chunk-LATQNIRW.js";
|
|
10
11
|
import {
|
|
11
12
|
discoverPolicy
|
|
12
13
|
} from "./chunk-FMZWHT4M.js";
|
|
13
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-JTR2E7RD.js";
|
|
14
15
|
import {
|
|
15
16
|
loadPolicy,
|
|
16
17
|
loadPolicyFromString
|
package/package.json
CHANGED