@tuent/sentinel 0.1.2 → 0.1.4

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.
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  DEFAULT_MAP_PATH,
3
- DEFAULT_MEDIUM_DISPOSITION,
4
3
  DEFAULT_OVERLAY_PATH,
5
4
  RoleValidator,
6
5
  TargetSensitivityScorer,
@@ -16,14 +15,17 @@ import {
16
15
  saveMap,
17
16
  saveOverlay,
18
17
  unionWithDefaultForbiddenPatterns,
19
- walkForbiddenInodeRoots,
20
- withPolicyReadExceptions
21
- } from "./chunk-QIYQWOLO.js";
18
+ walkForbiddenInodeRoots
19
+ } from "./chunk-FIEIGBYL.js";
22
20
  import {
21
+ DEFAULT_MEDIUM_DISPOSITION,
22
+ DEFAULT_QUARANTINE_AFTER,
23
+ DEFAULT_RESTRICT_AFTER,
23
24
  loadPolicy,
24
25
  policyToConfig,
25
- policyToRole
26
- } from "./chunk-WLIDSTS4.js";
26
+ policyToRole,
27
+ withPolicyReadExceptions
28
+ } from "./chunk-KWZ7JKKO.js";
27
29
  import {
28
30
  publicKeyToBase64,
29
31
  reconstructSigningPayload,
@@ -86,6 +88,10 @@ function decayWeight(daysSinceActive, currentWeight = 1, halfLifeDays = DECAY_HA
86
88
  }
87
89
 
88
90
  // ../core/src/dataEngine.ts
91
+ function parseLastActiveMs(lastActive) {
92
+ const t = new Date(lastActive).getTime();
93
+ return Number.isNaN(t) ? 0 : t;
94
+ }
89
95
  var DataEngine = class _DataEngine {
90
96
  points = /* @__PURE__ */ new Map();
91
97
  colors;
@@ -113,7 +119,7 @@ var DataEngine = class _DataEngine {
113
119
  if (p.core) {
114
120
  weights.set(p.label, Math.max(base, CORE_WEIGHT_FLOOR));
115
121
  } else {
116
- const lastActiveMs = new Date(p.lastActive).getTime();
122
+ const lastActiveMs = parseLastActiveMs(p.lastActive);
117
123
  const daysSince = (effectiveNow - lastActiveMs) / (1e3 * 60 * 60 * 24);
118
124
  weights.set(p.label, decayWeight(daysSince, base));
119
125
  }
@@ -135,7 +141,7 @@ var DataEngine = class _DataEngine {
135
141
  }
136
142
  build() {
137
143
  const sorted = [...this.points.values()].sort(
138
- (a, b) => new Date(a.lastActive).getTime() - new Date(b.lastActive).getTime()
144
+ (a, b) => parseLastActiveMs(a.lastActive) - parseLastActiveMs(b.lastActive)
139
145
  );
140
146
  const normalizedWeights = _DataEngine.normalizeWeights(sorted);
141
147
  const now = Date.now();
@@ -146,14 +152,14 @@ var DataEngine = class _DataEngine {
146
152
  let minTime = now;
147
153
  let maxTime = 0;
148
154
  for (const p of sorted) {
149
- const t = new Date(p.lastActive).getTime();
155
+ const t = parseLastActiveMs(p.lastActive);
150
156
  if (t < minTime) minTime = t;
151
157
  if (t > maxTime) maxTime = t;
152
158
  }
153
159
  const timeRange = maxTime - minTime || 1;
154
160
  const petals = sorted.map((point, i) => {
155
161
  const layer = sorted.length > 1 ? i / (sorted.length - 1) : 0.5;
156
- const t = new Date(point.lastActive).getTime();
162
+ const t = parseLastActiveMs(point.lastActive);
157
163
  const recency = (t - minTime) / timeRange;
158
164
  const openness = 0.15 + layer * 0.8;
159
165
  const connections = (point.connections ?? []).map((label) => labelToIdMap.get(label)).filter((id) => id !== void 0);
@@ -172,7 +178,8 @@ var DataEngine = class _DataEngine {
172
178
  source: point.source,
173
179
  core: point.core,
174
180
  sharable: point.sharable,
175
- files: point.files
181
+ files: point.files,
182
+ eventCount: point.eventCount
176
183
  };
177
184
  });
178
185
  const realCount = petals.length;
@@ -288,15 +295,18 @@ function mergeAgentPoints(existing, incoming) {
288
295
  const mergedWeight = Math.min((existing.weight ?? 0) + (incoming.weight ?? 0), 1);
289
296
  const mergedConnections = incoming.connections || existing.connections ? [.../* @__PURE__ */ new Set([...incoming.connections ?? [], ...existing.connections ?? []])] : void 0;
290
297
  const mergedFiles = incoming.files || existing.files ? [.../* @__PURE__ */ new Set([...incoming.files ?? [], ...existing.files ?? []])].slice(0, 20) : void 0;
298
+ const mergedLastActive = new Date(incoming.lastActive) >= new Date(existing.lastActive) ? incoming.lastActive : existing.lastActive;
299
+ const mergedEventCount = incoming.eventCount ?? existing.eventCount;
291
300
  return preserveUserMetadata(existing, {
292
301
  label: incoming.label,
293
302
  category: incoming.category,
294
- lastActive: incoming.lastActive,
303
+ lastActive: mergedLastActive,
295
304
  description: incoming.description,
296
305
  weight: mergedWeight,
297
306
  source: "agent",
298
307
  ...mergedConnections && { connections: mergedConnections },
299
- ...mergedFiles && { files: mergedFiles }
308
+ ...mergedFiles && { files: mergedFiles },
309
+ ...mergedEventCount !== void 0 && { eventCount: mergedEventCount }
300
310
  });
301
311
  }
302
312
  var ProfileStore = class {
@@ -305,6 +315,11 @@ var ProfileStore = class {
305
315
  petalCount;
306
316
  engine;
307
317
  profile = null;
318
+ /** Snapshot (label → serialized point) of the on-disk-synced dataPoint set,
319
+ * refreshed on every load/create/write. The base for save()'s three-way merge
320
+ * of concurrent external writes — lets us tell "I deleted this" (in baseline,
321
+ * gone from memory) from "another writer added this" (on disk, not in baseline). */
322
+ baseline = /* @__PURE__ */ new Map();
308
323
  constructor(options) {
309
324
  this.backend = options.backend;
310
325
  this.eventBus = options.eventBus;
@@ -315,6 +330,9 @@ var ProfileStore = class {
315
330
  this.profile = await this.backend.read();
316
331
  this.engine = new DataEngine({ petalCount: this.petalCount });
317
332
  if (this.profile) {
333
+ if (!Array.isArray(this.profile.dataPoints)) {
334
+ this.profile.dataPoints = [];
335
+ }
318
336
  for (const point of this.profile.dataPoints) {
319
337
  this.engine.addPoint(point);
320
338
  }
@@ -333,8 +351,15 @@ var ProfileStore = class {
333
351
  payload: { profileId: this.profile.id }
334
352
  });
335
353
  }
354
+ this.captureBaseline();
336
355
  return this.profile;
337
356
  }
357
+ /** Snapshot the on-disk-synced dataPoint set for save()'s three-way merge. */
358
+ captureBaseline() {
359
+ this.baseline = new Map(
360
+ (this.profile?.dataPoints ?? []).map((p) => [p.label, JSON.stringify(p)])
361
+ );
362
+ }
338
363
  async create(name) {
339
364
  const now = (/* @__PURE__ */ new Date()).toISOString();
340
365
  this.profile = {
@@ -346,6 +371,7 @@ var ProfileStore = class {
346
371
  updatedAt: now
347
372
  };
348
373
  await this.backend.write(this.profile);
374
+ this.captureBaseline();
349
375
  return this.profile;
350
376
  }
351
377
  async addPoint(point) {
@@ -365,6 +391,7 @@ var ProfileStore = class {
365
391
  }
366
392
  this.profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
367
393
  await this.backend.write(this.profile);
394
+ this.captureBaseline();
368
395
  this.eventBus?.emit({
369
396
  type: "data:updated",
370
397
  payload: { timestamp: Date.now() }
@@ -396,9 +423,46 @@ var ProfileStore = class {
396
423
  async save() {
397
424
  if (!this.profile) throw new Error("No profile loaded");
398
425
  if (this.backend.hasExternalChanges && await this.backend.hasExternalChanges()) {
399
- await this.load();
426
+ await this.mergeExternalChanges();
400
427
  }
401
428
  await this.backend.write(this.profile);
429
+ this.captureBaseline();
430
+ }
431
+ /**
432
+ * Three-way merge of a concurrent external write into the in-memory profile,
433
+ * invoked by save() when the backend reports the file changed since we last
434
+ * synced. Writers in this app are single-user but multi-process (agent scanner,
435
+ * diary watcher, dev server, CLI), so concurrent writes are real but rare.
436
+ *
437
+ * Model (base = the dataPoint set at our last load/write, see `baseline`):
438
+ * - a point present in memory → kept as-is (the SAVING process's adds/edits
439
+ * win on a same-point conflict — save() is an explicit "persist my state");
440
+ * - a disk point whose label is NEW since baseline → another writer's
441
+ * addition → preserved;
442
+ * - a point we dropped (in baseline, absent from memory) → our deletion is
443
+ * honored, UNLESS the other writer modified it (then their version is kept
444
+ * so a concurrent edit is never silently lost).
445
+ * Net: nothing vanishes except a same-point simultaneous edit, resolved
446
+ * last-writer (this save) — a deliberate, documented tie-break.
447
+ */
448
+ async mergeExternalChanges() {
449
+ if (!this.profile) return;
450
+ const disk = await this.backend.read();
451
+ if (!disk || !Array.isArray(disk.dataPoints)) return;
452
+ const merged = /* @__PURE__ */ new Map();
453
+ for (const p of this.profile.dataPoints) merged.set(p.label, p);
454
+ for (const dp of disk.dataPoints) {
455
+ if (merged.has(dp.label)) continue;
456
+ const baseJson = this.baseline.get(dp.label);
457
+ if (baseJson === void 0) {
458
+ merged.set(dp.label, dp);
459
+ } else if (baseJson !== JSON.stringify(dp)) {
460
+ merged.set(dp.label, dp);
461
+ }
462
+ }
463
+ this.profile.dataPoints = [...merged.values()];
464
+ this.engine = new DataEngine({ petalCount: this.petalCount });
465
+ for (const p of this.profile.dataPoints) this.engine.addPoint(p);
402
466
  }
403
467
  build() {
404
468
  return this.engine.build();
@@ -611,13 +675,15 @@ var AgentActivityClassifier = class {
611
675
  weight,
612
676
  source: "agent-monitor",
613
677
  files: Array.from(session.targetSet).slice(0, 50),
614
- connections: []
678
+ connections: [],
679
+ eventCount
615
680
  };
616
681
  }
617
682
  };
618
683
 
619
684
  // src/auditTrail.ts
620
685
  import { createHash } from "crypto";
686
+ import { existsSync } from "fs";
621
687
  var AuditTrail = class _AuditTrail {
622
688
  static GENESIS_HASH = "0".repeat(64);
623
689
  /** S21-P4: honest label for cumulative-stats.json's totalEntries. */
@@ -651,6 +717,11 @@ var AuditTrail = class _AuditTrail {
651
717
  this.agentId = agentId;
652
718
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
653
719
  const logDir = options?.logDir ?? `${home}/.dahlia/agents/${agentId}`;
720
+ if (options?.logDir && !existsSync(`${logDir}/audit.log`) && existsSync(`${logDir}/${agentId}/audit.log`)) {
721
+ throw new Error(
722
+ `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.`
723
+ );
724
+ }
654
725
  this.logPath = `${logDir}/audit.log`;
655
726
  this.statsPath = `${logDir}/cumulative-stats.json`;
656
727
  this.manifestPath = `${logDir}/audit.manifest`;
@@ -659,27 +730,11 @@ var AuditTrail = class _AuditTrail {
659
730
  this.maxFileSizeBytes = (options?.maxFileSizeMB ?? 10) * 1024 * 1024;
660
731
  }
661
732
  async open() {
662
- const { mkdir, appendFile, readFile } = await import("fs/promises");
733
+ const { mkdir, appendFile } = await import("fs/promises");
663
734
  const { dirname: dirname2 } = await import("path");
664
735
  await mkdir(dirname2(this.logPath), { recursive: true });
665
736
  await appendFile(this.logPath, "");
666
- this.currentChainHead = _AuditTrail.GENESIS_HASH;
667
- try {
668
- const raw = await readFile(this.logPath, "utf-8");
669
- const lines = raw.trimEnd().split("\n");
670
- for (let i = lines.length - 1; i >= 0; i--) {
671
- if (lines[i].length === 0) continue;
672
- try {
673
- const last = JSON.parse(lines[i]);
674
- if (typeof last.hash === "string" && last.hash.length === 64) {
675
- this.currentChainHead = last.hash;
676
- }
677
- } catch {
678
- }
679
- break;
680
- }
681
- } catch {
682
- }
737
+ this.currentChainHead = await this.lastHashOnDisk() ?? _AuditTrail.GENESIS_HASH;
683
738
  try {
684
739
  const { stat } = await import("fs/promises");
685
740
  this._lastKnownSize = (await stat(this.logPath)).size;
@@ -749,6 +804,9 @@ var AuditTrail = class _AuditTrail {
749
804
  if (finding.dedupKey !== void 0) {
750
805
  entry.dedupKey = finding.dedupKey;
751
806
  }
807
+ if (finding.targetKey !== void 0) {
808
+ entry.targetKey = finding.targetKey;
809
+ }
752
810
  await this.writeLine(entry);
753
811
  }
754
812
  /**
@@ -874,7 +932,11 @@ var AuditTrail = class _AuditTrail {
874
932
  for (const entry of entries) {
875
933
  if (options.type && entry.type !== options.type) continue;
876
934
  const entryMs = new Date(entry.timestamp).getTime();
877
- if (entryMs < startMs || entryMs > endMs) continue;
935
+ if (Number.isNaN(entryMs)) {
936
+ if (options.startTime || options.endTime) continue;
937
+ } else if (entryMs < startMs || entryMs > endMs) {
938
+ continue;
939
+ }
878
940
  if (options.severity && entry.type === "finding") {
879
941
  if (entry.severity !== options.severity) continue;
880
942
  }
@@ -967,6 +1029,7 @@ var AuditTrail = class _AuditTrail {
967
1029
  let signedEntries = 0;
968
1030
  let unsignedEntries = 0;
969
1031
  let invalidSignatures = 0;
1032
+ let seenHashedEntry = false;
970
1033
  let priorFileLastHash = null;
971
1034
  for (const file of files) {
972
1035
  const base = basename(file);
@@ -1037,6 +1100,7 @@ var AuditTrail = class _AuditTrail {
1037
1100
  unsignedEntries++;
1038
1101
  }
1039
1102
  globalIndex++;
1103
+ seenHashedEntry = true;
1040
1104
  checkpointReached = true;
1041
1105
  startIndex = aIdx + 1;
1042
1106
  previousHash = checkpoint.anchorHash;
@@ -1073,10 +1137,26 @@ var AuditTrail = class _AuditTrail {
1073
1137
  try {
1074
1138
  entry = JSON.parse(line);
1075
1139
  } catch {
1076
- globalIndex++;
1077
- continue;
1140
+ return attach({
1141
+ valid: false,
1142
+ totalEntries: globalIndex + 1,
1143
+ brokenAt: globalIndex,
1144
+ signedEntries,
1145
+ unsignedEntries,
1146
+ invalidSignatures
1147
+ });
1078
1148
  }
1079
1149
  if (typeof entry.hash !== "string" || entry.hash.length !== 64) {
1150
+ if (seenHashedEntry) {
1151
+ return attach({
1152
+ valid: false,
1153
+ totalEntries: globalIndex + 1,
1154
+ brokenAt: globalIndex,
1155
+ signedEntries,
1156
+ unsignedEntries,
1157
+ invalidSignatures
1158
+ });
1159
+ }
1080
1160
  globalIndex++;
1081
1161
  continue;
1082
1162
  }
@@ -1116,6 +1196,7 @@ var AuditTrail = class _AuditTrail {
1116
1196
  }
1117
1197
  previousHash = storedHash;
1118
1198
  globalIndex++;
1199
+ seenHashedEntry = true;
1119
1200
  }
1120
1201
  priorFileLastHash = previousHash;
1121
1202
  }
@@ -1753,6 +1834,23 @@ var AuditTrail = class _AuditTrail {
1753
1834
  * the file when an external append actually happened.
1754
1835
  */
1755
1836
  async reanchorFromDiskTail() {
1837
+ const lastHash = await this.lastHashOnDisk();
1838
+ if (lastHash) this.currentChainHead = lastHash;
1839
+ }
1840
+ /**
1841
+ * Scan the on-disk log backward for the last entry carrying a valid (64-hex)
1842
+ * hash, SKIPPING torn/corrupt trailing lines, and return that hash (or null
1843
+ * when no parseable hashed entry exists).
1844
+ *
1845
+ * Shared by open() (chain resume) and reanchorFromDiskTail() (live
1846
+ * re-anchor). The earlier inline versions inspected only the LAST non-empty
1847
+ * line and broke: a torn tail line (a partial write) left the head at genesis
1848
+ * (open) or stale (reanchor), so the next append forked a fresh chain from
1849
+ * genesis — destroying all prior linkage. Walking back leaves the torn line
1850
+ * as a single localized gap that verify() still flags, while the chain head
1851
+ * stays anchored to the last good entry.
1852
+ */
1853
+ async lastHashOnDisk() {
1756
1854
  const { readFile } = await import("fs/promises");
1757
1855
  try {
1758
1856
  const raw = await readFile(this.logPath, "utf-8");
@@ -1760,16 +1858,16 @@ var AuditTrail = class _AuditTrail {
1760
1858
  for (let i = lines.length - 1; i >= 0; i--) {
1761
1859
  if (lines[i].length === 0) continue;
1762
1860
  try {
1763
- const last = JSON.parse(lines[i]);
1764
- if (typeof last.hash === "string" && last.hash.length === 64) {
1765
- this.currentChainHead = last.hash;
1861
+ const entry = JSON.parse(lines[i]);
1862
+ if (typeof entry.hash === "string" && entry.hash.length === 64) {
1863
+ return entry.hash;
1766
1864
  }
1767
1865
  } catch {
1768
1866
  }
1769
- break;
1770
1867
  }
1771
1868
  } catch {
1772
1869
  }
1870
+ return null;
1773
1871
  }
1774
1872
  async readAllEntries() {
1775
1873
  const { readFile } = await import("fs/promises");
@@ -2013,7 +2111,7 @@ var DeviationDetector = class {
2013
2111
  checkVolumeSpike(dataPoint) {
2014
2112
  if (this.baseline.averageEventsPerSession === 0) return null;
2015
2113
  const description = dataPoint.description ?? "";
2016
- const eventCount = parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2114
+ const eventCount = dataPoint.eventCount ?? parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2017
2115
  const threshold = this.baseline.averageEventsPerSession * this.config.volumeSpikeMultiplier;
2018
2116
  if (eventCount <= threshold) return null;
2019
2117
  const multiplier = (eventCount / this.baseline.averageEventsPerSession).toFixed(1);
@@ -2162,7 +2260,7 @@ var DeviationDetector = class {
2162
2260
  checkActivityDrop(dataPoint) {
2163
2261
  if (this.baseline.averageEventsPerSession === 0) return null;
2164
2262
  const description = dataPoint.description ?? "";
2165
- const eventCount = parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2263
+ const eventCount = dataPoint.eventCount ?? parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2166
2264
  if (eventCount === 0) return null;
2167
2265
  const ratio = eventCount / this.baseline.averageEventsPerSession;
2168
2266
  if (ratio < 0.1) {
@@ -2356,12 +2454,8 @@ var COMMON_ABBREVIATIONS = /* @__PURE__ */ new Map([
2356
2454
  ["development", "dev"]
2357
2455
  ]);
2358
2456
  var DEFAULT_INTENT_CONFIG = {
2359
- defaultTtlMs: 8 * 60 * 60 * 1e3,
2457
+ defaultTtlMs: 8 * 60 * 60 * 1e3
2360
2458
  // 8 hours
2361
- keywordBoost: 0.15,
2362
- acceptableActionBoost: 0.3,
2363
- baseScore: 0.2,
2364
- missingTaskScore: 0.5
2365
2459
  };
2366
2460
  var DEFAULT_ACCEPTABLE_ACTIONS = [
2367
2461
  // Config/manifest reads
@@ -2411,15 +2505,14 @@ var DEFAULT_ACCEPTABLE_ACTIONS = [
2411
2505
  { action: "command_exec", targetPattern: "tsc*", reason: "TypeScript compiler" }
2412
2506
  ];
2413
2507
  function extractKeywords(description) {
2414
- const words = description.split(/[^a-zA-Z0-9]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
2415
- const withVariants = new Set(words);
2416
- for (const word of words) {
2417
- const variant = COMMON_ABBREVIATIONS.get(word);
2418
- if (variant) {
2419
- withVariants.add(variant);
2420
- }
2421
- }
2422
- return [...withVariants];
2508
+ const rawTokens = description.split(/[^a-zA-Z0-9]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 0);
2509
+ const keywords = /* @__PURE__ */ new Set();
2510
+ for (const token of rawTokens) {
2511
+ if (token.length > 2 && !STOP_WORDS.has(token)) keywords.add(token);
2512
+ const variant = COMMON_ABBREVIATIONS.get(token);
2513
+ if (variant) keywords.add(variant);
2514
+ }
2515
+ return [...keywords];
2423
2516
  }
2424
2517
  var IntentTracker = class {
2425
2518
  tasks = /* @__PURE__ */ new Map();
@@ -2708,6 +2801,17 @@ var SimilarityEngine = class {
2708
2801
  // src/evaluation.ts
2709
2802
  var DRIFT_WINDOW_SIZE = 5;
2710
2803
  var SUSTAINED_DRIFT_THRESHOLD = 0.25;
2804
+ function computeDriftSeverity(score, windowScores) {
2805
+ const base = score < 0.1 ? "CRITICAL" : score < 0.2 ? "HIGH" : "MEDIUM";
2806
+ if (windowScores.length < DRIFT_WINDOW_SIZE) {
2807
+ return { severity: base, sustained: false, avg: null };
2808
+ }
2809
+ const avg = windowScores.reduce((sum, s) => sum + s, 0) / windowScores.length;
2810
+ if (avg >= SUSTAINED_DRIFT_THRESHOLD) {
2811
+ return { severity: base, sustained: false, avg };
2812
+ }
2813
+ return { severity: base === "CRITICAL" ? "CRITICAL" : "HIGH", sustained: true, avg };
2814
+ }
2711
2815
  function evaluateEvent(event, state, deps) {
2712
2816
  const findings = [];
2713
2817
  let intentAlignment;
@@ -2728,9 +2832,9 @@ function evaluateEvent(event, state, deps) {
2728
2832
  if (newScores.length > DRIFT_WINDOW_SIZE) {
2729
2833
  newScores = newScores.slice(newScores.length - DRIFT_WINDOW_SIZE);
2730
2834
  }
2731
- const scoreSeverity = alignment.score < 0.1 ? "CRITICAL" : alignment.score < 0.2 ? "HIGH" : "MEDIUM";
2835
+ const driftVerdict = computeDriftSeverity(alignment.score, newScores);
2732
2836
  const driftFinding = {
2733
- severity: scoreSeverity,
2837
+ severity: driftVerdict.severity,
2734
2838
  kind: "informational",
2735
2839
  type: "intent_drift",
2736
2840
  agentId: event.agentId,
@@ -2745,17 +2849,9 @@ function evaluateEvent(event, state, deps) {
2745
2849
  recommendation: `Review whether ${describeEvent(event)} is necessary for the current task.`,
2746
2850
  timestamp: event.timestamp
2747
2851
  };
2748
- if (newScores.length >= DRIFT_WINDOW_SIZE) {
2749
- const avg = newScores.reduce((sum, s) => sum + s, 0) / newScores.length;
2750
- if (avg < SUSTAINED_DRIFT_THRESHOLD) {
2751
- const sustainedSeverity = "HIGH";
2752
- const severityRank = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
2753
- if (severityRank[sustainedSeverity] > severityRank[driftFinding.severity]) {
2754
- driftFinding.severity = sustainedSeverity;
2755
- }
2756
- driftFinding.description = `Sustained intent drift detected (${DRIFT_WINDOW_SIZE} consecutive misaligned actions, avg score ${avg.toFixed(2)}): ${alignment.reason}`;
2757
- driftFinding.softSignal = true;
2758
- }
2852
+ if (driftVerdict.sustained) {
2853
+ driftFinding.description = `Sustained intent drift detected (${DRIFT_WINDOW_SIZE} consecutive misaligned actions, avg score ${driftVerdict.avg.toFixed(2)}): ${alignment.reason}`;
2854
+ driftFinding.softSignal = true;
2759
2855
  }
2760
2856
  findings.push(driftFinding);
2761
2857
  } else {
@@ -2798,6 +2894,9 @@ var SentinelRunner = class {
2798
2894
  _maturityConfig;
2799
2895
  // Role validator options (injected by Sentinel facade for exception handling)
2800
2896
  _validatorOptions;
2897
+ // Facade's shared overlay/repo-map-aware scorer (injected before start()).
2898
+ // Falls back to a bare scorer when the runner is used standalone (tests).
2899
+ _sensitivityScorer;
2801
2900
  // Intent alignment
2802
2901
  intentTracker;
2803
2902
  similarityEngine;
@@ -2843,6 +2942,14 @@ var SentinelRunner = class {
2843
2942
  setRoleValidatorOptions(options) {
2844
2943
  this._validatorOptions = options;
2845
2944
  }
2945
+ /**
2946
+ * Inject the facade's shared sensitivity scorer (overlay + repo map aware)
2947
+ * so processEvent's validator scores identically to the facade's check().
2948
+ * Must be called BEFORE start() builds the validator.
2949
+ */
2950
+ setSensitivityScorer(scorer) {
2951
+ this._sensitivityScorer = scorer;
2952
+ }
2846
2953
  /** Set the behavioral baseline and start periodic gap checking. */
2847
2954
  setBaseline(baseline) {
2848
2955
  this._baseline = baseline;
@@ -2899,6 +3006,14 @@ var SentinelRunner = class {
2899
3006
  getSimilarityEngine() {
2900
3007
  return this.similarityEngine;
2901
3008
  }
3009
+ /**
3010
+ * Read-only view of the rolling misaligned-score window, for the
3011
+ * pre-execution gate (Sentinel.check) to apply the SAME sustained-drift
3012
+ * severity upgrade as the recording path — without mutating the window.
3013
+ */
3014
+ getRecentAlignmentScores() {
3015
+ return this.recentAlignmentScores;
3016
+ }
2902
3017
  /**
2903
3018
  * Side-effect-free pre-execution gate: evaluate an event and fire pre_execution
2904
3019
  * hooks WITHOUT running processEvent's full pipeline.
@@ -2978,6 +3093,7 @@ var SentinelRunner = class {
2978
3093
  }
2979
3094
  async runGapCheck() {
2980
3095
  if (!this._deviationDetector || !this.auditTrail) return;
3096
+ if (this._eventCount === 0) return;
2981
3097
  const finding = await this._deviationDetector.checkActivityGap(this.auditTrail);
2982
3098
  if (finding) {
2983
3099
  this.findings.push(finding);
@@ -3009,7 +3125,7 @@ var SentinelRunner = class {
3009
3125
  if (role) {
3010
3126
  this.validator = new RoleValidator(
3011
3127
  role,
3012
- new TargetSensitivityScorer(),
3128
+ this._sensitivityScorer ?? new TargetSensitivityScorer(),
3013
3129
  this._validatorOptions
3014
3130
  );
3015
3131
  }
@@ -3277,7 +3393,7 @@ var SentinelRunner = class {
3277
3393
  const { type } = this.adapterConfig;
3278
3394
  if (type === "manual") return;
3279
3395
  if (type === "log") {
3280
- const { LogAdapter } = await import("./logAdapter-IB6ZDEV2.js");
3396
+ const { LogAdapter } = await import("./logAdapter-WM43W3S7.js");
3281
3397
  const logPath = this.adapterConfig.logPath;
3282
3398
  if (!logPath) {
3283
3399
  throw new Error(`SentinelRunner: logPath is required for adapter type "log"`);
@@ -3295,7 +3411,7 @@ var SentinelRunner = class {
3295
3411
  }).catch((err) => console.warn(`SentinelRunner processEvent error: ${err}`));
3296
3412
  });
3297
3413
  } else if (type === "mcp") {
3298
- const { McpAdapter } = await import("./mcpAdapter-R47GX2P3.js");
3414
+ const { McpAdapter } = await import("./mcpAdapter-WYAXUE7T.js");
3299
3415
  this.adapter = new McpAdapter(this.agentId, this.agentId, {
3300
3416
  logDir: this.adapterConfig.mcpLogDir,
3301
3417
  pollIntervalMs: this.adapterConfig.pollIntervalMs
@@ -3425,6 +3541,9 @@ var SentinelManager = class {
3425
3541
  if (options?.validatorOptions) {
3426
3542
  runner.setRoleValidatorOptions(options.validatorOptions);
3427
3543
  }
3544
+ if (options?.sensitivityScorer) {
3545
+ runner.setSensitivityScorer(options.sensitivityScorer);
3546
+ }
3428
3547
  await runner.start();
3429
3548
  return runner;
3430
3549
  }
@@ -3529,7 +3648,7 @@ var SentinelManager = class {
3529
3648
  };
3530
3649
 
3531
3650
  // src/Sentinel.ts
3532
- import { lstatSync, existsSync } from "fs";
3651
+ import { lstatSync, existsSync as existsSync2 } from "fs";
3533
3652
  import { resolve as resolve2, dirname, sep as sep2 } from "path";
3534
3653
 
3535
3654
  // src/baselineBuilder.ts
@@ -3725,9 +3844,11 @@ var AlertManager = class {
3725
3844
  const topLevelMinKind = this.config.minKind ?? "actionable";
3726
3845
  const findingKindRank = KIND_ORDER[finding.kind] ?? KIND_ORDER.actionable;
3727
3846
  const isConvergedSoftSignal = finding.softSignal === true;
3847
+ let routed = false;
3728
3848
  for (const channel of this.config.channels) {
3729
3849
  const channelMinKind = channel.minKind ?? topLevelMinKind;
3730
3850
  if (!isConvergedSoftSignal && findingKindRank < KIND_ORDER[channelMinKind]) continue;
3851
+ routed = true;
3731
3852
  try {
3732
3853
  if (channel.type === "console") {
3733
3854
  await this.sendConsole(finding);
@@ -3740,7 +3861,9 @@ var AlertManager = class {
3740
3861
  console.warn(`Alert channel "${channel.name}" failed:`, err);
3741
3862
  }
3742
3863
  }
3743
- this.recentAlerts.set(dedupeKey, Date.now());
3864
+ if (routed) {
3865
+ this.recentAlerts.set(dedupeKey, Date.now());
3866
+ }
3744
3867
  }
3745
3868
  clearDedupeCache() {
3746
3869
  this.recentAlerts.clear();
@@ -5644,7 +5767,7 @@ var HookEngine = class {
5644
5767
  candidate = { kind: "allow" };
5645
5768
  selectedRegistration = null;
5646
5769
  }
5647
- if (evaluationResult && candidate.kind === "guide" && evaluationResult.findings.some(
5770
+ if (checkpoint === "pre_execution" && evaluationResult && candidate.kind === "guide" && evaluationResult.findings.some(
5648
5771
  (f) => (f.severity === "HIGH" || f.severity === "CRITICAL") && f.kind === "actionable"
5649
5772
  )) {
5650
5773
  const severityRank = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
@@ -5667,7 +5790,7 @@ var HookEngine = class {
5667
5790
  finding: blockingFinding
5668
5791
  };
5669
5792
  }
5670
- if (candidate.kind === "allow" && evaluationResult && evaluationResult.findings.some(
5793
+ if (checkpoint === "pre_execution" && candidate.kind === "allow" && evaluationResult && evaluationResult.findings.some(
5671
5794
  (f) => (f.severity === "HIGH" || f.severity === "CRITICAL") && f.kind === "actionable"
5672
5795
  )) {
5673
5796
  const severityRank = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
@@ -5913,8 +6036,8 @@ var Sentinel = class _Sentinel {
5913
6036
  this.manager = new SentinelManager(this.profileManager);
5914
6037
  this.sensitivityScorer = new TargetSensitivityScorer();
5915
6038
  this.environment = config?.environment ?? "development";
5916
- this.restrictThreshold = config?.enforcement?.restrictAfter ?? 2;
5917
- this.quarantineThreshold = config?.enforcement?.quarantineAfter ?? 3;
6039
+ this.restrictThreshold = config?.enforcement?.restrictAfter ?? DEFAULT_RESTRICT_AFTER;
6040
+ this.quarantineThreshold = config?.enforcement?.quarantineAfter ?? DEFAULT_QUARANTINE_AFTER;
5918
6041
  this.enablePreExecutionHooks = config?.enablePreExecutionHooks ?? false;
5919
6042
  this.promoteSet = new Set(config?.enforcement?.promote ?? []);
5920
6043
  this.maturityConfig = config?.enforcement?.baselineMaturity;
@@ -6154,10 +6277,18 @@ var Sentinel = class _Sentinel {
6154
6277
  );
6155
6278
  }
6156
6279
  async logModeChange(agentId, mode, reason, previousMode) {
6157
- const runner = this.manager.getRunner(agentId);
6158
- const trail = runner?.getAuditTrail();
6159
- if (!trail) return;
6160
- await trail.logModeChange(mode, reason, previousMode);
6280
+ const liveTrail = this.manager.getRunner(agentId)?.getAuditTrail();
6281
+ if (liveTrail) {
6282
+ await liveTrail.logModeChange(mode, reason, previousMode);
6283
+ return;
6284
+ }
6285
+ const trail = new AuditTrail(agentId, { logDir: `${this.agentsDir}/${agentId}` });
6286
+ await trail.open();
6287
+ try {
6288
+ await trail.logModeChange(mode, reason, previousMode);
6289
+ } finally {
6290
+ await trail.close();
6291
+ }
6161
6292
  }
6162
6293
  async stopAgent(agentId, reason) {
6163
6294
  const { mkdir, writeFile } = await import("fs/promises");
@@ -6264,7 +6395,10 @@ var Sentinel = class _Sentinel {
6264
6395
  const engine = runner.getSimilarityEngine();
6265
6396
  const alignment = engine.computeAlignment(activeTask, event);
6266
6397
  if (!alignment.aligned && !alignment.bypassed) {
6267
- const severity = alignment.score < 0.1 ? "CRITICAL" : alignment.score < 0.2 ? "HIGH" : "MEDIUM";
6398
+ const projectedWindow = [...runner.getRecentAlignmentScores(), alignment.score].slice(
6399
+ -DRIFT_WINDOW_SIZE
6400
+ );
6401
+ const { severity } = computeDriftSeverity(alignment.score, projectedWindow);
6268
6402
  return {
6269
6403
  severity,
6270
6404
  kind: "informational",
@@ -6389,7 +6523,10 @@ var Sentinel = class _Sentinel {
6389
6523
  // Approach-1 anchoring: give the runner's processEvent validator the SAME
6390
6524
  // workspaceRoot check() uses (this._workspaceRoot), supplied BEFORE start()
6391
6525
  // builds it — otherwise it logs phantom scope_violations on legit reads.
6392
- validatorOptions: { workspaceRoot: this._workspaceRoot }
6526
+ validatorOptions: { workspaceRoot: this._workspaceRoot },
6527
+ // Share the facade's overlay/repo-map-aware scorer so record() scores
6528
+ // sensitivity identically to check() (same live instance).
6529
+ sensitivityScorer: this.sensitivityScorer
6393
6530
  });
6394
6531
  this.wireRunner(runner);
6395
6532
  await this.enableAuditSigning(runner, agentId);
@@ -6418,7 +6555,7 @@ var Sentinel = class _Sentinel {
6418
6555
  );
6419
6556
  }
6420
6557
  }
6421
- if (!existsSync(root)) {
6558
+ if (!existsSync2(root)) {
6422
6559
  console.warn(
6423
6560
  `[Sentinel] Workspace root '${root}' does not exist on disk \u2014 anchored allowed patterns will match nothing and every allowed read may be denied.`
6424
6561
  );
@@ -6480,7 +6617,8 @@ var Sentinel = class _Sentinel {
6480
6617
  // Approach-1 anchoring: same workspaceRoot as check() (this._workspaceRoot),
6481
6618
  // supplied BEFORE start() builds the runner's validator. This is the live
6482
6619
  // gateway path (fromPolicy → monitor).
6483
- validatorOptions: { workspaceRoot: this._workspaceRoot }
6620
+ validatorOptions: { workspaceRoot: this._workspaceRoot },
6621
+ sensitivityScorer: this.sensitivityScorer
6484
6622
  });
6485
6623
  this.wireRunner(runner);
6486
6624
  await this.enableAuditSigning(runner, agentId);
@@ -6543,7 +6681,8 @@ var Sentinel = class _Sentinel {
6543
6681
  await this.profileManager.saveRole(agentId, role);
6544
6682
  const runner = await this.manager.getOrCreateAgent(agentId, void 0, {
6545
6683
  auditLogDir: this.agentsDir,
6546
- validatorOptions: { workspaceRoot: options.workspaceRoot, approvalFn: () => true }
6684
+ validatorOptions: { workspaceRoot: options.workspaceRoot, approvalFn: () => true },
6685
+ sensitivityScorer: this.sensitivityScorer
6547
6686
  });
6548
6687
  this.wireRunner(runner, options.workspaceRoot);
6549
6688
  await this.enableAuditSigning(runner, agentId);
@@ -6604,9 +6743,7 @@ var Sentinel = class _Sentinel {
6604
6743
  workspaceRoot,
6605
6744
  exceptions: role.exceptions,
6606
6745
  networkHosts: role.networkHosts,
6607
- expectedSchedule: role.expectedSchedule,
6608
- maxEventsPerHour: role.maxEventsPerHour,
6609
- maxSessionDuration: role.maxSessionDuration
6746
+ expectedSchedule: role.expectedSchedule
6610
6747
  });
6611
6748
  if (policy.enforcement?.approvalRequired) {
6612
6749
  sentinel.onApprovalRequired(createCliApproval());
@@ -6742,7 +6879,16 @@ var Sentinel = class _Sentinel {
6742
6879
  const recordOpts = this.enablePreExecutionHooks ? { _skipPreHooks: true } : void 0;
6743
6880
  const modeResult = await this.enforceMode(agentId, fullEvent);
6744
6881
  if (modeResult && modeResult.finding) {
6745
- await this._recordInternal({ ...fullEvent, authorized: false }, recordOpts);
6882
+ const recordResult = await this._recordInternal(
6883
+ { ...fullEvent, authorized: false },
6884
+ recordOpts
6885
+ );
6886
+ const alreadyCounted = recordResult.findings.some(
6887
+ (f) => _Sentinel.isEscalationEligible(f) && f.mentionOnly !== true
6888
+ );
6889
+ if (!alreadyCounted) {
6890
+ await this.logFinding(agentId, modeResult.finding);
6891
+ }
6746
6892
  await this.maybeEscalate(agentId, modeResult.finding);
6747
6893
  return { blocked: true, finding: modeResult.finding, result: void 0 };
6748
6894
  }
@@ -7421,8 +7567,10 @@ var Sentinel = class _Sentinel {
7421
7567
  const distinctKeys = /* @__PURE__ */ new Set();
7422
7568
  let keyless = 0;
7423
7569
  for (const f of survivors) {
7424
- const key = f.dedupKey;
7425
- if (typeof key === "string" && key.length > 0) distinctKeys.add(key);
7570
+ const tk = f.targetKey;
7571
+ const dk = f.dedupKey;
7572
+ if (typeof tk === "string" && tk.length > 0) distinctKeys.add(`t:${tk}`);
7573
+ else if (typeof dk === "string" && dk.length > 0) distinctKeys.add(`c:${dk}`);
7426
7574
  else keyless++;
7427
7575
  }
7428
7576
  return distinctKeys.size + keyless;
@@ -7470,4 +7618,4 @@ export {
7470
7618
  createCliApproval,
7471
7619
  Sentinel
7472
7620
  };
7473
- //# sourceMappingURL=chunk-GRN5P3H2.js.map
7621
+ //# sourceMappingURL=chunk-I2FVDDSG.js.map