@tuent/sentinel 0.1.3 → 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.
@@ -0,0 +1,10 @@
1
+ import {
2
+ Sentinel
3
+ } from "./chunk-I2FVDDSG.js";
4
+ import "./chunk-FIEIGBYL.js";
5
+ import "./chunk-KWZ7JKKO.js";
6
+ import "./chunk-NUXSUSYY.js";
7
+ export {
8
+ Sentinel
9
+ };
10
+ //# sourceMappingURL=Sentinel-4QKPFHTI.js.map
@@ -16,6 +16,7 @@ interface SkillNode {
16
16
  core?: boolean;
17
17
  sharable?: boolean;
18
18
  files?: string[];
19
+ eventCount?: number;
19
20
  }
20
21
 
21
22
  interface DataPoint {
@@ -29,6 +30,7 @@ interface DataPoint {
29
30
  files?: string[];
30
31
  sharable?: boolean;
31
32
  core?: boolean;
33
+ eventCount?: number;
32
34
  }
33
35
 
34
36
  interface StoredProfile {
@@ -89,16 +91,41 @@ declare class ProfileStore {
89
91
  private petalCount;
90
92
  private engine;
91
93
  private profile;
94
+ /** Snapshot (label → serialized point) of the on-disk-synced dataPoint set,
95
+ * refreshed on every load/create/write. The base for save()'s three-way merge
96
+ * of concurrent external writes — lets us tell "I deleted this" (in baseline,
97
+ * gone from memory) from "another writer added this" (on disk, not in baseline). */
98
+ private baseline;
92
99
  constructor(options: {
93
100
  backend: StorageBackend;
94
101
  eventBus?: EventBus;
95
102
  petalCount?: number;
96
103
  });
97
104
  load(): Promise<StoredProfile | null>;
105
+ /** Snapshot the on-disk-synced dataPoint set for save()'s three-way merge. */
106
+ private captureBaseline;
98
107
  create(name: string): Promise<StoredProfile>;
99
108
  addPoint(point: DataPoint): Promise<void>;
100
109
  addPoints(points: DataPoint[]): void;
101
110
  save(): Promise<void>;
111
+ /**
112
+ * Three-way merge of a concurrent external write into the in-memory profile,
113
+ * invoked by save() when the backend reports the file changed since we last
114
+ * synced. Writers in this app are single-user but multi-process (agent scanner,
115
+ * diary watcher, dev server, CLI), so concurrent writes are real but rare.
116
+ *
117
+ * Model (base = the dataPoint set at our last load/write, see `baseline`):
118
+ * - a point present in memory → kept as-is (the SAVING process's adds/edits
119
+ * win on a same-point conflict — save() is an explicit "persist my state");
120
+ * - a disk point whose label is NEW since baseline → another writer's
121
+ * addition → preserved;
122
+ * - a point we dropped (in baseline, absent from memory) → our deletion is
123
+ * honored, UNLESS the other writer modified it (then their version is kept
124
+ * so a concurrent edit is never silently lost).
125
+ * Net: nothing vanishes except a same-point simultaneous edit, resolved
126
+ * last-writer (this save) — a deliberate, documented tie-break.
127
+ */
128
+ private mergeExternalChanges;
102
129
  build(): SkillNode[];
103
130
  getProfile(): StoredProfile | null;
104
131
  }
@@ -380,7 +407,6 @@ interface SentinelConfig {
380
407
  mcpLogDir?: string;
381
408
  webhookPort?: number;
382
409
  webhookApiKey?: string;
383
- roleDefinitionPath?: string;
384
410
  readExisting?: boolean;
385
411
  }[];
386
412
  alerts?: {
@@ -400,9 +426,6 @@ interface SentinelConfig {
400
426
  minKind?: "informational" | "actionable";
401
427
  }[];
402
428
  };
403
- alertWebhook?: string;
404
- baselineWindowDays?: number;
405
- checkIntervalMs?: number;
406
429
  }
407
430
  /** A declared task/intent for an agent. */
408
431
  interface TaskIntent {
@@ -447,13 +470,18 @@ interface IntentAlignmentResult {
447
470
  bypassed?: boolean;
448
471
  bypassReason?: string | null;
449
472
  }
450
- /** Configuration for intent alignment scoring. */
473
+ /**
474
+ * Configuration for intent alignment scoring.
475
+ *
476
+ * NOTE: the keywordBoost / acceptableActionBoost / baseScore /
477
+ * missingTaskScore knobs were removed — they were defined, defaulted, and
478
+ * typed but never read by any scoring path (vestiges of a pre-weighted
479
+ * scoring model superseded by SimilarityEngine.keywordSimilarity). They
480
+ * promised tunability that did not exist; the live weights are fixed in
481
+ * SimilarityEngine. defaultTtlMs is the only consumed numeric knob.
482
+ */
451
483
  interface IntentAlignmentConfig {
452
484
  defaultTtlMs: number;
453
- keywordBoost: number;
454
- acceptableActionBoost: number;
455
- baseScore: number;
456
- missingTaskScore: number;
457
485
  disableDefaultAcceptable?: boolean;
458
486
  denyAcceptable?: string[];
459
487
  similarityThreshold?: number;
@@ -864,6 +892,20 @@ declare class AuditTrail {
864
892
  * the file when an external append actually happened.
865
893
  */
866
894
  private reanchorFromDiskTail;
895
+ /**
896
+ * Scan the on-disk log backward for the last entry carrying a valid (64-hex)
897
+ * hash, SKIPPING torn/corrupt trailing lines, and return that hash (or null
898
+ * when no parseable hashed entry exists).
899
+ *
900
+ * Shared by open() (chain resume) and reanchorFromDiskTail() (live
901
+ * re-anchor). The earlier inline versions inspected only the LAST non-empty
902
+ * line and broke: a torn tail line (a partial write) left the head at genesis
903
+ * (open) or stale (reanchor), so the next append forked a fresh chain from
904
+ * genesis — destroying all prior linkage. Walking back leaves the torn line
905
+ * as a single localized gap that verify() still flags, while the chain head
906
+ * stays anchored to the last good entry.
907
+ */
908
+ private lastHashOnDisk;
867
909
  private readAllEntries;
868
910
  private loadCumulativeStats;
869
911
  /**
@@ -916,6 +958,54 @@ interface AgentClassifierConfig {
916
958
  minDurationMs: number;
917
959
  }
918
960
 
961
+ interface SensitivityRule {
962
+ pattern: string;
963
+ sensitivity: number;
964
+ category: string;
965
+ description: string;
966
+ }
967
+ interface TargetSensitivityResult {
968
+ sensitivity: number;
969
+ category: string;
970
+ description: string;
971
+ matchedRule: string;
972
+ actionMultiplier: number;
973
+ effectiveScore: number;
974
+ groundedInRepoMap: boolean;
975
+ }
976
+ declare class TargetSensitivityScorer {
977
+ private rules;
978
+ private repoMap;
979
+ private repoRoot;
980
+ private overlay;
981
+ constructor(customRules?: SensitivityRule[], repoMap?: RepoSensitivityMap, repoRoot?: string, overlay?: SensitivityOverlay);
982
+ scoreTarget(target: string, action: string): TargetSensitivityResult;
983
+ /** Pattern-only scoring (Tier 2). Used directly by reject decisions. */
984
+ private _scoreByPatterns;
985
+ /**
986
+ * URL-aware component scoring (Sprint 6b W1 fix).
987
+ * Parses the URL, extracts path/query-values/fragment, decodes each once,
988
+ * scores each against forbidden-pattern rules, takes MAX.
989
+ */
990
+ private _scoreUrlTarget;
991
+ scoreEvent(event: AgentActivityEvent): TargetSensitivityResult;
992
+ /**
993
+ * Replaces the active repo sensitivity map. Pass null to clear.
994
+ * If newRepoRoot is provided, it overrides the map's repoRoot.
995
+ */
996
+ updateRepoMap(newMap: RepoSensitivityMap | null, newRepoRoot?: string): void;
997
+ /**
998
+ * Replaces the active sensitivity overlay. Pass null to clear.
999
+ */
1000
+ updateOverlay(newOverlay: SensitivityOverlay | null): void;
1001
+ /**
1002
+ * Returns all pattern-based rules that match the target. Does NOT
1003
+ * consult the repo map. Used by RepoSensitivityScanner during
1004
+ * scan-time enumeration.
1005
+ */
1006
+ getAllMatchingRules(target: string): SensitivityRule[];
1007
+ }
1008
+
919
1009
  /** Lazy-init wrapper passed via RoleValidatorOptions so Sentinel can own the cache. */
920
1010
  interface ForbiddenInodeCacheRef {
921
1011
  getOrBuild: () => Set<bigint>;
@@ -1229,6 +1319,7 @@ declare class SentinelRunner {
1229
1319
  private _hookEngine?;
1230
1320
  private _maturityConfig?;
1231
1321
  private _validatorOptions?;
1322
+ private _sensitivityScorer?;
1232
1323
  private intentTracker;
1233
1324
  private similarityEngine;
1234
1325
  private recentAlignmentScores;
@@ -1249,6 +1340,12 @@ declare class SentinelRunner {
1249
1340
  setMaturityConfig(config: Partial<BaselineMaturityConfig>): void;
1250
1341
  /** Set RoleValidator options (exception approval fn, audit callback). */
1251
1342
  setRoleValidatorOptions(options: RoleValidatorOptions): void;
1343
+ /**
1344
+ * Inject the facade's shared sensitivity scorer (overlay + repo map aware)
1345
+ * so processEvent's validator scores identically to the facade's check().
1346
+ * Must be called BEFORE start() builds the validator.
1347
+ */
1348
+ setSensitivityScorer(scorer: TargetSensitivityScorer): void;
1252
1349
  /** Set the behavioral baseline and start periodic gap checking. */
1253
1350
  setBaseline(baseline: AgentBaseline): void;
1254
1351
  /** Declare a new task intent for this agent. */
@@ -1265,6 +1362,12 @@ declare class SentinelRunner {
1265
1362
  getActiveTask(): TaskIntent | null;
1266
1363
  /** Expose the similarity engine for pre-execution intent checks. */
1267
1364
  getSimilarityEngine(): SimilarityEngine;
1365
+ /**
1366
+ * Read-only view of the rolling misaligned-score window, for the
1367
+ * pre-execution gate (Sentinel.check) to apply the SAME sustained-drift
1368
+ * severity upgrade as the recording path — without mutating the window.
1369
+ */
1370
+ getRecentAlignmentScores(): readonly number[];
1268
1371
  /**
1269
1372
  * Side-effect-free pre-execution gate: evaluate an event and fire pre_execution
1270
1373
  * hooks WITHOUT running processEvent's full pipeline.
@@ -16,6 +16,9 @@ var LogAdapter = class {
16
16
  pendingFragment = "";
17
17
  running = false;
18
18
  polling = false;
19
+ /** The in-flight poll (if any), so stop() can let it finish emitting its
20
+ * already-read batch before persisting the cursor. */
21
+ pollInFlight = null;
19
22
  pollTimer = null;
20
23
  onEvent = null;
21
24
  readExisting;
@@ -33,6 +36,7 @@ var LogAdapter = class {
33
36
  if (this.running) return;
34
37
  this.onEvent = onEvent;
35
38
  this.running = true;
39
+ this.pendingFragment = "";
36
40
  const savedPosition = await this.loadCursor();
37
41
  if (savedPosition !== null) {
38
42
  this.lastReadPosition = savedPosition;
@@ -47,7 +51,9 @@ var LogAdapter = class {
47
51
  } else {
48
52
  this.lastReadPosition = 0;
49
53
  }
50
- this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
54
+ this.pollTimer = setInterval(() => {
55
+ this.pollInFlight = this.poll();
56
+ }, this.pollIntervalMs);
51
57
  console.log(`LogAdapter watching ${this.logPath} for agent ${this.agentId}`);
52
58
  }
53
59
  async stop() {
@@ -55,6 +61,13 @@ var LogAdapter = class {
55
61
  clearInterval(this.pollTimer);
56
62
  this.pollTimer = null;
57
63
  }
64
+ if (this.pollInFlight) {
65
+ try {
66
+ await this.pollInFlight;
67
+ } catch {
68
+ }
69
+ this.pollInFlight = null;
70
+ }
58
71
  await this.saveCursor();
59
72
  this.running = false;
60
73
  this.onEvent = null;
@@ -66,7 +79,6 @@ var LogAdapter = class {
66
79
  try {
67
80
  const lines = await this.readNewLines();
68
81
  for (const line of lines) {
69
- if (!this.running) break;
70
82
  const event = this.parseLine(line);
71
83
  if (event) {
72
84
  try {
@@ -194,10 +206,9 @@ var LogAdapter = class {
194
206
  try {
195
207
  const { writeFile, mkdir } = await import("fs/promises");
196
208
  await mkdir(this.stateDir, { recursive: true });
197
- await writeFile(
198
- path,
199
- JSON.stringify({ position: this.lastReadPosition, logPath: this.logPath }) + "\n"
200
- );
209
+ const fragmentBytes = Buffer.byteLength(this.pendingFragment, "utf-8");
210
+ const position = Math.max(0, this.lastReadPosition - fragmentBytes);
211
+ await writeFile(path, JSON.stringify({ position, logPath: this.logPath }) + "\n");
201
212
  } catch (err) {
202
213
  console.warn(`Failed to save log cursor: ${err}`);
203
214
  }
@@ -235,4 +246,4 @@ var LogAdapter = class {
235
246
  export {
236
247
  LogAdapter
237
248
  };
238
- //# sourceMappingURL=chunk-PDWWRZXF.js.map
249
+ //# sourceMappingURL=chunk-2IPSTUNH.js.map
@@ -0,0 +1,47 @@
1
+ // src/setup/policyDiscovery.ts
2
+ import { existsSync, statSync } from "fs";
3
+ import { resolve, join, dirname } from "path";
4
+ function isTrustedPolicyStat(stats, processUid) {
5
+ if ((stats.mode & 2) !== 0) {
6
+ return { trusted: false, reason: "file is world-writable" };
7
+ }
8
+ if (processUid !== void 0 && stats.uid !== processUid) {
9
+ return {
10
+ trusted: false,
11
+ reason: `file is owned by uid ${stats.uid}, not the current user (uid ${processUid})`
12
+ };
13
+ }
14
+ return { trusted: true, reason: null };
15
+ }
16
+ function isTrustedOnDisk(candidate) {
17
+ if (process.platform === "win32") return true;
18
+ let stats;
19
+ try {
20
+ stats = statSync(candidate);
21
+ } catch {
22
+ return false;
23
+ }
24
+ const processUid = typeof process.getuid === "function" ? process.getuid() : void 0;
25
+ const verdict = isTrustedPolicyStat({ uid: stats.uid, mode: stats.mode }, processUid);
26
+ if (!verdict.trusted) {
27
+ console.warn(`[Sentinel] Ignoring untrusted policy file ${candidate}: ${verdict.reason}`);
28
+ }
29
+ return verdict.trusted;
30
+ }
31
+ function discoverPolicy(startDir, home) {
32
+ let dir = resolve(startDir);
33
+ const homeAbs = resolve(home);
34
+ while (true) {
35
+ const candidate = join(dir, ".sentinel.yaml");
36
+ if (existsSync(candidate) && isTrustedOnDisk(candidate)) return candidate;
37
+ if (dir === homeAbs) return null;
38
+ const parent = dirname(dir);
39
+ if (parent === dir) return null;
40
+ dir = parent;
41
+ }
42
+ }
43
+
44
+ export {
45
+ discoverPolicy
46
+ };
47
+ //# sourceMappingURL=chunk-B6S2PBS4.js.map