@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 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).
@@ -0,0 +1,10 @@
1
+ import {
2
+ Sentinel
3
+ } from "./chunk-SSDIBY52.js";
4
+ import "./chunk-JTR2E7RD.js";
5
+ import "./chunk-WLIDSTS4.js";
6
+ import "./chunk-NUXSUSYY.js";
7
+ export {
8
+ Sentinel
9
+ };
10
+ //# sourceMappingURL=Sentinel-5CQ6HKXS.js.map
@@ -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-QIYQWOLO.js";
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
- // P5 cold-start readiness poll (kept byte-equivalent to
218
- // setup/gatewayReadiness.ts \u2014 a generated string can't import it). Bounded WAIT
219
- // on /api/sentinel/health so cc's first PreToolUse hits a LISTENING gateway
220
- // instead of racing the daemon's warmup into the tiered fail-closed. Health
221
- // returns 200 only once the port is bound AFTER baseline+translator wiring, so
222
- // 200 == evaluation-ready. On timeout we exit anyway; the first pre-tool-use
223
- // uses the existing fail-closed, UNCHANGED. Never fail-opens.
224
- async function waitForGatewayReady() {
225
- const deadline = Date.now() + 5000;
226
- while (Date.now() < deadline) {
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 pid = parseInt(readFileSync(PID_PATH, "utf-8").trim(), 10);
244
- process.kill(pid, 0); // throws if process doesn't exist
245
- await waitForGatewayReady(); // alive but may still be warming \u2014 wait, bounded
246
- process.exit(0); // gateway is running (and now ready, or timed out)
247
- } catch {
248
- // Stale PID \u2014 continue to launch
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
- if (!policyPath) {
264
- process.stderr.write("Sentinel: no .sentinel.yaml found. Gateway not started.\\n", () => process.exit(0));
265
- }
266
-
267
- // Launch gateway daemon (detached). Extension-aware: an installed package
268
- // substitutes dist/gatewayDaemon.js (plain node); the dev tree substitutes a
269
- // .ts (node --import tsx/esm).
270
- const gatewayArgs = GATEWAY_ENTRY_POINT.endsWith(".ts")
271
- ? ["--import", "tsx/esm", GATEWAY_ENTRY_POINT, "--policy", policyPath, "--port", String(PORT)]
272
- : [GATEWAY_ENTRY_POINT, "--policy", policyPath, "--port", String(PORT)];
273
- const child = spawn("node", gatewayArgs, { detached: true, stdio: "ignore" });
274
- child.unref();
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 waitForGatewayReady(port);
633
- return { action: "reused", pid: lock.pid, policyPath };
634
- }
635
- const gatewayEntry = resolveGatewayEntryPoint();
636
- const gatewayArgs = gatewayEntry.endsWith(".ts") ? ["--import", "tsx/esm", gatewayEntry, "--policy", policyPath, "--port", String(port)] : [gatewayEntry, "--policy", policyPath, "--port", String(port)];
637
- const child = spawn("node", gatewayArgs, { detached: true, stdio: "ignore" });
638
- child.unref();
639
- if (child.pid) {
640
- writePidFile(home, child.pid);
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: child.pid, policyPath };
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-FWIISAZZ.js.map
732
+ //# sourceMappingURL=chunk-2TJ5Z53T.js.map
@@ -15,7 +15,7 @@ import {
15
15
  scanContentForForbiddenBasenames,
16
16
  scanGlobPattern,
17
17
  tokenizePaths
18
- } from "./chunk-QIYQWOLO.js";
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
- let extractedStrings = 0;
793
- for (const key of keys) {
794
- const value = toolInput[key];
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 && extractedStrings === 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
- version: GATEWAY_VERSION,
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-XMSJE4DZ.js");
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-L4R3LPJS.js.map
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
- const safeCommandMention = event.action === "command_exec" && isPositionallySafeMention(event.primaryTarget);
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 (!safeCommandMention && primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
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 sensitivity = this.sensitivityScorer?.scoreTarget(matchedTargetValue, event.action);
1173
- const isCritical = sensitivity ? sensitivity.effectiveScore >= 0.9 : false;
1174
- const finding = this.makeFinding(event, {
1175
- severity: "HIGH",
1176
- type: "unauthorized_target",
1177
- description: `Agent accessed '${matchedTargetValue}' which matches forbidden pattern '${pattern}'`,
1178
- recommendation: getTargetRecommendation(
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-QIYQWOLO.js.map
1900
+ //# sourceMappingURL=chunk-JTR2E7RD.js.map
@@ -18,7 +18,7 @@ import {
18
18
  unionWithDefaultForbiddenPatterns,
19
19
  walkForbiddenInodeRoots,
20
20
  withPolicyReadExceptions
21
- } from "./chunk-QIYQWOLO.js";
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 (!existsSync(root)) {
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 key = f.dedupKey;
7425
- if (typeof key === "string" && key.length > 0) distinctKeys.add(key);
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-GRN5P3H2.js.map
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{runInitClaudeCode as de}from"./chunk-FWIISAZZ.js";import{AgentProfileManager as H,AlertManager as ge,AuditTrail as A,BaselineBuilder as N,CorrelationDetector as ue,DeviationDetector as B,FileStorageBackend as fe,ProfileStore as pe,ReportGenerator as me,Sentinel as T,SentinelRunner as he,generateFleetReport as ye}from"./chunk-GRN5P3H2.js";import{readReleaseToken as we}from"./chunk-LATQNIRW.js";import{deriveAgentId as $e}from"./chunk-B5QKJHSV.js";import"./chunk-FMZWHT4M.js";import"./chunk-QIYQWOLO.js";import{loadPolicy as ve}from"./chunk-WLIDSTS4.js";import{getOrCreateKeyPair as E}from"./chunk-NUXSUSYY.js";import{join as u}from"path";import{homedir as m}from"os";import{readFile as M,writeFile as R,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 i=Math.floor(r/24);if(i<14)return`${i} day${i===1?"":"s"} ago`;const d=Math.floor(i/7);if(i<60)return`${d} week${d===1?"":"s"} ago`;if(i>=365)return"over a year ago";const c=Math.floor(i/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 i=Ae(t.lastActive),d=t.weight!=null?Ie(t.weight,t.lastActive):"stable";if(n.push(` Temporal: Last active ${i} | 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(s=>o.get(s)??De(s));n.push(` Connections: ${c.join(", ")}`)}if(t.files&&t.files.length>0){const c=t.files.slice(0,10).map(s=>s.split("/").pop()??s);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(s=>` ${s}`).join(`
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"),j=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 b(e,o){try{const n=await M(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(s=>[s.agentId,String(s.score),s.status,s.mode,s.findings,s.lastEvent,s.baseline?"\u2713":"\u2717"]),t=[o,...n],a=o.map((s,l)=>Math.max(...t.map(g=>g[l].length))),r=(s,l)=>s+" ".repeat(l-s.length),i=o.map((s,l)=>r(s,a[l])).join(" "),d=a.map(s=>"-".repeat(s)).join(" "),c=n.map(s=>s.map((l,g)=>r(l,a[g])).join(" "));return[i,d,...c].join(`
18
- `)}function te(e){const o=["Agent ID","Mode","Reason","Changed"],n=e.map(s=>[s.agentId,s.mode.toUpperCase(),s.reason,s.changed]),t=[o,...n],a=o.map((s,l)=>Math.max(...t.map(g=>g[l].length))),r=(s,l)=>s+" ".repeat(l-s.length),i=o.map((s,l)=>r(s,a[l])).join(" "),d=a.map(s=>"-".repeat(s)).join(" "),c=n.map(s=>s.map((l,g)=>r(l,a[g])).join(" "));return[i,d,...c].join(`
19
- `)}function ne(e){const o=["Agent ID","Sessions","Last Active","Role","Baseline","Mode","Status"],n=e.map(s=>[s.agentId,String(s.sessions),s.lastActive,s.roleDefined?"\u2713":"\u2717",s.baselineDefined?"\u2713":"\u2717",s.mode??"normal",s.status]),t=[o,...n],a=o.map((s,l)=>Math.max(...t.map(g=>g[l].length))),r=(s,l)=>s+" ".repeat(l-s.length),i=o.map((s,l)=>r(s,a[l])).join(" "),d=a.map(s=>"-".repeat(s)).join(" "),c=n.map(s=>s.map((l,g)=>r(l,a[g])).join(" "));return[i,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(),i=a.getFindings(),d=i.filter(y=>y.severity==="HIGH"||y.severity==="CRITICAL").length,c=i.filter(y=>y.severity==="MEDIUM").length;let s=`${i.length} findings`;d>0?s=`${d} HIGH`:c>0&&(s=`${c} MEDIUM`);const l=P(a.sessionCount,i),g=t.padEnd(18),h=r.padEnd(5);n.push(`[${g}] ${h}| ${a.eventCount} events | ${a.sessionCount} sessions | ${s} | ${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 i={};for(const c of a)i[c.severity]=(i[c.severity]??0)+1;const d=Object.entries(i).map(([c,s])=>`${s} ${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 b(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 R(u(n,e,"mode.json"),JSON.stringify({mode:"restricted",reason:o,timestamp:new Date().toISOString(),previousMode:a}),"utf-8");const r=await E(u(n,e)),i=new A(e,{logDir:u(n,e)});await i.open(),i.setSigningKey(r.privateKey,r.publicKey),await i.logModeChange("restricted",o,a),await i.close(),console.log(`Agent ${e} RESTRICTED: ${o}`)}async function nt(e,o){const n=u(m(),".dahlia","agents"),a=(await b(n,e)).mode;await W(u(n,e),{recursive:!0}),await R(u(n,e,"mode.json"),JSON.stringify({mode:"quarantined",reason:o,timestamp:new Date().toISOString(),previousMode:a}),"utf-8");const r=await E(u(n,e)),i=new A(e,{logDir:u(n,e)});await i.open(),i.setSigningKey(r.privateKey,r.publicKey),await i.logModeChange("quarantined",o,a),await i.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)"}),i=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",s=>c+=s),d.on("end",()=>{try{t({status:d.statusCode??0,body:JSON.parse(c)})}catch{t({status:d.statusCode??0,body:c})}})});i.on("error",a),i.setTimeout(3e3,()=>i.destroy(new Error("timeout"))),i.write(r),i.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 s=await at(a.port,a.token,t);if(s.status===200&&typeof s.body=="object"&&s.body.ok){console.log(se({id:t,selfDerived:n,previousMode:String(s.body.previousMode),mode:String(s.body.mode)}));return}const l=typeof s.body=="object"?JSON.stringify(s.body):s.body;console.error(`[live] daemon refused release (HTTP ${s.status}): ${l}`);return}catch(s){console.error(`[live] daemon release call failed: ${s.message}. Falling back to mode.json.`)}const r=await b(o,t);if(r.mode==="normal"){console.log(`Agent ${t} is already in normal mode.`);return}const i=r.mode;await R(u(o,t,"mode.json"),JSON.stringify({mode:"normal",reason:"manual release",timestamp:new Date().toISOString(),previousMode:i}),"utf-8");const d=await E(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",i),await c.close(),console.log(`[fallback] Agent ${t} RELEASED (was ${i}) \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 b(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 i=(await S.loadAgentProfile(t)).build().filter(I=>I.source==="agent-monitor"),d=i.length,c=i.length>0?i.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(i,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"),C=await b(y,t);o.push({agentId:t,sessions:d,lastActive:c,roleDefined:g!==null,baselineDefined:l!==null,status:P(d,h),mode:C.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),i=o.build().filter(D=>D.source==="agent-monitor"),d=O(i,7),c=d.map(oe),s=[];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};s.push(...D.analyzeSession(ce))}}const l=u(m(),".dahlia","profile.json"),g=new fe(l),h=new pe({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:s,t:y}),k=new Date().toISOString().split("T")[0],$=`Sentinel Agent Report \u2014 ${e} \u2014 ${d.length} recent sessions \u2014 generated ${k}
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,i]of Object.entries(a.actionDistribution))console.log(` ${r}: ${i}%`);console.log(` Saved to: ~/.dahlia/agents/${e}/baseline.json`)}async function dt(e,o,n){let t;if(n){let a;try{a=await M(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 R(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 M(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 i;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}};i=new ge(l)}for(const l of n)try{const g=new he(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}`)}),i&&g.setAlertManager(i),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)},s=()=>{c().catch(l=>{console.error("Error during shutdown:",l),process.exit(1)})};process.on("SIGINT",s),process.on("SIGTERM",s)}async function ft(e,o,n){const a=await new me(e).generateReport({periodDays:o,format:n});process.stdout.write(a)}async function pt(e,o){const n=await ye({periodDays:e,format:o});process.stdout.write(n)}async function mt(){const e=await q();if(!e){console.log("No sentinel config found. Create ~/.dahlia/sentinel.json or use --init-config.");return}const o=u(m(),".dahlia","agents"),n=new Map;try{for(const r of e.agents){const i=new A(r.agentId,{logDir:u(o,r.agentId)});await i.open(),n.set(r.agentId,i)}const a=await new ue().detect(n);if(a.length===0){console.log("No cross-agent correlations detected.");return}console.log(`Found ${a.length} cross-agent correlation(s):
30
- `);for(const r of a)console.log(`[${r.severity}] ${r.rule}`),console.log(` Agent A: ${r.agentA.agentId} \u2014 ${r.agentA.action} on ${r.agentA.target}`),console.log(` Agent B: ${r.agentB.agentId} \u2014 ${r.agentB.action} on ${r.agentB.target}`),console.log(` Time delta: ${Math.round(r.timeDeltaMs/1e3)}s`),console.log(` Recommendation: ${r.recommendation}`),console.log("")}finally{for(const t of n.values())await t.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 M(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 E(n),a=new A(e,{logDir:n});a.setSigningKey(t.privateKey,t.publicKey);const r=F&&x?[{file:F,reason:x}]:void 0,i=await a.enrollManifest(r?{quarantine:r}:void 0);console.log(`Manifest enrolled for ${e}: ${i.records} file record(s) signed + chained`+(i.quarantined?`, ${i.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 i of r.issues)console.log(` [${i.type}] ${i.detail}`);for(const i of r.quarantined)console.log(` [QUARANTINED] ${i.file} \u2014 ${i.reason} (retained on disk, excluded from verdict)`)}const a=await n.query({type:"finding",limit:1e4});if(a.length>0){let r=0,i=0,d=0;for(const s of a){const l=s.decision;l==="deny"?i++:l==="modify"?d++:r++}console.log(` Findings: ${a.length} total \u2014 ${r} allowed, ${i} denied, ${d} modified`);const c=a.slice(0,5);for(const s of c){const l=s,g=l.decision,h=l.modification,y=l.description,C=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}`:C?` [allowed (legacy)] ${y}`:` [allowed] ${y}`)}}await n.close()}async function $t(){const e=u(m(),".dahlia","agents"),o=new T({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 i=await o.getHealthScore(r);a.push({agentId:r,score:i.score,status:i.status,mode:i.mode,findings:`${i.findings.critical}/${i.findings.high}/${i.findings.medium}/${i.findings.low}`,lastEvent:i.lastEvent?i.lastEvent.split("T")[0]:"-",baseline:i.baselineEstablished})}await o.stop(),console.log(`Agent Health Dashboard
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 R(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 T.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 T({agentsDir:t});await a.addAgent(e,e);const r=z?z.split(","):void 0,i=Y?Y.split(",").map(c=>c.trim()):void 0,d=a.startTask(e,o,n,{relaxedActions:r,phases:i});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 T({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 T({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(i=>`${i.action}:${i.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"}),i=(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 s=c.data;return s&&typeof s=="object"&&"aligned"in s&&!s.aligned}).slice(-5);if(d.length>0){console.log(`
78
- Last ${d.length} misaligned action(s):`);for(const c of d){const s=c.data,l=typeof s.score=="number"?s.score.toFixed(2):"?",g=s.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(i.length>0){console.log(`
79
- Recent drift findings: ${i.length}`);for(const c of i.slice(-3)){const s=c.data,l=s.severity??"?",g=s.description??"";console.log(` [${l}] ${typeof g=="string"?g.slice(0,80):g}`)}}await n.close()}async function Dt(e,o){try{const n=await de({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=j?parseInt(j,10):30;await pt(e,Z==="json"?"json":"markdown")}else if(He&&p){const e=j?parseInt(j,10):30;await ft(p,e,Z==="json"?"json":"markdown")}else Ke?await mt():p?await lt(p):await rt();
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();
@@ -1,4 +1,4 @@
1
- import { v as Sentinel, e as AgentRole, S as SecurityFinding } from '../Sentinel-xFCyXH45.js';
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;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SentinelGateway
3
- } from "../chunk-L4R3LPJS.js";
3
+ } from "../chunk-G74MMDKA.js";
4
4
  import "../chunk-B5QKJHSV.js";
5
5
  import "../chunk-FMZWHT4M.js";
6
- import "../chunk-QIYQWOLO.js";
6
+ import "../chunk-JTR2E7RD.js";
7
7
  import "../chunk-WLIDSTS4.js";
8
8
  export {
9
9
  SentinelGateway
@@ -1,26 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runGatewayDaemon
4
- } from "./chunk-L4R3LPJS.js";
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-QIYQWOLO.js";
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 policyPath;
13
- var port;
14
- for (let i = 0; i < args.length; i++) {
15
- if (args[i] === "--policy" && args[i + 1]) policyPath = args[++i];
16
- else if (args[i] === "--port" && args[i + 1]) port = parseInt(args[++i], 10);
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 (!policyPath) {
19
- console.error("[SENTINEL GATEWAY] --policy <path> is required");
20
- process.exit(1);
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-xFCyXH45.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-xFCyXH45.js';
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
- * 1. Discover .sentinel.yaml (walk up from cwd to $HOME)
134
- * 2. Acquire gateway lock (PID file check)
135
- * 3. If gateway already running exit early
136
- * 4. If no policy found → exit early (no gateway needed)
137
- * 5. Spawn gateway detached, write PID file, unref
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-GRN5P3H2.js";
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-QIYQWOLO.js";
14
+ import "./chunk-JTR2E7RD.js";
14
15
  import {
15
16
  loadPolicy,
16
17
  loadPolicyFromString
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuent/sentinel",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "AI agent behavioral security monitoring SDK",
5
5
  "author": "Tuent LLC",
6
6
  "keywords": [
@@ -1,10 +0,0 @@
1
- import {
2
- Sentinel
3
- } from "./chunk-GRN5P3H2.js";
4
- import "./chunk-QIYQWOLO.js";
5
- import "./chunk-WLIDSTS4.js";
6
- import "./chunk-NUXSUSYY.js";
7
- export {
8
- Sentinel
9
- };
10
- //# sourceMappingURL=Sentinel-XMSJE4DZ.js.map