@tuent/sentinel 0.1.3 → 0.1.5

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-JTR2E7RD.js";
18
+ walkForbiddenInodeRoots
19
+ } from "./chunk-M5EEVMLU.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-SKE74CYZ.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;
@@ -336,8 +351,15 @@ var ProfileStore = class {
336
351
  payload: { profileId: this.profile.id }
337
352
  });
338
353
  }
354
+ this.captureBaseline();
339
355
  return this.profile;
340
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
+ }
341
363
  async create(name) {
342
364
  const now = (/* @__PURE__ */ new Date()).toISOString();
343
365
  this.profile = {
@@ -349,6 +371,7 @@ var ProfileStore = class {
349
371
  updatedAt: now
350
372
  };
351
373
  await this.backend.write(this.profile);
374
+ this.captureBaseline();
352
375
  return this.profile;
353
376
  }
354
377
  async addPoint(point) {
@@ -368,6 +391,7 @@ var ProfileStore = class {
368
391
  }
369
392
  this.profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
370
393
  await this.backend.write(this.profile);
394
+ this.captureBaseline();
371
395
  this.eventBus?.emit({
372
396
  type: "data:updated",
373
397
  payload: { timestamp: Date.now() }
@@ -399,9 +423,46 @@ var ProfileStore = class {
399
423
  async save() {
400
424
  if (!this.profile) throw new Error("No profile loaded");
401
425
  if (this.backend.hasExternalChanges && await this.backend.hasExternalChanges()) {
402
- await this.load();
426
+ await this.mergeExternalChanges();
403
427
  }
404
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);
405
466
  }
406
467
  build() {
407
468
  return this.engine.build();
@@ -614,7 +675,8 @@ var AgentActivityClassifier = class {
614
675
  weight,
615
676
  source: "agent-monitor",
616
677
  files: Array.from(session.targetSet).slice(0, 50),
617
- connections: []
678
+ connections: [],
679
+ eventCount
618
680
  };
619
681
  }
620
682
  };
@@ -668,27 +730,11 @@ var AuditTrail = class _AuditTrail {
668
730
  this.maxFileSizeBytes = (options?.maxFileSizeMB ?? 10) * 1024 * 1024;
669
731
  }
670
732
  async open() {
671
- const { mkdir, appendFile, readFile } = await import("fs/promises");
733
+ const { mkdir, appendFile } = await import("fs/promises");
672
734
  const { dirname: dirname2 } = await import("path");
673
735
  await mkdir(dirname2(this.logPath), { recursive: true });
674
736
  await appendFile(this.logPath, "");
675
- this.currentChainHead = _AuditTrail.GENESIS_HASH;
676
- try {
677
- const raw = await readFile(this.logPath, "utf-8");
678
- const lines = raw.trimEnd().split("\n");
679
- for (let i = lines.length - 1; i >= 0; i--) {
680
- if (lines[i].length === 0) continue;
681
- try {
682
- const last = JSON.parse(lines[i]);
683
- if (typeof last.hash === "string" && last.hash.length === 64) {
684
- this.currentChainHead = last.hash;
685
- }
686
- } catch {
687
- }
688
- break;
689
- }
690
- } catch {
691
- }
737
+ this.currentChainHead = await this.lastHashOnDisk() ?? _AuditTrail.GENESIS_HASH;
692
738
  try {
693
739
  const { stat } = await import("fs/promises");
694
740
  this._lastKnownSize = (await stat(this.logPath)).size;
@@ -886,7 +932,11 @@ var AuditTrail = class _AuditTrail {
886
932
  for (const entry of entries) {
887
933
  if (options.type && entry.type !== options.type) continue;
888
934
  const entryMs = new Date(entry.timestamp).getTime();
889
- 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
+ }
890
940
  if (options.severity && entry.type === "finding") {
891
941
  if (entry.severity !== options.severity) continue;
892
942
  }
@@ -979,6 +1029,7 @@ var AuditTrail = class _AuditTrail {
979
1029
  let signedEntries = 0;
980
1030
  let unsignedEntries = 0;
981
1031
  let invalidSignatures = 0;
1032
+ let seenHashedEntry = false;
982
1033
  let priorFileLastHash = null;
983
1034
  for (const file of files) {
984
1035
  const base = basename(file);
@@ -1049,6 +1100,7 @@ var AuditTrail = class _AuditTrail {
1049
1100
  unsignedEntries++;
1050
1101
  }
1051
1102
  globalIndex++;
1103
+ seenHashedEntry = true;
1052
1104
  checkpointReached = true;
1053
1105
  startIndex = aIdx + 1;
1054
1106
  previousHash = checkpoint.anchorHash;
@@ -1085,10 +1137,26 @@ var AuditTrail = class _AuditTrail {
1085
1137
  try {
1086
1138
  entry = JSON.parse(line);
1087
1139
  } catch {
1088
- globalIndex++;
1089
- continue;
1140
+ return attach({
1141
+ valid: false,
1142
+ totalEntries: globalIndex + 1,
1143
+ brokenAt: globalIndex,
1144
+ signedEntries,
1145
+ unsignedEntries,
1146
+ invalidSignatures
1147
+ });
1090
1148
  }
1091
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
+ }
1092
1160
  globalIndex++;
1093
1161
  continue;
1094
1162
  }
@@ -1128,6 +1196,7 @@ var AuditTrail = class _AuditTrail {
1128
1196
  }
1129
1197
  previousHash = storedHash;
1130
1198
  globalIndex++;
1199
+ seenHashedEntry = true;
1131
1200
  }
1132
1201
  priorFileLastHash = previousHash;
1133
1202
  }
@@ -1765,6 +1834,23 @@ var AuditTrail = class _AuditTrail {
1765
1834
  * the file when an external append actually happened.
1766
1835
  */
1767
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() {
1768
1854
  const { readFile } = await import("fs/promises");
1769
1855
  try {
1770
1856
  const raw = await readFile(this.logPath, "utf-8");
@@ -1772,16 +1858,16 @@ var AuditTrail = class _AuditTrail {
1772
1858
  for (let i = lines.length - 1; i >= 0; i--) {
1773
1859
  if (lines[i].length === 0) continue;
1774
1860
  try {
1775
- const last = JSON.parse(lines[i]);
1776
- if (typeof last.hash === "string" && last.hash.length === 64) {
1777
- 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;
1778
1864
  }
1779
1865
  } catch {
1780
1866
  }
1781
- break;
1782
1867
  }
1783
1868
  } catch {
1784
1869
  }
1870
+ return null;
1785
1871
  }
1786
1872
  async readAllEntries() {
1787
1873
  const { readFile } = await import("fs/promises");
@@ -2025,7 +2111,7 @@ var DeviationDetector = class {
2025
2111
  checkVolumeSpike(dataPoint) {
2026
2112
  if (this.baseline.averageEventsPerSession === 0) return null;
2027
2113
  const description = dataPoint.description ?? "";
2028
- const eventCount = parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2114
+ const eventCount = dataPoint.eventCount ?? parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2029
2115
  const threshold = this.baseline.averageEventsPerSession * this.config.volumeSpikeMultiplier;
2030
2116
  if (eventCount <= threshold) return null;
2031
2117
  const multiplier = (eventCount / this.baseline.averageEventsPerSession).toFixed(1);
@@ -2174,7 +2260,7 @@ var DeviationDetector = class {
2174
2260
  checkActivityDrop(dataPoint) {
2175
2261
  if (this.baseline.averageEventsPerSession === 0) return null;
2176
2262
  const description = dataPoint.description ?? "";
2177
- const eventCount = parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2263
+ const eventCount = dataPoint.eventCount ?? parseEventCount(description) ?? estimateEventsFromWeight(dataPoint.weight ?? 0);
2178
2264
  if (eventCount === 0) return null;
2179
2265
  const ratio = eventCount / this.baseline.averageEventsPerSession;
2180
2266
  if (ratio < 0.1) {
@@ -2368,12 +2454,8 @@ var COMMON_ABBREVIATIONS = /* @__PURE__ */ new Map([
2368
2454
  ["development", "dev"]
2369
2455
  ]);
2370
2456
  var DEFAULT_INTENT_CONFIG = {
2371
- defaultTtlMs: 8 * 60 * 60 * 1e3,
2457
+ defaultTtlMs: 8 * 60 * 60 * 1e3
2372
2458
  // 8 hours
2373
- keywordBoost: 0.15,
2374
- acceptableActionBoost: 0.3,
2375
- baseScore: 0.2,
2376
- missingTaskScore: 0.5
2377
2459
  };
2378
2460
  var DEFAULT_ACCEPTABLE_ACTIONS = [
2379
2461
  // Config/manifest reads
@@ -2423,15 +2505,14 @@ var DEFAULT_ACCEPTABLE_ACTIONS = [
2423
2505
  { action: "command_exec", targetPattern: "tsc*", reason: "TypeScript compiler" }
2424
2506
  ];
2425
2507
  function extractKeywords(description) {
2426
- const words = description.split(/[^a-zA-Z0-9]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
2427
- const withVariants = new Set(words);
2428
- for (const word of words) {
2429
- const variant = COMMON_ABBREVIATIONS.get(word);
2430
- if (variant) {
2431
- withVariants.add(variant);
2432
- }
2433
- }
2434
- 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];
2435
2516
  }
2436
2517
  var IntentTracker = class {
2437
2518
  tasks = /* @__PURE__ */ new Map();
@@ -2720,6 +2801,17 @@ var SimilarityEngine = class {
2720
2801
  // src/evaluation.ts
2721
2802
  var DRIFT_WINDOW_SIZE = 5;
2722
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
+ }
2723
2815
  function evaluateEvent(event, state, deps) {
2724
2816
  const findings = [];
2725
2817
  let intentAlignment;
@@ -2740,9 +2832,9 @@ function evaluateEvent(event, state, deps) {
2740
2832
  if (newScores.length > DRIFT_WINDOW_SIZE) {
2741
2833
  newScores = newScores.slice(newScores.length - DRIFT_WINDOW_SIZE);
2742
2834
  }
2743
- const scoreSeverity = alignment.score < 0.1 ? "CRITICAL" : alignment.score < 0.2 ? "HIGH" : "MEDIUM";
2835
+ const driftVerdict = computeDriftSeverity(alignment.score, newScores);
2744
2836
  const driftFinding = {
2745
- severity: scoreSeverity,
2837
+ severity: driftVerdict.severity,
2746
2838
  kind: "informational",
2747
2839
  type: "intent_drift",
2748
2840
  agentId: event.agentId,
@@ -2757,17 +2849,9 @@ function evaluateEvent(event, state, deps) {
2757
2849
  recommendation: `Review whether ${describeEvent(event)} is necessary for the current task.`,
2758
2850
  timestamp: event.timestamp
2759
2851
  };
2760
- if (newScores.length >= DRIFT_WINDOW_SIZE) {
2761
- const avg = newScores.reduce((sum, s) => sum + s, 0) / newScores.length;
2762
- if (avg < SUSTAINED_DRIFT_THRESHOLD) {
2763
- const sustainedSeverity = "HIGH";
2764
- const severityRank = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
2765
- if (severityRank[sustainedSeverity] > severityRank[driftFinding.severity]) {
2766
- driftFinding.severity = sustainedSeverity;
2767
- }
2768
- driftFinding.description = `Sustained intent drift detected (${DRIFT_WINDOW_SIZE} consecutive misaligned actions, avg score ${avg.toFixed(2)}): ${alignment.reason}`;
2769
- driftFinding.softSignal = true;
2770
- }
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;
2771
2855
  }
2772
2856
  findings.push(driftFinding);
2773
2857
  } else {
@@ -2810,6 +2894,9 @@ var SentinelRunner = class {
2810
2894
  _maturityConfig;
2811
2895
  // Role validator options (injected by Sentinel facade for exception handling)
2812
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;
2813
2900
  // Intent alignment
2814
2901
  intentTracker;
2815
2902
  similarityEngine;
@@ -2855,6 +2942,14 @@ var SentinelRunner = class {
2855
2942
  setRoleValidatorOptions(options) {
2856
2943
  this._validatorOptions = options;
2857
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
+ }
2858
2953
  /** Set the behavioral baseline and start periodic gap checking. */
2859
2954
  setBaseline(baseline) {
2860
2955
  this._baseline = baseline;
@@ -2911,6 +3006,14 @@ var SentinelRunner = class {
2911
3006
  getSimilarityEngine() {
2912
3007
  return this.similarityEngine;
2913
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
+ }
2914
3017
  /**
2915
3018
  * Side-effect-free pre-execution gate: evaluate an event and fire pre_execution
2916
3019
  * hooks WITHOUT running processEvent's full pipeline.
@@ -3022,7 +3125,7 @@ var SentinelRunner = class {
3022
3125
  if (role) {
3023
3126
  this.validator = new RoleValidator(
3024
3127
  role,
3025
- new TargetSensitivityScorer(),
3128
+ this._sensitivityScorer ?? new TargetSensitivityScorer(),
3026
3129
  this._validatorOptions
3027
3130
  );
3028
3131
  }
@@ -3290,7 +3393,7 @@ var SentinelRunner = class {
3290
3393
  const { type } = this.adapterConfig;
3291
3394
  if (type === "manual") return;
3292
3395
  if (type === "log") {
3293
- const { LogAdapter } = await import("./logAdapter-IB6ZDEV2.js");
3396
+ const { LogAdapter } = await import("./logAdapter-WM43W3S7.js");
3294
3397
  const logPath = this.adapterConfig.logPath;
3295
3398
  if (!logPath) {
3296
3399
  throw new Error(`SentinelRunner: logPath is required for adapter type "log"`);
@@ -3308,7 +3411,7 @@ var SentinelRunner = class {
3308
3411
  }).catch((err) => console.warn(`SentinelRunner processEvent error: ${err}`));
3309
3412
  });
3310
3413
  } else if (type === "mcp") {
3311
- const { McpAdapter } = await import("./mcpAdapter-R47GX2P3.js");
3414
+ const { McpAdapter } = await import("./mcpAdapter-WYAXUE7T.js");
3312
3415
  this.adapter = new McpAdapter(this.agentId, this.agentId, {
3313
3416
  logDir: this.adapterConfig.mcpLogDir,
3314
3417
  pollIntervalMs: this.adapterConfig.pollIntervalMs
@@ -3438,6 +3541,9 @@ var SentinelManager = class {
3438
3541
  if (options?.validatorOptions) {
3439
3542
  runner.setRoleValidatorOptions(options.validatorOptions);
3440
3543
  }
3544
+ if (options?.sensitivityScorer) {
3545
+ runner.setSensitivityScorer(options.sensitivityScorer);
3546
+ }
3441
3547
  await runner.start();
3442
3548
  return runner;
3443
3549
  }
@@ -3738,9 +3844,11 @@ var AlertManager = class {
3738
3844
  const topLevelMinKind = this.config.minKind ?? "actionable";
3739
3845
  const findingKindRank = KIND_ORDER[finding.kind] ?? KIND_ORDER.actionable;
3740
3846
  const isConvergedSoftSignal = finding.softSignal === true;
3847
+ let routed = false;
3741
3848
  for (const channel of this.config.channels) {
3742
3849
  const channelMinKind = channel.minKind ?? topLevelMinKind;
3743
3850
  if (!isConvergedSoftSignal && findingKindRank < KIND_ORDER[channelMinKind]) continue;
3851
+ routed = true;
3744
3852
  try {
3745
3853
  if (channel.type === "console") {
3746
3854
  await this.sendConsole(finding);
@@ -3753,7 +3861,9 @@ var AlertManager = class {
3753
3861
  console.warn(`Alert channel "${channel.name}" failed:`, err);
3754
3862
  }
3755
3863
  }
3756
- this.recentAlerts.set(dedupeKey, Date.now());
3864
+ if (routed) {
3865
+ this.recentAlerts.set(dedupeKey, Date.now());
3866
+ }
3757
3867
  }
3758
3868
  clearDedupeCache() {
3759
3869
  this.recentAlerts.clear();
@@ -5657,7 +5767,7 @@ var HookEngine = class {
5657
5767
  candidate = { kind: "allow" };
5658
5768
  selectedRegistration = null;
5659
5769
  }
5660
- if (evaluationResult && candidate.kind === "guide" && evaluationResult.findings.some(
5770
+ if (checkpoint === "pre_execution" && evaluationResult && candidate.kind === "guide" && evaluationResult.findings.some(
5661
5771
  (f) => (f.severity === "HIGH" || f.severity === "CRITICAL") && f.kind === "actionable"
5662
5772
  )) {
5663
5773
  const severityRank = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
@@ -5680,7 +5790,7 @@ var HookEngine = class {
5680
5790
  finding: blockingFinding
5681
5791
  };
5682
5792
  }
5683
- if (candidate.kind === "allow" && evaluationResult && evaluationResult.findings.some(
5793
+ if (checkpoint === "pre_execution" && candidate.kind === "allow" && evaluationResult && evaluationResult.findings.some(
5684
5794
  (f) => (f.severity === "HIGH" || f.severity === "CRITICAL") && f.kind === "actionable"
5685
5795
  )) {
5686
5796
  const severityRank = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
@@ -5926,8 +6036,8 @@ var Sentinel = class _Sentinel {
5926
6036
  this.manager = new SentinelManager(this.profileManager);
5927
6037
  this.sensitivityScorer = new TargetSensitivityScorer();
5928
6038
  this.environment = config?.environment ?? "development";
5929
- this.restrictThreshold = config?.enforcement?.restrictAfter ?? 2;
5930
- this.quarantineThreshold = config?.enforcement?.quarantineAfter ?? 3;
6039
+ this.restrictThreshold = config?.enforcement?.restrictAfter ?? DEFAULT_RESTRICT_AFTER;
6040
+ this.quarantineThreshold = config?.enforcement?.quarantineAfter ?? DEFAULT_QUARANTINE_AFTER;
5931
6041
  this.enablePreExecutionHooks = config?.enablePreExecutionHooks ?? false;
5932
6042
  this.promoteSet = new Set(config?.enforcement?.promote ?? []);
5933
6043
  this.maturityConfig = config?.enforcement?.baselineMaturity;
@@ -6167,10 +6277,18 @@ var Sentinel = class _Sentinel {
6167
6277
  );
6168
6278
  }
6169
6279
  async logModeChange(agentId, mode, reason, previousMode) {
6170
- const runner = this.manager.getRunner(agentId);
6171
- const trail = runner?.getAuditTrail();
6172
- if (!trail) return;
6173
- 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
+ }
6174
6292
  }
6175
6293
  async stopAgent(agentId, reason) {
6176
6294
  const { mkdir, writeFile } = await import("fs/promises");
@@ -6277,7 +6395,10 @@ var Sentinel = class _Sentinel {
6277
6395
  const engine = runner.getSimilarityEngine();
6278
6396
  const alignment = engine.computeAlignment(activeTask, event);
6279
6397
  if (!alignment.aligned && !alignment.bypassed) {
6280
- 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);
6281
6402
  return {
6282
6403
  severity,
6283
6404
  kind: "informational",
@@ -6402,7 +6523,10 @@ var Sentinel = class _Sentinel {
6402
6523
  // Approach-1 anchoring: give the runner's processEvent validator the SAME
6403
6524
  // workspaceRoot check() uses (this._workspaceRoot), supplied BEFORE start()
6404
6525
  // builds it — otherwise it logs phantom scope_violations on legit reads.
6405
- 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
6406
6530
  });
6407
6531
  this.wireRunner(runner);
6408
6532
  await this.enableAuditSigning(runner, agentId);
@@ -6493,7 +6617,8 @@ var Sentinel = class _Sentinel {
6493
6617
  // Approach-1 anchoring: same workspaceRoot as check() (this._workspaceRoot),
6494
6618
  // supplied BEFORE start() builds the runner's validator. This is the live
6495
6619
  // gateway path (fromPolicy → monitor).
6496
- validatorOptions: { workspaceRoot: this._workspaceRoot }
6620
+ validatorOptions: { workspaceRoot: this._workspaceRoot },
6621
+ sensitivityScorer: this.sensitivityScorer
6497
6622
  });
6498
6623
  this.wireRunner(runner);
6499
6624
  await this.enableAuditSigning(runner, agentId);
@@ -6556,7 +6681,8 @@ var Sentinel = class _Sentinel {
6556
6681
  await this.profileManager.saveRole(agentId, role);
6557
6682
  const runner = await this.manager.getOrCreateAgent(agentId, void 0, {
6558
6683
  auditLogDir: this.agentsDir,
6559
- validatorOptions: { workspaceRoot: options.workspaceRoot, approvalFn: () => true }
6684
+ validatorOptions: { workspaceRoot: options.workspaceRoot, approvalFn: () => true },
6685
+ sensitivityScorer: this.sensitivityScorer
6560
6686
  });
6561
6687
  this.wireRunner(runner, options.workspaceRoot);
6562
6688
  await this.enableAuditSigning(runner, agentId);
@@ -6617,9 +6743,7 @@ var Sentinel = class _Sentinel {
6617
6743
  workspaceRoot,
6618
6744
  exceptions: role.exceptions,
6619
6745
  networkHosts: role.networkHosts,
6620
- expectedSchedule: role.expectedSchedule,
6621
- maxEventsPerHour: role.maxEventsPerHour,
6622
- maxSessionDuration: role.maxSessionDuration
6746
+ expectedSchedule: role.expectedSchedule
6623
6747
  });
6624
6748
  if (policy.enforcement?.approvalRequired) {
6625
6749
  sentinel.onApprovalRequired(createCliApproval());
@@ -6755,7 +6879,16 @@ var Sentinel = class _Sentinel {
6755
6879
  const recordOpts = this.enablePreExecutionHooks ? { _skipPreHooks: true } : void 0;
6756
6880
  const modeResult = await this.enforceMode(agentId, fullEvent);
6757
6881
  if (modeResult && modeResult.finding) {
6758
- 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
+ }
6759
6892
  await this.maybeEscalate(agentId, modeResult.finding);
6760
6893
  return { blocked: true, finding: modeResult.finding, result: void 0 };
6761
6894
  }
@@ -7485,4 +7618,4 @@ export {
7485
7618
  createCliApproval,
7486
7619
  Sentinel
7487
7620
  };
7488
- //# sourceMappingURL=chunk-SSDIBY52.js.map
7621
+ //# sourceMappingURL=chunk-7R6EA7JG.js.map