@tuent/sentinel 0.1.0

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,1785 @@
1
+ import { KeyObject } from 'node:crypto';
2
+
3
+ interface SkillNode {
4
+ id: string;
5
+ category: string;
6
+ label: string;
7
+ description: string;
8
+ weight: number;
9
+ recency: number;
10
+ lastActive: string;
11
+ color: string;
12
+ openness: number;
13
+ connections: string[];
14
+ layer: number;
15
+ source?: "seed" | "agent" | "manual" | "diary" | "conversation" | "agent-monitor";
16
+ core?: boolean;
17
+ sharable?: boolean;
18
+ files?: string[];
19
+ }
20
+
21
+ interface DataPoint {
22
+ label: string;
23
+ category: string;
24
+ lastActive: string;
25
+ description?: string;
26
+ weight?: number;
27
+ connections?: string[];
28
+ source?: "seed" | "agent" | "manual" | "diary" | "conversation" | "agent-monitor";
29
+ files?: string[];
30
+ sharable?: boolean;
31
+ core?: boolean;
32
+ }
33
+
34
+ interface StoredProfile {
35
+ version: 1;
36
+ id: string;
37
+ name: string;
38
+ dataPoints: DataPoint[];
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ }
42
+ interface StorageBackend {
43
+ read(): Promise<StoredProfile | null>;
44
+ write(profile: StoredProfile): Promise<void>;
45
+ /** Returns true if the backing store was modified externally since the last read/write. */
46
+ hasExternalChanges?(): Promise<boolean>;
47
+ }
48
+
49
+ interface DataUpdatedEvent {
50
+ type: "data:updated";
51
+ payload: {
52
+ timestamp: number;
53
+ };
54
+ }
55
+ interface NodeAddedEvent {
56
+ type: "petal:added";
57
+ payload: {
58
+ petalId: string;
59
+ category: string;
60
+ };
61
+ }
62
+ interface NodeDecayedEvent {
63
+ type: "petal:decayed";
64
+ payload: {
65
+ petalId: string;
66
+ };
67
+ }
68
+ interface ProfileGeneratedEvent {
69
+ type: "profile:generated";
70
+ payload: {
71
+ profileId: string;
72
+ };
73
+ }
74
+ type ProfileBusEvent = DataUpdatedEvent | NodeAddedEvent | NodeDecayedEvent | ProfileGeneratedEvent;
75
+ type EventMap = {
76
+ [E in ProfileBusEvent as E["type"]]: E;
77
+ };
78
+ type Listener<T extends ProfileBusEvent["type"]> = (event: EventMap[T]) => void;
79
+ declare class EventBus {
80
+ private listeners;
81
+ on<T extends ProfileBusEvent["type"]>(type: T, listener: Listener<T>): () => void;
82
+ emit<T extends ProfileBusEvent["type"]>(event: EventMap[T]): void;
83
+ clear(): void;
84
+ }
85
+
86
+ declare class ProfileStore {
87
+ private backend;
88
+ private eventBus?;
89
+ private petalCount;
90
+ private engine;
91
+ private profile;
92
+ constructor(options: {
93
+ backend: StorageBackend;
94
+ eventBus?: EventBus;
95
+ petalCount?: number;
96
+ });
97
+ load(): Promise<StoredProfile | null>;
98
+ create(name: string): Promise<StoredProfile>;
99
+ addPoint(point: DataPoint): Promise<void>;
100
+ addPoints(points: DataPoint[]): void;
101
+ save(): Promise<void>;
102
+ build(): SkillNode[];
103
+ getProfile(): StoredProfile | null;
104
+ }
105
+
106
+ /** What an AI agent does — a single observed action event. */
107
+ interface AgentActivityEvent {
108
+ agentId: string;
109
+ agentName: string;
110
+ agentRole: string;
111
+ action: "file_read" | "file_write" | "api_call" | "database_query" | "command_exec" | "tool_invocation" | "network_request";
112
+ /**
113
+ * All resources this action touches — the full target set.
114
+ *
115
+ * Replaces the pre-refactor singular `target: string` field.
116
+ * Each element is a path, URL, command, query, or other resource identifier.
117
+ * The array is non-empty: at minimum one target is always present.
118
+ *
119
+ * Examples:
120
+ * - file_read: ["/path/to/file"]
121
+ * - command_exec: ["git status", "/repo/.env"] (command + extracted paths)
122
+ * - tool_invocation: ["summarize project files"] (Task description)
123
+ * - network_request: ["https://example.com/api"] (URL)
124
+ * - file_write (NotebookEdit): ["/path/to/notebook.ipynb", "cell source content"]
125
+ *
126
+ * Policy evaluation checks every element against forbidden patterns.
127
+ * The matched element becomes SecurityFinding.evidence.target (singular).
128
+ */
129
+ targets: string[];
130
+ /**
131
+ * Derived display field — the principal target for human readability.
132
+ *
133
+ * Invariant: `primaryTarget === targets[0]`.
134
+ * Computed at event construction time (extractTarget), not stored
135
+ * independently in the audit log. Present for ergonomic access in
136
+ * code paths that need a single representative target (UI display,
137
+ * dedup keys, scope-narrowing checks, report summaries).
138
+ *
139
+ * For migration entries, primaryTarget is "schema:audit-log-v2".
140
+ */
141
+ primaryTarget: string;
142
+ /**
143
+ * Schema version marker. Value is 2 for all post-refactor entries.
144
+ *
145
+ * Pre-refactor entries lack this field; the migration script adds it
146
+ * during the one-time v1→v2 migration. Read-path code uses absence
147
+ * of this field (or value < 2) to identify legacy entries that need
148
+ * the v1→v2 normalization (target: string → targets: [string]).
149
+ */
150
+ schemaVersion: 2;
151
+ timestamp: string;
152
+ duration?: number;
153
+ metadata?: Record<string, string>;
154
+ authorized?: boolean;
155
+ /**
156
+ * Session identifier — system-managed by Sentinel.
157
+ *
158
+ * Generated automatically via `crypto.randomUUID()` on the first `wrap()` call
159
+ * for an agent. Reused across subsequent `wrap()` calls within the same session.
160
+ * Reset when `completeSession()` is called; the next `wrap()` generates a new ID.
161
+ *
162
+ * Optional at the type level (for external code constructing events manually),
163
+ * but Sentinel guarantees it is always populated at runtime on events reaching
164
+ * `execute()` and on audit trail entries.
165
+ *
166
+ * If a caller provides sessionId on the incoming event, Sentinel preserves it
167
+ * (does not overwrite).
168
+ */
169
+ sessionId?: string;
170
+ /**
171
+ * Human principal identifier — operator-supplied via `wrap()` options.
172
+ *
173
+ * Represents the human user who initiated the agent's operation. Not every
174
+ * operation has a human principal (system jobs, automated processes), so
175
+ * absence is valid — no auto-generation, no default.
176
+ */
177
+ userId?: string;
178
+ }
179
+ /** What an AI agent SHOULD do — its defined role and boundaries. */
180
+ interface AgentRole {
181
+ agentId: string;
182
+ name: string;
183
+ description: string;
184
+ allowedActions: string[];
185
+ allowedTargetPatterns: string[];
186
+ forbiddenTargetPatterns: string[];
187
+ expectedSchedule?: {
188
+ activeDays?: string[];
189
+ activeHours?: [number, number];
190
+ };
191
+ maxEventsPerHour?: number;
192
+ maxSessionDuration?: number;
193
+ /** Scoped exceptions to forbidden target patterns. */
194
+ exceptions?: RoleException[];
195
+ /**
196
+ * Host allowlist for network_request targets (Sprint 6b W3c).
197
+ * Lowercase, trimmed, trailing-dot-stripped on load.
198
+ * When present, URL-parsed hosts matching an entry suppress MEDIUM scope_violation.
199
+ * CRITICAL findings (scheme/CIDR/credential-tier) are NOT overridden.
200
+ */
201
+ networkHosts?: string[];
202
+ }
203
+ /**
204
+ * A scoped exception to a forbidden target pattern.
205
+ *
206
+ * When a forbidden pattern matches, the exception check runs AFTER an
207
+ * independent sensitivity score gate: if `effectiveScore >= 0.9` (CRITICAL
208
+ * credentials/system/pii), the exception path is skipped entirely and the
209
+ * finding is returned unchanged. Exceptions can never downgrade CRITICAL
210
+ * findings.
211
+ */
212
+ interface RoleException {
213
+ /** Glob pattern matching the target this exception covers. */
214
+ target: string;
215
+ /** Which actions this exception permits for the matched target. */
216
+ allowedActions: AgentActivityEvent["action"][];
217
+ /** If set, active task description must contain this keyword (case-insensitive). */
218
+ requiresTask?: string;
219
+ /** If true, the exception must be explicitly approved via approval callback. */
220
+ requiresApproval?: boolean;
221
+ /** If set, exception is valid for this many ms after the active task started. */
222
+ expiresAfter?: number;
223
+ /** Kind to assign when the exception fires. Default: "informational". */
224
+ downgradeKindTo?: "informational" | "actionable";
225
+ }
226
+ /** Context passed to the exception approval callback. */
227
+ interface ExceptionApprovalContext {
228
+ finding: SecurityFinding;
229
+ exception: RoleException;
230
+ activeTask: TaskIntent | null;
231
+ expiresAt: Date | null;
232
+ }
233
+ /** Approval function for exceptions that require explicit approval. */
234
+ type ExceptionApprovalFn = (ctx: ExceptionApprovalContext) => Promise<boolean> | boolean;
235
+ /**
236
+ * A detected anomaly from behavioral analysis.
237
+ *
238
+ * The `kind` field distinguishes between two semantic categories:
239
+ *
240
+ * - `"informational"` — behavioral observations that qualify context but do not
241
+ * indicate a boundary violation on their own. Typical types: volume_spike,
242
+ * access_pattern, temporal_anomaly (from deviation/schedule checks),
243
+ * behavioral_absence, intent_drift, and target_sensitivity findings with
244
+ * effectiveScore < 0.8.
245
+ *
246
+ * - `"actionable"` — boundary violations that represent a concrete policy breach
247
+ * requiring operator attention. Typical types: role_violation, unauthorized_target,
248
+ * scope_violation, agent_quarantined, agent_restricted, hook_block, and
249
+ * target_sensitivity findings with effectiveScore >= 0.8.
250
+ */
251
+ interface SecurityFinding {
252
+ severity: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
253
+ kind: "informational" | "actionable";
254
+ /**
255
+ * Finding type. `workspace_mismatch` (a gateway routing refusal — a request
256
+ * from a different workspace than the daemon was launched to serve) is
257
+ * intentionally NOT in `ESCALATION_ELIGIBLE_TYPES`: it denies the action but
258
+ * must not count toward the served agent's escalation ladder, because it is a
259
+ * routing error, not that agent's misbehavior. Same non-eligible posture as
260
+ * `bash_analysis`. Escalation-ineligibility is a consequence of what the type
261
+ * means, not a knob — see Sentinel.ESCALATION_ELIGIBLE_TYPES.
262
+ */
263
+ type: "scope_violation" | "temporal_anomaly" | "access_pattern" | "volume_spike" | "unauthorized_target" | "role_violation" | "behavioral_absence" | "agent_quarantined" | "agent_restricted" | "intent_drift" | "hook_block" | "bash_analysis" | "workspace_mismatch";
264
+ agentId: string;
265
+ agentName: string;
266
+ description: string;
267
+ evidence: {
268
+ action: string;
269
+ target: string;
270
+ timestamp: string;
271
+ baselineComparison?: string;
272
+ };
273
+ recommendation: string;
274
+ timestamp: string;
275
+ decision?: "allow" | "deny" | "modify";
276
+ modification?: {
277
+ type: "append_args";
278
+ args: string[];
279
+ };
280
+ softSignal?: boolean;
281
+ }
282
+ /** Computed behavioral baseline for an agent over a time window. */
283
+ /**
284
+ * Statistical profile of an agent's historical behavior, computed from observed
285
+ * sessions. Used by DeviationDetector to identify anomalies.
286
+ *
287
+ * Baseline maturity tiers:
288
+ * - **empty** (sessionCount === 0): No data observed. All deviation checks are
289
+ * suppressed except target sensitivity (which doesn't use baseline data).
290
+ * - **immature** (sessionCount > 0 but below configured thresholds): Not enough
291
+ * data for reliable statistical deviation. Volume spike, access pattern,
292
+ * category shift, temporal anomaly, and weight anomaly are suppressed.
293
+ * Behavioral absence and target sensitivity still fire.
294
+ * - **mature** (meets all thresholds): All checks fire normally.
295
+ */
296
+ interface AgentBaseline {
297
+ agentId: string;
298
+ computedAt: string;
299
+ periodDays: number;
300
+ totalSessions: number;
301
+ totalEvents: number;
302
+ actionDistribution: Record<string, number>;
303
+ topTargets: string[];
304
+ averageEventsPerSession: number;
305
+ averageSessionDurationMinutes: number;
306
+ typicalActiveHours: [number, number];
307
+ typicalActiveDays: string[];
308
+ normalWeightRange: [number, number];
309
+ /** Total sessions observed (used for maturity gating). */
310
+ sessionCount: number;
311
+ /** Calendar span in days between earliest and latest observed node. */
312
+ daysObserved: number;
313
+ /** Count of distinct activity categories observed. */
314
+ categoryDiversity: number;
315
+ }
316
+ /** Thresholds for baseline maturity tiers. All three must be met for "mature". */
317
+ interface BaselineMaturityConfig {
318
+ minSessions: number;
319
+ minDaysObserved: number;
320
+ minCategoryDiversity: number;
321
+ }
322
+ /** Adapter configuration for connecting a SentinelRunner to a data source. */
323
+ interface AdapterConfig {
324
+ type: "log" | "webhook" | "mcp" | "manual";
325
+ logPath?: string;
326
+ logFormat?: "json-lines" | "csv";
327
+ fieldMapping?: Record<string, string>;
328
+ mcpLogDir?: string;
329
+ webhookPort?: number;
330
+ webhookApiKey?: string;
331
+ pollIntervalMs?: number;
332
+ /** When true, process existing log content on start instead of tailing from end. */
333
+ readExisting?: boolean;
334
+ }
335
+ /** Sentinel monitoring configuration. */
336
+ interface SentinelConfig {
337
+ agents: {
338
+ agentId: string;
339
+ name: string;
340
+ adapterType: "log" | "webhook" | "mcp" | "manual";
341
+ logPath?: string;
342
+ logFormat?: "json-lines" | "csv";
343
+ fieldMapping?: Record<string, string>;
344
+ mcpLogDir?: string;
345
+ webhookPort?: number;
346
+ webhookApiKey?: string;
347
+ roleDefinitionPath?: string;
348
+ readExisting?: boolean;
349
+ }[];
350
+ alerts?: {
351
+ minSeverity?: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
352
+ minKind?: "informational" | "actionable";
353
+ dedupeWindowMs?: number;
354
+ quietHoursEnabled?: boolean;
355
+ quietHours?: [number, number];
356
+ channels?: {
357
+ type: "console" | "webhook" | "file";
358
+ name: string;
359
+ config: {
360
+ url?: string;
361
+ headers?: Record<string, string>;
362
+ filePath?: string;
363
+ };
364
+ minKind?: "informational" | "actionable";
365
+ }[];
366
+ };
367
+ alertWebhook?: string;
368
+ baselineWindowDays?: number;
369
+ checkIntervalMs?: number;
370
+ }
371
+ /** A declared task/intent for an agent. */
372
+ interface TaskIntent {
373
+ taskId: string;
374
+ agentId: string;
375
+ description: string;
376
+ keywords: string[];
377
+ acceptableActions: AcceptableAction[];
378
+ startedAt: string;
379
+ ttlMs: number;
380
+ status: "active" | "completed" | "expired";
381
+ completedAt?: string;
382
+ relaxedActions?: AgentActivityEvent["action"][];
383
+ phases?: string[];
384
+ /**
385
+ * Sprint 23 P2 — per-session SCOPE narrowing globs (from a leading `SCOPE:`
386
+ * prompt line). When present, the effective allowed-target set is narrowed to
387
+ * `base ∩ scope`: a target allowed by the base role but matching NONE of these
388
+ * globs is a hard, ladder-eligible violation (RoleValidator Check 3.5). This is
389
+ * a pure NARROWING — it only ever subtracts from the base allowed set and can
390
+ * never widen it (the base allow check runs first and denies out-of-base
391
+ * targets before scope is consulted). The narrowing is session-scoped: it lives
392
+ * on the active task and expires/clears exactly when the task does (TTL,
393
+ * completion, or supersede by a later declaration). Distinct tier from the
394
+ * fuzzy P1 `INTENT:` semantic drift, which stays soft/advisory.
395
+ */
396
+ scopePatterns?: string[];
397
+ }
398
+ /** An action+target pattern considered on-task. */
399
+ interface AcceptableAction {
400
+ action: AgentActivityEvent["action"];
401
+ targetPattern: string;
402
+ }
403
+ /** Result of checking an event against the active task intent. */
404
+ interface IntentAlignmentResult {
405
+ aligned: boolean;
406
+ score: number;
407
+ taskId: string | null;
408
+ reason: string;
409
+ keywordMatches: string[];
410
+ acceptableActionMatch: boolean;
411
+ bypassed?: boolean;
412
+ bypassReason?: string | null;
413
+ }
414
+ /** Configuration for intent alignment scoring. */
415
+ interface IntentAlignmentConfig {
416
+ defaultTtlMs: number;
417
+ keywordBoost: number;
418
+ acceptableActionBoost: number;
419
+ baseScore: number;
420
+ missingTaskScore: number;
421
+ disableDefaultAcceptable?: boolean;
422
+ denyAcceptable?: string[];
423
+ similarityThreshold?: number;
424
+ }
425
+ /** A single file entry in a repo sensitivity scan. */
426
+ interface RepoSensitivityEntry {
427
+ path: string;
428
+ sensitivity: number;
429
+ category: string;
430
+ confidence: "low" | "medium" | "high";
431
+ matchedRules: string[];
432
+ }
433
+ /** Aggregate statistics for a repo sensitivity scan. */
434
+ interface RepoSensitivitySummary {
435
+ totalFiles: number;
436
+ sensitiveFiles: number;
437
+ byCategory: Record<string, number>;
438
+ byConfidence: Record<"low" | "medium" | "high", number>;
439
+ }
440
+ /** Complete result of scanning a repository for sensitive files. */
441
+ interface RepoSensitivityMap {
442
+ scannedAt: string;
443
+ repoRoot: string;
444
+ entries: RepoSensitivityEntry[];
445
+ summary: RepoSensitivitySummary;
446
+ }
447
+ /** The three operator decision types for overlay entries. */
448
+ type OverlayDecisionType = "reject" | "accept" | "override";
449
+ /**
450
+ * A single operator-controlled overlay decision for a file path.
451
+ *
452
+ * - `reject`: false positive — fall through to pattern matching (skip map entry)
453
+ * - `accept`: informational confirmation — map entry used as-is
454
+ * - `override`: operator supplies custom sensitivity/category
455
+ */
456
+ interface OverlayDecision {
457
+ path: string;
458
+ decision: OverlayDecisionType;
459
+ /** Operator-provided note (e.g., "this is a tutorial file, not a real key"). */
460
+ reason?: string;
461
+ /** Custom sensitivity (only meaningful for "override" decisions). */
462
+ sensitivity?: number;
463
+ /** Custom category (only meaningful for "override" decisions). */
464
+ category?: string;
465
+ /** ISO timestamp when the decision was made. */
466
+ decidedAt: string;
467
+ }
468
+ /** Persisted overlay file: operator decisions layered on top of the auto map. */
469
+ interface SensitivityOverlay {
470
+ version: "1.0";
471
+ repoRoot: string;
472
+ decisions: OverlayDecision[];
473
+ updatedAt: string;
474
+ }
475
+
476
+ interface AuditQueryOptions {
477
+ type?: "event" | "finding" | "session" | "baseline" | "mode_change" | "intent_start" | "intent_end" | "intent_check" | "exception_applied" | "exception_approved" | "elevated_scrutiny";
478
+ startTime?: string;
479
+ endTime?: string;
480
+ severity?: string;
481
+ limit?: number;
482
+ }
483
+ interface AuditEntry {
484
+ type: string;
485
+ timestamp: string;
486
+ agentId: string;
487
+ /** Session identifier — injected automatically by AuditTrail when session context is set. */
488
+ sessionId?: string;
489
+ /** Human principal identifier — injected automatically when set via session context. */
490
+ userId?: string;
491
+ /** Ed25519 signature (base64) of the entry payload. Present when audit trail signing is enabled. */
492
+ signature?: string;
493
+ /** Ed25519 public key (base64, raw 32 bytes) of the signer. For offline verification. */
494
+ signerPublicKey?: string;
495
+ [key: string]: unknown;
496
+ }
497
+ interface AuditStats {
498
+ totalEntries: number;
499
+ eventCount: number;
500
+ findingCount: number;
501
+ sessionCount: number;
502
+ baselineCount: number;
503
+ intentStartCount: number;
504
+ intentEndCount: number;
505
+ intentCheckCount: number;
506
+ intentDriftCount: number;
507
+ averageAlignmentScore: number | null;
508
+ oldestEntry: string | null;
509
+ newestEntry: string | null;
510
+ fileSizeBytes: number;
511
+ findingsBySeverity: Record<string, number>;
512
+ }
513
+ /**
514
+ * S21-P1 — signed, hash-chained file-level manifest.
515
+ *
516
+ * The per-entry hash chain resets to genesis at each rotated file, so deleting a
517
+ * WHOLE rotated file is undetectable by the entry chain alone. The manifest adds
518
+ * a layer ABOVE the signed entry chain (it never mutates an entry): one record
519
+ * per audit file, recording the file's last-entry hash + entry count. The
520
+ * records are themselves Ed25519-signed (same key as the trail) and hash-chained
521
+ * (genesis-rooted, identical discipline to entries), so removing or editing a
522
+ * record breaks the manifest's own chain/signature.
523
+ */
524
+ interface ManifestIssue {
525
+ /** MANIFEST_FILE_MISSING (dangling record → file deleted), MANIFEST_HASH_MISMATCH
526
+ * (present file truncated/replaced), MANIFEST_TAMPERED (manifest's own chain or
527
+ * signature broken — record removed/edited/re-signed with a foreign key). */
528
+ type: "MANIFEST_FILE_MISSING" | "MANIFEST_HASH_MISMATCH" | "MANIFEST_TAMPERED";
529
+ file?: string;
530
+ detail: string;
531
+ }
532
+ /**
533
+ * S21-P5 — disclosure of a VALID signed checkpoint. When present, verify()
534
+ * certifies the chain from the anchor FORWARD (the clean tail) and surfaces this
535
+ * record so the certified verdict is VALID-WITH-DISCLOSURE, never a silent VALID.
536
+ * The pre-checkpoint history is retained on disk and disclosed here (count +
537
+ * enumeration reference), never rewritten.
538
+ */
539
+ interface ManifestCheckpoint {
540
+ /** Audit file (basename) the anchor lives in, e.g. "audit.log". */
541
+ file: string;
542
+ /** 1-based line of the anchor entry within that file. */
543
+ line: number;
544
+ /** Timestamp of the anchor entry (the first certified entry). */
545
+ anchorTimestamp: string;
546
+ /** Why this trail is checkpointed (documented dev-era contamination, characterized). */
547
+ reason: string;
548
+ /** Reference to the forensic enumeration that characterized the breaks. */
549
+ enumerationRef: string;
550
+ /** Documented benign linkage breaks BEFORE the anchor (disclosed, not failed). */
551
+ preCheckpointBreaks: number;
552
+ /** Tampering found by the enumeration (the honest claim — expected 0). */
553
+ tamperingCount: number;
554
+ /** Human-readable VALID-with-disclosure line surfaced in verify output. */
555
+ disclosure: string;
556
+ }
557
+ interface ManifestCheck {
558
+ ok: boolean;
559
+ recordCount: number;
560
+ issues: ManifestIssue[];
561
+ /** S21-P2: files excluded from the pass/fail verdict by a VALID signed
562
+ * quarantine record (disclosure — VALID-with-disclosure, not VALID-by-hiding). */
563
+ quarantined: {
564
+ file: string;
565
+ reason: string;
566
+ }[];
567
+ /** S21-P5: present when a VALID signed checkpoint scopes the certified verdict
568
+ * to the post-anchor tail. Disclosure (not suppression) — the pre-checkpoint
569
+ * scope is visibly reported, the history retained on disk. */
570
+ checkpoint?: ManifestCheckpoint;
571
+ }
572
+ /**
573
+ * S21-P4 — the derived cumulative-stats.json counter (display-only; no security
574
+ * decision depends on it — enforcement uses getEffectiveBlockCount, baseline uses
575
+ * audit-log queries).
576
+ */
577
+ interface CumulativeStatsFile {
578
+ totalEntries: number;
579
+ eventCount: number;
580
+ findingCount: number;
581
+ sessionCount: number;
582
+ baselineCount: number;
583
+ findingsBySeverity: Record<string, number>;
584
+ /** S21-P4: what totalEntries honestly counts — RETAINED entries (current +
585
+ * rotated archives .1–.3 + cold-archive); EXCLUDES manual pre-* backup
586
+ * snapshots (which duplicate historical content) and any cold files pruned
587
+ * before a recompute. Maintained monotonically per write (rotation-durable). */
588
+ countScope?: string;
589
+ }
590
+ declare class AuditTrail {
591
+ private static readonly GENESIS_HASH;
592
+ /** S21-P4: honest label for cumulative-stats.json's totalEntries. */
593
+ private static readonly STATS_SCOPE;
594
+ private agentId;
595
+ private logPath;
596
+ private statsPath;
597
+ private manifestPath;
598
+ private manifestHeadPath;
599
+ private coldDir;
600
+ /** Fix D: in-process write serialization. Every writeLine chains onto this so
601
+ * concurrent calls on one instance can't interleave their read-tail + append. */
602
+ private _writeChain;
603
+ /** Fix D: byte size of logPath right after THIS instance's last append. The
604
+ * reanchor guard compares against it: equal ⇒ we are still the sole writer
605
+ * (no-op, byte-identical hot path); changed ⇒ another instance appended ⇒
606
+ * re-read the on-disk tail before chaining. Null until first known. */
607
+ private _lastKnownSize;
608
+ private _coldCounter;
609
+ /** S21-P3b: set on rotation to the rotated file's last hash; stamped as the
610
+ * next entry's signed `chainPrev` (cross-file link), then cleared. */
611
+ private _chainLinkPending;
612
+ private opened;
613
+ private maxFileSizeBytes;
614
+ private currentChainHead;
615
+ private _sessionId?;
616
+ private _userId?;
617
+ private _signingPrivateKey?;
618
+ private _signingPublicKeyB64?;
619
+ constructor(agentId: string, options?: {
620
+ logDir?: string;
621
+ maxFileSizeMB?: number;
622
+ });
623
+ open(): Promise<void>;
624
+ /**
625
+ * Set the current session context for this audit trail.
626
+ * All subsequent log entries will include these values automatically.
627
+ * Call with undefined to clear (e.g., after completeSession).
628
+ *
629
+ * Note: sessionId is persisted on entries; query/filter by sessionId is future work.
630
+ */
631
+ setSessionContext(sessionId?: string, userId?: string): void;
632
+ /**
633
+ * Enable Ed25519 signing on all subsequent audit entries (AARM R5).
634
+ * Once set, every entry written via writeLine() will include `signature`
635
+ * and `signerPublicKey` fields. Call before writing entries.
636
+ */
637
+ setSigningKey(privateKey: KeyObject, publicKey: KeyObject): void;
638
+ logEvent(event: AgentActivityEvent): Promise<void>;
639
+ logFinding(finding: SecurityFinding): Promise<void>;
640
+ /**
641
+ * Log an agent-level ELEVATED-SCRUTINY advisory flag (the soft-signal
642
+ * consequence of converging behavioral deviation).
643
+ *
644
+ * This is advisory metadata, NOT an enforcement state. It is written as its
645
+ * own audit entry type ("elevated_scrutiny"), never as a "finding" and never
646
+ * as a "mode_change", so it can never be picked up by getEffectiveBlockCount
647
+ * (which counts only eligible findings) or perturb the restrict/quarantine
648
+ * ladder. It is read back by the report/fleet surfaces and self-clears: the
649
+ * flag is "active" only while now < expiresAt (a rolling decay window). A
650
+ * fresh convergence re-raises and extends the window; absence of convergence
651
+ * lets it lapse on its own.
652
+ */
653
+ logElevatedScrutiny(reason: string, windowMs: number): Promise<void>;
654
+ logSessionSummary(dataPoint: DataPoint): Promise<void>;
655
+ logModeChange(mode: string, reason: string, previousMode: string): Promise<void>;
656
+ logBaselineComputed(baseline: AgentBaseline): Promise<void>;
657
+ logIntentStart(taskIntent: TaskIntent): Promise<void>;
658
+ logIntentEnd(agentId: string, taskId: string): Promise<void>;
659
+ logIntentCheck(result: IntentAlignmentResult, event: AgentActivityEvent): Promise<void>;
660
+ query(options?: AuditQueryOptions): Promise<AuditEntry[]>;
661
+ getStats(): Promise<AuditStats>;
662
+ /**
663
+ * Verify audit trail integrity: hash chain AND Ed25519 signatures.
664
+ *
665
+ * Single-pass loop per file. For each entry:
666
+ * 1. Signature check (if signed) — content integrity + non-repudiation
667
+ * 2. Hash chain check — ordering + completeness
668
+ *
669
+ * Signature-first ordering surfaces content tampering before sequencing
670
+ * tampering for cleaner error reporting.
671
+ *
672
+ * Unsigned entries (pre-signing or signing disabled) emit warnings but
673
+ * do not fail verification (backward compatibility via lazy defaulting).
674
+ *
675
+ * S21-P5: when a VALID signed checkpoint exists, verify certifies the chain
676
+ * from the checkpoint anchor FORWARD (the clean tail) and DISCLOSES the
677
+ * retained pre-checkpoint history (manifest.checkpoint). Pass
678
+ * `{ fullHistory: true }` to ignore the checkpoint scoping and verify the
679
+ * entire history (which reports the documented pre-checkpoint breaks). A trail
680
+ * with no checkpoint verifies byte-identically regardless of the flag.
681
+ */
682
+ verify(options?: {
683
+ fullHistory?: boolean;
684
+ }): Promise<{
685
+ valid: boolean;
686
+ totalEntries: number;
687
+ brokenAt?: number;
688
+ signedEntries: number;
689
+ unsignedEntries: number;
690
+ invalidSignatures: number;
691
+ /** S21-P1: present ONLY when an audit.manifest exists for this trail. Absent
692
+ * (and behavior byte-identical to pre-S21-P1) for manifest-less trails. */
693
+ manifest?: ManifestCheck;
694
+ }>;
695
+ /** Read-only fingerprint of one audit file. Returns null if absent/empty. */
696
+ private fingerprintFile;
697
+ /** Hash of the entry at a 1-based line position (counting non-empty lines),
698
+ * or null if absent/unparseable. Used by checkManifest's live-file append
699
+ * tolerance to confirm the manifested boundary entry is still in place.
700
+ * Read-only. */
701
+ private hashAtLine;
702
+ /** Present audit files, OLDEST→NEWEST: highest archive (.N) … .1, then current.
703
+ * Oldest-first ordering puts archives in non-last manifest positions (so
704
+ * scrubbing an archive's record breaks the manifest chain) and the live
705
+ * current file last. Read-only. */
706
+ private listAuditFiles;
707
+ /** The public key the trail's entries are signed with (binds the manifest to
708
+ * the same key — a manifest re-signed with a foreign key is rejected). Reads
709
+ * the newest signed entry of the current file. Read-only. */
710
+ private trailSignerKey;
711
+ /**
712
+ * Enroll (or re-enroll) the signed manifest for this trail. READ-ONLY over the
713
+ * audit files — it hashes each file in place and writes ONLY audit.manifest;
714
+ * no audit entry is read-modified or re-signed. Requires a signing key (same
715
+ * key discipline as entries). Records are genesis-rooted hash-chained + signed,
716
+ * mirroring the entry chain exactly (sign content → hash covers content+sig).
717
+ *
718
+ * Honest scope: protects from enrollment FORWARD. Pre-enrollment deletions
719
+ * cannot be proven retroactively (there was no manifest to dangle).
720
+ */
721
+ enrollManifest(options?: {
722
+ /** S21-P2: files to mark quarantined (excluded from the verify verdict but
723
+ * RETAINED on disk and disclosed). Each must name a real, documented reason.
724
+ * Operational — the caller decides what/why; never auto-populated. */
725
+ quarantine?: {
726
+ file: string;
727
+ reason: string;
728
+ }[];
729
+ /** S21-P5: a checkpoint anchoring verify() to a documented entry FORWARD. The
730
+ * anchor binds to a SPECIFIC entry (file + 1-based line + that entry's hash);
731
+ * verify() then certifies the post-anchor tail while DISCLOSING the retained
732
+ * pre-checkpoint history. anchorHash/anchorTimestamp are derived from the file
733
+ * at enroll time unless provided (the auto-re-enroll path passes them through
734
+ * verbatim so a rotation re-enroll never re-derives a moved anchor). */
735
+ checkpoint?: {
736
+ file: string;
737
+ line: number;
738
+ reason: string;
739
+ enumerationRef: string;
740
+ preCheckpointBreaks: number;
741
+ tamperingCount: number;
742
+ anchorHash?: string;
743
+ anchorTimestamp?: string;
744
+ };
745
+ }): Promise<{
746
+ records: number;
747
+ quarantined: number;
748
+ checkpointed: number;
749
+ }>;
750
+ /**
751
+ * S21-P3 (II) — verify the head anchor for an anchored manifest. Returns a
752
+ * tamper issue if the anchor is missing, foreign-signed, signature-invalid, or
753
+ * its (manifestHead, recordCount) doesn't match the manifest's actual head.
754
+ * Read-only.
755
+ */
756
+ private verifyHeadAnchor;
757
+ /**
758
+ * Cross-check the manifest against the on-disk audit files. Returns null when
759
+ * NO manifest exists (caller then preserves pre-S21-P1 behavior). Detects:
760
+ * - MANIFEST_TAMPERED — manifest's own chain/signature broken, a record
761
+ * removed/reordered/edited, or re-signed with a key
762
+ * other than the trail's entry-signing key.
763
+ * - MANIFEST_FILE_MISSING — a record whose file is absent (whole file deleted).
764
+ * - MANIFEST_HASH_MISMATCH — a present file whose last-hash/count differs from
765
+ * its record (file truncated/replaced).
766
+ * Read-only.
767
+ */
768
+ /**
769
+ * Phase 1 — verify the manifest's OWN integrity: per-record Ed25519 signature
770
+ * bound to the trail's entry-signing key + genesis-rooted hash-chain. Returns
771
+ * the parsed records and an integrity verdict; null when no manifest exists.
772
+ * Shared by checkManifest() and trustedQuarantineSet() so the quarantine-skip
773
+ * decision and the reported verdict use IDENTICAL integrity gating. Read-only.
774
+ */
775
+ private verifyManifestIntegrity;
776
+ /**
777
+ * S21-P2 — the set of files quarantined by a VALID manifest. Empty unless the
778
+ * manifest is present AND fully integrity-valid: a tampered/forged/removed
779
+ * manifest record grants NO quarantine (fail-closed), so a quarantine can
780
+ * never be a silent escape hatch. Read-only.
781
+ */
782
+ private trustedQuarantineSet;
783
+ /**
784
+ * S21-P5 — the trusted checkpoint, IF the manifest is present AND fully
785
+ * integrity-valid (identical fail-closed gating to trustedQuarantineSet). A
786
+ * forged (foreign-key), moved (anchorHash/line are signed fields), or removed
787
+ * (chain/head-anchor break) checkpoint fails integrity → returns null → verify
788
+ * falls back to the full strict walk and the pre-checkpoint breaks resurface.
789
+ * So a checkpoint can never be a silent masking hatch. Read-only.
790
+ */
791
+ private trustedCheckpoint;
792
+ /**
793
+ * Cross-check the manifest against the on-disk audit files. Returns null when
794
+ * NO manifest exists (caller preserves pre-S21-P1 behavior). Detects:
795
+ * - MANIFEST_TAMPERED — manifest's own chain/signature broken, a record
796
+ * removed/reordered/edited, or re-signed with a key
797
+ * other than the trail's entry-signing key.
798
+ * - MANIFEST_FILE_MISSING — a record whose file is absent (whole file deleted).
799
+ * - MANIFEST_HASH_MISMATCH — a present file whose last-hash/count differs from
800
+ * its record (truncated/replaced).
801
+ * Also surfaces quarantined files (disclosure). Read-only.
802
+ */
803
+ private checkManifest;
804
+ /** Write an arbitrary audit entry. Used by HookEngine and other subsystems. */
805
+ logEntry(entry: AuditEntry): Promise<void>;
806
+ close(): Promise<void>;
807
+ private rotateIfNeeded;
808
+ /**
809
+ * S21-P3 (2) — keep the manifest current across a rotation. After a rotation
810
+ * shifts filenames, re-enroll the manifest (re-fingerprint the new layout +
811
+ * update the head anchor), PRESERVING existing quarantine records. No-op if no
812
+ * manifest is enrolled (forward-only — trails without a manifest are untouched)
813
+ * or if no signing key is set.
814
+ */
815
+ private autoUpdateManifestAfterRotation;
816
+ private computeHash;
817
+ private writeLine;
818
+ private _writeLineCritical;
819
+ /**
820
+ * Fix D — re-anchor the in-memory chain head to the ACTUAL on-disk tail.
821
+ *
822
+ * `currentChainHead` is in-memory and advanced per write; a concurrent or
823
+ * out-of-process AuditTrail instance (e.g. the CLI mode-change commands)
824
+ * appending to the same file does NOT update it. This re-reads the on-disk
825
+ * tail (identical "last entry hash" logic as open()) so the next entry chains
826
+ * THROUGH whatever was actually appended last, rather than to a stale
827
+ * predecessor. Caller gates this on a detected size change, so it only reads
828
+ * the file when an external append actually happened.
829
+ */
830
+ private reanchorFromDiskTail;
831
+ private readAllEntries;
832
+ private loadCumulativeStats;
833
+ /**
834
+ * S21-P4: derive cumulative stats from the entries actually retained on disk —
835
+ * the hot window (current + rotated .1–.3, via readAllEntries) PLUS the
836
+ * cold-archive (cold/), which is out of the hot window but still retained.
837
+ * EXCLUDES manual pre-* backup snapshots (they duplicate historical content and
838
+ * would double-count). Read-only over the trail; touches no signed entry.
839
+ */
840
+ private deriveStatsFromDisk;
841
+ /**
842
+ * S21-P4: one-time correction — regenerate cumulative-stats.json from the
843
+ * entries actually retained on disk (hot window + cold-archive). Use when the
844
+ * counter is known out of sync (e.g. the Sprint-18 migration reset). Returns
845
+ * the recomputed stats. Read-only over the signed trail.
846
+ */
847
+ recomputeCumulativeStats(): Promise<CumulativeStatsFile>;
848
+ /** Bootstrap cumulative stats when no stats file exists. S21-P4: seeds from ALL
849
+ * retained entries (hot window + cold-archive) so a re-seed after stats-file
850
+ * loss doesn't undercount the way the pre-P4 hot-window-only seed did. */
851
+ private bootstrapCumulativeStats;
852
+ private incrementCumulativeStats;
853
+ private parseEventCount;
854
+ }
855
+
856
+ declare class AgentProfileManager {
857
+ private agentsDir;
858
+ constructor(agentsDir?: string);
859
+ getAgentProfilePath(agentId: string): string;
860
+ getRolePath(agentId: string): string;
861
+ createAgent(agentId: string, name: string, role?: AgentRole): Promise<ProfileStore>;
862
+ loadAgentProfile(agentId: string): Promise<ProfileStore>;
863
+ saveRole(agentId: string, role: AgentRole): Promise<void>;
864
+ loadRole(agentId: string): Promise<AgentRole | null>;
865
+ listAgents(): Promise<string[]>;
866
+ agentExists(agentId: string): Promise<boolean>;
867
+ hasBaseline(agentId: string): Promise<boolean>;
868
+ }
869
+
870
+ interface ReportOptions {
871
+ periodDays: number;
872
+ format: "markdown" | "json";
873
+ includeAllEvents: boolean;
874
+ includeRecommendations: boolean;
875
+ }
876
+
877
+ interface AgentClassifierConfig {
878
+ sessionTimeoutMs: number;
879
+ minEvents: number;
880
+ minDurationMs: number;
881
+ }
882
+
883
+ /** Lazy-init wrapper passed via RoleValidatorOptions so Sentinel can own the cache. */
884
+ interface ForbiddenInodeCacheRef {
885
+ getOrBuild: () => Set<bigint>;
886
+ }
887
+ /** Options for RoleValidator exception handling. */
888
+ interface RoleValidatorOptions {
889
+ /** Synchronous approval callback for exceptions with requiresApproval. */
890
+ approvalFn?: (ctx: ExceptionApprovalContext) => boolean;
891
+ /** Synchronous callback invoked when an exception audit entry is produced. */
892
+ onAuditEntry?: (entry: AuditEntry) => void;
893
+ /** Session-scoped forbidden-inode cache for hardlink detection (Sprint 9). */
894
+ forbiddenInodeCache?: ForbiddenInodeCacheRef;
895
+ /**
896
+ * Absolute workspace root used to anchor bare allowed patterns in Check 3
897
+ * (Approach 1). Derived upstream as repo.root ?? dirname(policyPath) and
898
+ * threaded fromPolicy → monitor → here. Empty string disables anchoring
899
+ * (patterns matched as-authored — the documented fallback).
900
+ */
901
+ workspaceRoot?: string;
902
+ }
903
+
904
+ interface AlertChannel {
905
+ type: "console" | "webhook" | "file";
906
+ name: string;
907
+ config: {
908
+ url?: string;
909
+ headers?: Record<string, string>;
910
+ filePath?: string;
911
+ };
912
+ /** Per-channel minimum kind threshold. Overrides top-level minKind when set. */
913
+ minKind?: "informational" | "actionable";
914
+ }
915
+ interface AlertConfig {
916
+ channels: AlertChannel[];
917
+ minSeverity: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
918
+ dedupeWindowMs: number;
919
+ quietHoursEnabled: boolean;
920
+ quietHours?: [number, number];
921
+ /**
922
+ * Minimum finding kind to route to alert channels.
923
+ * Defaults to "actionable" — behavioral observations (informational findings
924
+ * like volume_spike, temporal_anomaly, access_pattern) are suppressed by
925
+ * default to prevent alert fatigue. Set to "informational" to receive all
926
+ * findings including behavioral observations.
927
+ */
928
+ minKind?: "informational" | "actionable";
929
+ }
930
+ declare class AlertManager {
931
+ private config;
932
+ private recentAlerts;
933
+ constructor(config?: Partial<AlertConfig>);
934
+ alert(finding: SecurityFinding): Promise<void>;
935
+ clearDedupeCache(): void;
936
+ private sendConsole;
937
+ private severityColor;
938
+ private sendWebhook;
939
+ private sendFile;
940
+ /** Parse the similarity score from an intent_drift finding's baselineComparison. */
941
+ private parseIntentScore;
942
+ /** Parse the task description from an intent_drift finding's description. */
943
+ private parseTaskDescription;
944
+ }
945
+
946
+ interface KeywordSimilarityResult {
947
+ rawScore: number;
948
+ matchedPrimary: string[];
949
+ matchedPhase: string[];
950
+ missedKeywords: string[];
951
+ pathMatches: string[];
952
+ }
953
+ declare class SimilarityEngine {
954
+ private config;
955
+ private threshold;
956
+ private acceptableActions;
957
+ constructor(config?: Partial<IntentAlignmentConfig>);
958
+ computeAlignment(intent: TaskIntent, event: AgentActivityEvent): IntentAlignmentResult;
959
+ keywordSimilarity(primaryKeywords: string[], phaseKeywords: string[], event: AgentActivityEvent): KeywordSimilarityResult;
960
+ /** Check if an event matches acceptable action rules. */
961
+ private checkAcceptable;
962
+ }
963
+
964
+ /**
965
+ * PURITY CONTRACT — evaluateEvent
966
+ *
967
+ * This function is the core security evaluation logic extracted from
968
+ * SentinelRunner.processEvent(). It is the single source of truth for
969
+ * block/allow decisions across both the wrap() middleware path and the
970
+ * (future) HookEngine invocation path.
971
+ *
972
+ * INVARIANTS:
973
+ * - Returns a verdict. Does NOT log to audit trail.
974
+ * - Does NOT trigger violation callbacks or alerts.
975
+ * - Does NOT escalate modes or update block counts.
976
+ * - Does NOT modify ProfileStore or classify events into sessions.
977
+ * - Does NOT mutate its input state objects.
978
+ * - Same input produces same output with zero side effects.
979
+ *
980
+ * CALLER RESPONSIBILITIES:
981
+ * - Persisting findings to audit trail
982
+ * - Dispatching violation callbacks and alerts
983
+ * - Mode escalation (maybeEscalate)
984
+ * - Storing the returned newRecentAlignmentScores
985
+ * - Logging intent checks to audit trail
986
+ * - Session classification and DataPoint persistence
987
+ *
988
+ * NOTE ON DOUBLE EVALUATION (existing quirk, preserved intentionally):
989
+ * In the current architecture, wrap() calls check() pre-execution
990
+ * (role validation once) then record() post-execution which triggers
991
+ * processEvent() (role validation AGAIN). Same event, evaluated twice,
992
+ * duplicate findings dispatched. This is existing behavior — callbacks
993
+ * are idempotent-ish. Future cleanup candidate.
994
+ */
995
+
996
+ /** Output from evaluateEvent — pure verdict with no side effects applied. */
997
+ interface EvaluationResult {
998
+ findings: SecurityFinding[];
999
+ intentAlignment?: IntentAlignmentResult;
1000
+ newRecentAlignmentScores: number[];
1001
+ }
1002
+
1003
+ /**
1004
+ * Hook type system for Sentinel's parallel enforcement pipeline.
1005
+ *
1006
+ * ARCHITECTURAL INVARIANT:
1007
+ * Core defines the safety floor. Hooks can only go stricter or more contextual,
1008
+ * never more permissive. evaluateEvent() is the unquestioned source of truth
1009
+ * for block/allow decisions. Hooks extend behavior; they do not override
1010
+ * safety-critical decisions.
1011
+ *
1012
+ * Guide responses are for recoverable misalignment only. Never use Guide for
1013
+ * boundary violations, credential access, system files, or PII. These always
1014
+ * Block. An attacker who can get the agent to retry after guidance has a new
1015
+ * attack vector.
1016
+ */
1017
+
1018
+ /** Severity levels for hook responses, matching SecurityFinding severity. */
1019
+ type SecuritySeverity = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
1020
+ /**
1021
+ * Only two checkpoints in v1. Other checkpoints (pre_prompt, pre_mcp,
1022
+ * post_mcp, pre_tool, post_tool) are deliberately deferred until concrete
1023
+ * integration needs justify them. Adding surface area ahead of demand
1024
+ * creates maintenance tax for capabilities nobody uses.
1025
+ */
1026
+ type HookCheckpoint = "pre_execution" | "post_execution";
1027
+ /** Context passed to every hook handler invocation. */
1028
+ interface HookContext {
1029
+ agentId: string;
1030
+ checkpoint: HookCheckpoint;
1031
+ event: AgentActivityEvent;
1032
+ metadata: Record<string, unknown>;
1033
+ timestamp: string;
1034
+ }
1035
+ /** Proceed with execution. */
1036
+ type AllowResponse = {
1037
+ kind: "allow";
1038
+ };
1039
+ /** Prevent execution. First Block wins and short-circuits. */
1040
+ type BlockResponse = {
1041
+ kind: "block";
1042
+ reason: string;
1043
+ severity: SecuritySeverity;
1044
+ finding?: SecurityFinding;
1045
+ };
1046
+ /**
1047
+ * Fields that hooks may modify via suggestedAction. Restricted to `target`
1048
+ * and `metadata` only — hooks CANNOT modify agentId, agentRole, agentName,
1049
+ * action, timestamp, duration, or authorized. Without this restriction a
1050
+ * hook could suggest `{ agentRole: "admin" }` to bypass role validation,
1051
+ * `{ agentId: "other" }` to shift to another agent's policy, or
1052
+ * `{ authorized: true }` to mark actions as pre-authorized.
1053
+ */
1054
+ type ModifiableEventFields = {
1055
+ target?: string;
1056
+ metadata?: Record<string, string>;
1057
+ };
1058
+ /**
1059
+ * Advisory correction — delivers guidance to the agent so it can
1060
+ * course-correct, but does not block execution. First Guide wins
1061
+ * (no concatenation). See ARCHITECTURAL INVARIANT above for scope
1062
+ * restrictions on when Guide may be used.
1063
+ *
1064
+ * `suggestedAction` is an optional runtime modification that Sentinel
1065
+ * applies to the action BEFORE execute() runs in `wrap()`. When present:
1066
+ *
1067
+ * - REQUIRES `enablePreExecutionHooks: true` on the Sentinel instance.
1068
+ * When the flag is off (default), hooks fire post-execution and
1069
+ * suggestedAction is never processed.
1070
+ * - Safety floor: if the ORIGINAL action produces a HIGH/CRITICAL
1071
+ * actionable finding, the modification is rejected entirely.
1072
+ * - Modified-event safety: after applying the modification, Sentinel
1073
+ * re-checks the modified target's sensitivity score. If it would score
1074
+ * HIGH or CRITICAL (effectiveScore >= 0.7), the modification is rejected.
1075
+ * - Scope narrowing: simple path-prefix narrowing is enforced at runtime
1076
+ * (e.g., "src/" → "src/auth/login.ts" allowed; "src/auth/" → "src/"
1077
+ * rejected as broadening). Complex glob narrowing is hook-author
1078
+ * responsibility — Sentinel will not catch all cases of glob broadening,
1079
+ * but will reject modifications introducing HIGH/CRITICAL sensitivity.
1080
+ * - Multiple hooks: first Guide with suggestedAction wins (hook priority
1081
+ * order). Register hooks in priority order knowing earlier ones win.
1082
+ * - An `action_modified` audit entry records every applied modification.
1083
+ *
1084
+ * Distinct from `guidance` (agent-facing suggestion text) — suggestedAction
1085
+ * is a runtime parameter change Sentinel applies before execution.
1086
+ */
1087
+ type GuideResponse = {
1088
+ kind: "guide";
1089
+ guidance: string;
1090
+ suggestedAction?: ModifiableEventFields;
1091
+ };
1092
+ /** Discriminated union of all hook response types. */
1093
+ type HookResponse = AllowResponse | BlockResponse | GuideResponse;
1094
+ /** A hook handler — may be sync or async. */
1095
+ type HookHandler = (context: HookContext) => Promise<HookResponse> | HookResponse;
1096
+ /** A registered hook with metadata. */
1097
+ interface HookRegistration {
1098
+ id: string;
1099
+ agentId: string | null;
1100
+ checkpoint: HookCheckpoint;
1101
+ handler: HookHandler;
1102
+ registeredAt: string;
1103
+ priority?: number;
1104
+ failClosed?: boolean;
1105
+ scope: "global" | "agent";
1106
+ status: "active" | "orphaned";
1107
+ }
1108
+
1109
+ /**
1110
+ * HookEngine — parallel invocation path for Sentinel enforcement.
1111
+ *
1112
+ * INVARIANT: Core defines the safety floor. Hooks can only go stricter or more
1113
+ * contextual, never more permissive. evaluateEvent() is the unquestioned source
1114
+ * of truth for block/allow decisions. Hooks extend behavior; they do not
1115
+ * override safety-critical decisions.
1116
+ *
1117
+ * CRITICAL GUARDRAIL: If evaluateEvent returns any finding with severity
1118
+ * HIGH or CRITICAL, HookEngine rejects any Guide response from hooks and falls
1119
+ * back to Block. This prevents hooks from softening safety-critical findings.
1120
+ *
1121
+ * Guide responses are for recoverable misalignment only. Never use Guide for
1122
+ * boundary violations, credential access, system files, or PII.
1123
+ */
1124
+
1125
+ interface HookEngineOptions {
1126
+ debug?: boolean;
1127
+ logAllFirings?: boolean;
1128
+ }
1129
+ declare class HookEngine {
1130
+ private hooks;
1131
+ private auditTrail;
1132
+ private options;
1133
+ constructor(auditTrail: AuditTrail, options?: HookEngineOptions);
1134
+ registerHook(agentId: string, checkpoint: HookCheckpoint, handler: HookHandler, options?: {
1135
+ priority?: number;
1136
+ failClosed?: boolean;
1137
+ }): string;
1138
+ registerGlobalHook(checkpoint: HookCheckpoint, handler: HookHandler, options?: {
1139
+ priority?: number;
1140
+ failClosed?: boolean;
1141
+ }): string;
1142
+ unregisterHook(id: string): boolean;
1143
+ listHooks(agentId?: string): HookRegistration[];
1144
+ fireHook(checkpoint: HookCheckpoint, context: HookContext, evaluationResult?: EvaluationResult): Promise<HookResponse>;
1145
+ private logAudit;
1146
+ private emitDebug;
1147
+ }
1148
+
1149
+ interface ProcessEventResult {
1150
+ dataPoint?: DataPoint;
1151
+ finding?: SecurityFinding;
1152
+ findings?: SecurityFinding[];
1153
+ intentAlignment?: IntentAlignmentResult;
1154
+ guidance?: string;
1155
+ blocked?: boolean;
1156
+ }
1157
+ /**
1158
+ * Result from firePreExecutionGate — a side-effect-free pre-execution verdict.
1159
+ *
1160
+ * Unlike processEvent, this does NOT log to audit trail, dispatch violation
1161
+ * callbacks, classify events, or increment counters. It only runs evaluateEvent
1162
+ * and fires pre_execution hooks (whose handlers may have their own side effects).
1163
+ */
1164
+ interface PreExecutionGateResult {
1165
+ blocked: boolean;
1166
+ finding: SecurityFinding | null;
1167
+ evaluation: EvaluationResult;
1168
+ /** The raw hook response (Allow, Block, or Guide). Null when no HookEngine. */
1169
+ hookResponse: HookResponse | null;
1170
+ }
1171
+ declare class SentinelRunner {
1172
+ private agentId;
1173
+ private config;
1174
+ private adapterConfig;
1175
+ private classifier;
1176
+ private profileManager;
1177
+ private validator;
1178
+ private store;
1179
+ private findings;
1180
+ private adapter;
1181
+ private webhookReceiver;
1182
+ private auditTrail;
1183
+ private alertManager;
1184
+ private _started;
1185
+ private _eventCount;
1186
+ private _sessionCount;
1187
+ private _onFinding;
1188
+ private _auditLogDir;
1189
+ private _baseline;
1190
+ private _deviationDetector;
1191
+ private _gapCheckInterval;
1192
+ private _processQueue;
1193
+ private _hookEngine?;
1194
+ private _maturityConfig?;
1195
+ private _validatorOptions?;
1196
+ private intentTracker;
1197
+ private similarityEngine;
1198
+ private recentAlignmentScores;
1199
+ constructor(agentId: string, config?: Partial<AgentClassifierConfig>, adapterConfig?: AdapterConfig, intentConfig?: Partial<IntentAlignmentConfig>);
1200
+ /** Allow injection of a custom profile manager (for testing with temp dirs). */
1201
+ setProfileManager(manager: AgentProfileManager): void;
1202
+ /** Allow injection of a custom audit log directory (for testing with temp dirs). */
1203
+ setAuditLogDir(dir: string): void;
1204
+ /** Register a callback invoked on every new SecurityFinding. */
1205
+ setFindingCallback(cb: (finding: SecurityFinding) => void): void;
1206
+ /** Attach an AlertManager so findings trigger real-time alerts. */
1207
+ setAlertManager(manager: AlertManager): void;
1208
+ /** Attach a HookEngine for pre/post execution hook firing. */
1209
+ setHookEngine(engine: HookEngine): void;
1210
+ /** Read-only access to the attached HookEngine (if any). */
1211
+ get hooks(): HookEngine | undefined;
1212
+ /** Set baseline maturity thresholds (from YAML policy or programmatic config). */
1213
+ setMaturityConfig(config: Partial<BaselineMaturityConfig>): void;
1214
+ /** Set RoleValidator options (exception approval fn, audit callback). */
1215
+ setRoleValidatorOptions(options: RoleValidatorOptions): void;
1216
+ /** Set the behavioral baseline and start periodic gap checking. */
1217
+ setBaseline(baseline: AgentBaseline): void;
1218
+ /** Declare a new task intent for this agent. */
1219
+ startTask(taskId: string, description: string, config?: {
1220
+ acceptableActions?: AcceptableAction[];
1221
+ ttlMs?: number;
1222
+ relaxedActions?: AgentActivityEvent["action"][];
1223
+ phases?: string[];
1224
+ scopePatterns?: string[];
1225
+ }): TaskIntent;
1226
+ /** End the current task for this agent. */
1227
+ endTask(): TaskIntent | null;
1228
+ /** Get the active task for this agent (null if none or expired). */
1229
+ getActiveTask(): TaskIntent | null;
1230
+ /** Expose the similarity engine for pre-execution intent checks. */
1231
+ getSimilarityEngine(): SimilarityEngine;
1232
+ /**
1233
+ * Side-effect-free pre-execution gate: evaluate an event and fire pre_execution
1234
+ * hooks WITHOUT running processEvent's full pipeline.
1235
+ *
1236
+ * This method is designed for wrap() to consult hooks before calling execute().
1237
+ * It does NOT:
1238
+ * - Write to the audit trail (logEvent, logFinding)
1239
+ * - Dispatch violation callbacks (onFinding)
1240
+ * - Classify events into sessions
1241
+ * - Increment event counters
1242
+ * - Mutate alignment score state
1243
+ *
1244
+ * It DOES:
1245
+ * - Run evaluateEvent() (pure evaluation)
1246
+ * - Fire pre_execution hooks via HookEngine (handlers may have their own side effects)
1247
+ * - Apply the guardrail: HIGH/CRITICAL evaluation findings synthesize Block
1248
+ * even if all hooks returned Allow
1249
+ */
1250
+ firePreExecutionGate(event: AgentActivityEvent): Promise<PreExecutionGateResult>;
1251
+ private runGapCheck;
1252
+ get eventCount(): number;
1253
+ get sessionCount(): number;
1254
+ start(): Promise<void>;
1255
+ processEvent(event: AgentActivityEvent, options?: {
1256
+ _skipPreHooks?: boolean;
1257
+ }): Promise<ProcessEventResult>;
1258
+ processEvents(events: AgentActivityEvent[]): Promise<{
1259
+ dataPoints: DataPoint[];
1260
+ findings: SecurityFinding[];
1261
+ }>;
1262
+ /**
1263
+ * Flush the classifier to close the current session, then run
1264
+ * session-level deviation analysis if a baseline is configured.
1265
+ * Returns any SecurityFindings produced by the deviation detector.
1266
+ */
1267
+ completeSession(): Promise<SecurityFinding[]>;
1268
+ getFindings(): SecurityFinding[];
1269
+ clearFindings(): void;
1270
+ getAuditTrail(): AuditTrail | null;
1271
+ stop(): Promise<void>;
1272
+ private startAdapter;
1273
+ }
1274
+
1275
+ interface CorrelationFinding {
1276
+ rule: string;
1277
+ severity: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
1278
+ description: string;
1279
+ agentA: {
1280
+ agentId: string;
1281
+ action: string;
1282
+ target: string;
1283
+ timestamp: string;
1284
+ };
1285
+ agentB: {
1286
+ agentId: string;
1287
+ action: string;
1288
+ target: string;
1289
+ timestamp: string;
1290
+ };
1291
+ timeDeltaMs: number;
1292
+ recommendation: string;
1293
+ }
1294
+
1295
+ type AgentMode = "normal" | "restricted" | "quarantined";
1296
+ interface MonitorOptions {
1297
+ name: string;
1298
+ description?: string;
1299
+ allowedActions?: string[];
1300
+ allowedTargetPatterns?: string[];
1301
+ forbiddenTargetPatterns?: string[];
1302
+ exceptions?: RoleException[];
1303
+ /** Host allowlist for network_request targets (Sprint 6b W3c). */
1304
+ networkHosts?: string[];
1305
+ expectedSchedule?: {
1306
+ activeHours?: [number, number];
1307
+ activeDays?: string[];
1308
+ };
1309
+ maxEventsPerHour?: number;
1310
+ maxSessionDuration?: number;
1311
+ adapter?: AdapterConfig;
1312
+ intentConfig?: Partial<IntentAlignmentConfig>;
1313
+ /**
1314
+ * Absolute workspace root for anchoring bare allowed patterns (Approach 1).
1315
+ * Supplied by fromPolicy() as repo.root ?? dirname(policyPath). When omitted,
1316
+ * allowed patterns are matched as-authored: direct callers must then supply
1317
+ * absolute or globstar-prefixed allowed patterns themselves.
1318
+ */
1319
+ workspaceRoot?: string;
1320
+ }
1321
+ /**
1322
+ * Public SDK facade for Tuent Sentinel.
1323
+ *
1324
+ * This is the primary entry point for external developers.
1325
+ * It wraps the internal manager, validators, and reporters into a
1326
+ * single cohesive API.
1327
+ */
1328
+ declare class Sentinel {
1329
+ private manager;
1330
+ private profileManager;
1331
+ private alertManager;
1332
+ private violationCallbacks;
1333
+ private alertCallbacks;
1334
+ private exitSignalCallbacks;
1335
+ private approvalCallback?;
1336
+ private agentsDir;
1337
+ private sensitivityScorer;
1338
+ private agentModes;
1339
+ private globalHookRegistrations;
1340
+ private environment;
1341
+ private restrictThreshold;
1342
+ private quarantineThreshold;
1343
+ private enablePreExecutionHooks;
1344
+ private promoteSet;
1345
+ private maturityConfig?;
1346
+ private _exceptionApprovalFn;
1347
+ /** Audit entries captured during the last check() call — used by wrap() for exception routing. */
1348
+ private _lastCheckExceptionEntries;
1349
+ private _repoRoot;
1350
+ /** Absolute workspace root for Check 3 allowed-pattern anchoring (Approach 1). */
1351
+ private _workspaceRoot;
1352
+ /**
1353
+ * Per-agentId init locks for the Approach-B per-workspace lazy-create path
1354
+ * (B5a), mirroring B3's manager-level lock but at the Sentinel facade level
1355
+ * (where role-save + baseline + finding-callback wiring live). Stored
1356
+ * synchronously so concurrent first-requests for the same workspace run the
1357
+ * full setup exactly once. Dormant until the gateway's isolation gate is on.
1358
+ */
1359
+ private readonly _workspaceInitLocks;
1360
+ private _scanOnStartup;
1361
+ private _mapPath;
1362
+ private _overlayPath;
1363
+ private _repoMap;
1364
+ private _overlay;
1365
+ private _repoInitPromise;
1366
+ /**
1367
+ * Per-agent session ID tracking (R6 identity binding).
1368
+ * First wrap() call for an agent generates a UUID; subsequent calls reuse it.
1369
+ * completeSession() clears the entry so the next wrap() starts a new session.
1370
+ */
1371
+ private currentSessionIds;
1372
+ /**
1373
+ * Per-agent session-scoped forbidden-inode caches (Sprint 9 hardlink closure).
1374
+ * Built lazily on first nlink > 1 observation per session.
1375
+ * Cleared on completeSession() / removeAgent().
1376
+ */
1377
+ private forbiddenInodeCaches;
1378
+ constructor(config?: Partial<SentinelConfig> & {
1379
+ agentsDir?: string;
1380
+ environment?: "development" | "production";
1381
+ enforcement?: {
1382
+ restrictAfter?: number;
1383
+ quarantineAfter?: number;
1384
+ promote?: string[];
1385
+ baselineMaturity?: Partial<BaselineMaturityConfig>;
1386
+ };
1387
+ enablePreExecutionHooks?: boolean;
1388
+ repo?: {
1389
+ root?: string;
1390
+ scanOnStartup?: boolean;
1391
+ mapPath?: string;
1392
+ overlayPath?: string;
1393
+ };
1394
+ });
1395
+ getMode(agentId: string): AgentMode;
1396
+ isQuarantined(agentId: string): boolean;
1397
+ isRestricted(agentId: string): boolean;
1398
+ getQuarantinedAgents(): string[];
1399
+ getRestrictedAgents(): string[];
1400
+ /**
1401
+ * Current escalation-eligible block count for an agent.
1402
+ *
1403
+ * Intentionally async: the count is DERIVED from the audit log (not an
1404
+ * in-memory counter), so it survives gateway restarts and is the single source
1405
+ * of truth for the restrict/quarantine ladder. Callers must await it.
1406
+ */
1407
+ getBlockCount(agentId: string): Promise<number>;
1408
+ getEnvironment(): "development" | "production";
1409
+ /**
1410
+ * Initializes the repo sensitivity map. If scanOnStartup is true and
1411
+ * repoRoot is set, performs a fresh scan and persists the map. Otherwise
1412
+ * attempts to load an existing map from disk.
1413
+ */
1414
+ private _initRepoMap;
1415
+ /**
1416
+ * Returns a promise that resolves when the repo map initialization is
1417
+ * complete. Safe to call multiple times (idempotent). Returns immediately
1418
+ * if no repo init was started.
1419
+ */
1420
+ waitForRepoInit(): Promise<void>;
1421
+ /**
1422
+ * Triggers a fresh scan of the repo, persists the updated map, and
1423
+ * grounds the scorer with the new data. Requires repoRoot to be set.
1424
+ */
1425
+ rescan(repoRoot?: string): Promise<RepoSensitivityMap>;
1426
+ /** Returns the current repo sensitivity map, or null if none is loaded. */
1427
+ getRepoMap(): RepoSensitivityMap | null;
1428
+ /** Returns the current sensitivity overlay, or null if none is loaded. */
1429
+ getOverlay(): SensitivityOverlay | null;
1430
+ /**
1431
+ * Sets an overlay decision for a file path. Persists the overlay to disk.
1432
+ * Creates a new overlay if none exists.
1433
+ */
1434
+ setOverlayDecision(filePath: string, decision: OverlayDecisionType, options?: {
1435
+ reason?: string;
1436
+ sensitivity?: number;
1437
+ category?: string;
1438
+ }): Promise<void>;
1439
+ /**
1440
+ * Removes an overlay decision for a file path. Persists the overlay to disk.
1441
+ */
1442
+ removeOverlayDecision(filePath: string): Promise<void>;
1443
+ /**
1444
+ * Reloads the overlay from disk. Use after external edits to the overlay file.
1445
+ */
1446
+ reloadOverlay(): Promise<void>;
1447
+ quarantine(agentId: string, reason: string): Promise<void>;
1448
+ restrict(agentId: string, reason: string): Promise<void>;
1449
+ release(agentId: string, reason?: string): Promise<void>;
1450
+ private persistMode;
1451
+ private logModeChange;
1452
+ private stopAgent;
1453
+ private loadModeFromDisk;
1454
+ /**
1455
+ * Pre-execution check: validate an event against an agent's role definition.
1456
+ * Returns a finding if the event violates the role, or null if allowed.
1457
+ * If the agent is not registered, returns a LOW finding warning about missing registration.
1458
+ */
1459
+ check(agentId: string, event: AgentActivityEvent): Promise<SecurityFinding | null>;
1460
+ /**
1461
+ * Post-execution record: record an event, classify into sessions,
1462
+ * check baseline deviations. Returns findings and dataPoint if session closed.
1463
+ * If the agent is not registered, returns a warning string.
1464
+ *
1465
+ */
1466
+ record(event: AgentActivityEvent): Promise<{
1467
+ dataPoint?: DataPoint;
1468
+ findings: SecurityFinding[];
1469
+ warning?: string;
1470
+ intentAlignment?: IntentAlignmentResult;
1471
+ }>;
1472
+ /**
1473
+ * Internal record path. `options._skipPreHooks` suppresses pre_execution hook
1474
+ * firing in processEvent — used by wrap() when enablePreExecutionHooks is on, so
1475
+ * the gate (already fired via firePreExecutionGate) doesn't double-fire. Kept
1476
+ * OFF the public record() signature so the internal flag isn't a consumer API.
1477
+ */
1478
+ private _recordInternal;
1479
+ /**
1480
+ * Query the audit trail for an agent.
1481
+ * If the agent is not registered, logs a warning and returns an empty array.
1482
+ */
1483
+ query(agentId: string, options?: AuditQueryOptions): Promise<AuditEntry[]>;
1484
+ /**
1485
+ * Public method allowing external code (notably the gateway) to write
1486
+ * findings directly to an agent's audit trail. Used by the gateway's
1487
+ * pre-tool-use handler to persist deny decisions and allow-with-finding
1488
+ * emissions that don't flow through wrap().
1489
+ *
1490
+ * Without this method, gateway-level Sentinel decisions are returned in
1491
+ * HTTP responses to cc but never recorded as type:"finding" entries in
1492
+ * the audit log. That breaks SOC-tooling integration and forensic
1493
+ * queries — restored in Sprint 6a hotfix between Prompts 6 and 7.
1494
+ */
1495
+ logFinding(agentId: string, finding: SecurityFinding): Promise<void>;
1496
+ /**
1497
+ * Persist a gateway-level deny finding and evaluate enforcement escalation.
1498
+ *
1499
+ * Called from SentinelGateway deny paths that short-circuit before wrap().
1500
+ * Combines logFinding (audit persistence) with maybeEscalate (enforcement
1501
+ * ladder evaluation) into a single atomic call. Without this, gateway
1502
+ * denies are persisted to the audit log but never trigger mode transitions.
1503
+ *
1504
+ * Sprint 16 — closes the enforcement ladder gap where 7 gateway DENY paths
1505
+ * bypassed wrap() and therefore never reached maybeEscalate().
1506
+ */
1507
+ handleGatewayDeny(agentId: string, finding: SecurityFinding): Promise<void>;
1508
+ addAgent(agentId: string, name: string, role?: AgentRole): Promise<void>;
1509
+ /**
1510
+ * Validate a derived workspace root and emit the Approach-1 safety-valve
1511
+ * warnings (point 6). Returns the resolved root regardless (the warning is
1512
+ * advisory) so anchoring still functions; the operator is told when the
1513
+ * root looks wrong.
1514
+ *
1515
+ * - At or above $HOME → the policy was likely not found at a repo root, so
1516
+ * anchoring to home would be far too broad. Warn loudly.
1517
+ * - Anchor prefix directories that don't exist on disk → warn once so a
1518
+ * typo'd root surfaces instead of silently denying every allowed read.
1519
+ */
1520
+ private validateWorkspaceRoot;
1521
+ monitor(agentId: string, options: MonitorOptions): Promise<void>;
1522
+ /**
1523
+ * Approach B / B5a — lazily get-or-create a fully-configured per-workspace
1524
+ * runner, idempotent and concurrency-safe, WITHOUT mutating the daemon-global
1525
+ * `_workspaceRoot` (so Approach A's single-workspace semantics are untouched).
1526
+ *
1527
+ * Reuses B3's concurrency-safe runner construction (manager.getOrCreateAgent)
1528
+ * but adds the Sentinel-level setup monitor() does — persist the merged role,
1529
+ * wire the finding callback / alert manager, enable audit signing, set the
1530
+ * baseline, load the mode — minus wireRunner()'s workspaceRoot clobber (which
1531
+ * would force every workspace's validator onto the global root). Each runner
1532
+ * anchors to ITS OWN `workspaceRoot` via validatorOptions.
1533
+ *
1534
+ * Touches activity (B4) on create. The gateway also touches on each request.
1535
+ * DORMANT: only the gateway's isolation gate (off by default) calls this.
1536
+ */
1537
+ getOrCreateWorkspaceAgent(agentId: string, options: {
1538
+ workspaceRoot: string;
1539
+ role: AgentRole;
1540
+ name: string;
1541
+ }): Promise<SentinelRunner>;
1542
+ private _initWorkspaceAgent;
1543
+ /** B4 delegators for the gateway (touch on request, start/stop the idle sweep). */
1544
+ touchAgent(agentId: string): void;
1545
+ startWorkspaceSweep(intervalMs?: number): void;
1546
+ stopWorkspaceSweep(): void;
1547
+ evictIdleWorkspaceRunners(now?: number): Promise<string[]>;
1548
+ /**
1549
+ * Ids of all currently-registered runners (global + per-workspace under B).
1550
+ * B5b-Phase-2: the gateway uses this to complete EVERY active session on
1551
+ * shutdown, not just the global identity.
1552
+ */
1553
+ listActiveAgentIds(): string[];
1554
+ static quickStart(agentId: string, name: string, logPath?: string): Promise<Sentinel>;
1555
+ static fromPolicy(yamlPath: string, options?: {
1556
+ agentsDir?: string;
1557
+ }): Promise<Sentinel>;
1558
+ removeAgent(agentId: string): Promise<void>;
1559
+ computeBaseline(agentId: string): Promise<AgentBaseline>;
1560
+ /**
1561
+ * Inject a pre-built baseline into a registered agent's runner.
1562
+ * Creates the DeviationDetector in the appropriate maturity tier
1563
+ * (empty / immature / mature) based on the baseline's metrics.
1564
+ */
1565
+ setBaseline(agentId: string, baseline: AgentBaseline): void;
1566
+ /**
1567
+ * Flush the current session for an agent and run session-level
1568
+ * deviation analysis. Returns any SecurityFindings produced.
1569
+ */
1570
+ completeSession(agentId: string): Promise<SecurityFinding[]>;
1571
+ /** Declare a new task intent for an agent. Returns the TaskIntent object. */
1572
+ startTask(agentId: string, taskId: string, description: string, config?: {
1573
+ acceptableActions?: AcceptableAction[];
1574
+ ttlMs?: number;
1575
+ relaxedActions?: AgentActivityEvent["action"][];
1576
+ phases?: string[];
1577
+ scopePatterns?: string[];
1578
+ }): TaskIntent | null;
1579
+ /** End the current task for an agent. Returns the completed TaskIntent or null. */
1580
+ endTask(agentId: string): TaskIntent | null;
1581
+ /** Get the active task for an agent (null if none or expired). */
1582
+ getActiveTask(agentId: string): TaskIntent | null;
1583
+ /**
1584
+ * Returns the existing sessionId for an agent, or generates a new one.
1585
+ * New sessions are created on the first wrap() call for an agent, or
1586
+ * after completeSession() clears the previous session.
1587
+ */
1588
+ private getOrCreateSessionId;
1589
+ /**
1590
+ * Middleware wrapper: pre-check, execute, post-record in one call.
1591
+ * Blocks execution if agent is quarantined/restricted, check() returns HIGH/CRITICAL,
1592
+ * pre-execution hooks block (when enablePreExecutionHooks is on),
1593
+ * or an approval callback rejects a MEDIUM finding.
1594
+ *
1595
+ * The `execute` function receives the effective event — the original event with
1596
+ * any hook-suggested modifications applied. When no modifications occur (the
1597
+ * common case), effectiveEvent is the original event. Callers that don't need
1598
+ * modification support can ignore the parameter — `() => Promise<T>` is
1599
+ * assignable to `(event: AgentActivityEvent) => Promise<T>` in TypeScript.
1600
+ *
1601
+ * MODIFY decision (AARM R4): When `enablePreExecutionHooks` is true and a
1602
+ * pre-execution hook returns Guide with `suggestedAction`, Sentinel applies
1603
+ * the modification subject to safety checks before calling execute().
1604
+ */
1605
+ wrap<T>(agentId: string, event: Omit<AgentActivityEvent, "timestamp">, execute: (effectiveEvent: AgentActivityEvent) => Promise<T>, options?: {
1606
+ userId?: string;
1607
+ }): Promise<{
1608
+ result?: T;
1609
+ blocked: boolean;
1610
+ finding?: SecurityFinding;
1611
+ }>;
1612
+ /**
1613
+ * Factory for creating a wrapped tool function with a fixed agent identity.
1614
+ * The execute function receives the effective event (with any modifications
1615
+ * applied by hooks). See wrap() for MODIFY decision semantics.
1616
+ */
1617
+ wrapTool(agentId: string, agentName: string, agentRole: string): <T>(action: AgentActivityEvent["action"], target: string, execute: (effectiveEvent: AgentActivityEvent) => Promise<T>, options?: {
1618
+ userId?: string;
1619
+ }) => Promise<{
1620
+ result?: T;
1621
+ blocked: boolean;
1622
+ finding?: SecurityFinding;
1623
+ }>;
1624
+ /**
1625
+ * Apply a hook-suggested modification to an event, subject to safety checks.
1626
+ * Returns the modified event if checks pass, or null if rejected.
1627
+ *
1628
+ * Safety checks:
1629
+ * 1. Safety floor (original): if the original event already has a HIGH/CRITICAL
1630
+ * actionable finding, modifications cannot rescue it.
1631
+ * 2. Scope narrowing: if the suggested target is not a prefix-extension of the
1632
+ * original primaryTarget (i.e., broadening), the modification is rejected.
1633
+ * 3. Sensitivity re-check (modified): if the modified target scores HIGH/CRITICAL
1634
+ * on sensitivity (effectiveScore >= 0.7), the modification is rejected.
1635
+ */
1636
+ private applyModification;
1637
+ private logModificationRejected;
1638
+ detectCorrelations(windowMs?: number): Promise<CorrelationFinding[]>;
1639
+ report(agentId: string, options?: Partial<ReportOptions>): Promise<string>;
1640
+ fleetReport(options?: {
1641
+ periodDays?: number;
1642
+ format?: "markdown" | "json";
1643
+ }): Promise<string>;
1644
+ getHealthScore(agentId: string): Promise<{
1645
+ score: number;
1646
+ status: "healthy" | "caution" | "at_risk" | "critical" | "quarantined" | "restricted" | "new";
1647
+ mode: AgentMode;
1648
+ findings: {
1649
+ critical: number;
1650
+ high: number;
1651
+ medium: number;
1652
+ low: number;
1653
+ };
1654
+ lastEvent: string | null;
1655
+ baselineEstablished: boolean;
1656
+ agentName: string;
1657
+ }>;
1658
+ /**
1659
+ * Register a hook handler for a specific agent's lifecycle checkpoint.
1660
+ *
1661
+ * @param agentId - The ID of the agent to attach the hook to. Agent must be registered.
1662
+ * @param checkpoint - Either 'pre_execution' or 'post_execution'.
1663
+ * @param handler - Sync or async function returning Allow, Block, or Guide response.
1664
+ * @param options - Optional priority (higher fires first, default 0) and failClosed (default false).
1665
+ * @returns Unique hook ID for later unregistration via off().
1666
+ * @throws If agentId is not a registered agent.
1667
+ *
1668
+ * @example
1669
+ * const hookId = sentinel.on('my-agent', 'pre_execution', async (ctx) => {
1670
+ * if (ctx.event.target.startsWith('/production')) {
1671
+ * return { kind: 'block', reason: 'production access blocked', severity: 'HIGH' };
1672
+ * }
1673
+ * return { kind: 'allow' };
1674
+ * });
1675
+ */
1676
+ on(agentId: string, checkpoint: HookCheckpoint, handler: HookHandler, options?: {
1677
+ priority?: number;
1678
+ failClosed?: boolean;
1679
+ }): string;
1680
+ /**
1681
+ * Register a global hook that fires for ALL agents — current and future.
1682
+ *
1683
+ * Immediately registers the hook on every currently-active runner. When a new
1684
+ * agent is added via addAgent() or monitor(), the hook is automatically
1685
+ * replayed onto the new runner.
1686
+ *
1687
+ * @param checkpoint - Either 'pre_execution' or 'post_execution'.
1688
+ * @param handler - Sync or async function returning Allow, Block, or Guide response.
1689
+ * @param options - Optional priority (higher fires first, default 0) and failClosed (default false).
1690
+ * @returns Sentinel-level hook ID for later unregistration via off().
1691
+ *
1692
+ * @example
1693
+ * const hookId = sentinel.onAll('pre_execution', (ctx) => {
1694
+ * if (ctx.event.target.includes('.env')) {
1695
+ * return { kind: 'block', reason: 'env access forbidden', severity: 'CRITICAL' };
1696
+ * }
1697
+ * return { kind: 'allow' };
1698
+ * });
1699
+ */
1700
+ onAll(checkpoint: HookCheckpoint, handler: HookHandler, options?: {
1701
+ priority?: number;
1702
+ failClosed?: boolean;
1703
+ }): string;
1704
+ /**
1705
+ * Unregister a hook by its ID. Works for both agent-specific and global hooks.
1706
+ *
1707
+ * @param hookId - The hook ID returned by on() or onAll().
1708
+ * @returns true if the hook was found and removed, false otherwise.
1709
+ */
1710
+ off(hookId: string): boolean;
1711
+ /**
1712
+ * List registered hooks. If agentId is provided, returns that agent's hooks
1713
+ * plus any global hooks. If omitted, returns all hooks across all agents.
1714
+ *
1715
+ * @param agentId - Optional agent ID to filter by.
1716
+ * @returns Array of hook registrations.
1717
+ */
1718
+ listHooks(agentId?: string): HookRegistration[];
1719
+ /** Replay all pending global hooks onto a newly-created runner. */
1720
+ private replayGlobalHooks;
1721
+ /** Lazily create and attach a HookEngine to a runner if it doesn't have one. */
1722
+ private ensureHookEngine;
1723
+ start(): Promise<void>;
1724
+ stop(): Promise<void>;
1725
+ onViolation(callback: (finding: SecurityFinding) => void): void;
1726
+ onAlert(callback: (finding: SecurityFinding) => void): void;
1727
+ onExitSignal(callback: (agentId: string, reason: string) => void): void;
1728
+ onApprovalRequired(callback: (event: AgentActivityEvent, finding: SecurityFinding) => Promise<boolean>): void;
1729
+ /**
1730
+ * Register a callback for exception approvals.
1731
+ *
1732
+ * Distinct from onApprovalRequired (which gates MEDIUM-finding triage):
1733
+ * - onApprovalRequired: "Sentinel found a borderline MEDIUM finding. Operator decides."
1734
+ * - onExceptionApprovalRequired: "Sentinel found a forbidden access, but a YAML
1735
+ * exception says it might be okay if a human signs off. Operator approves the
1736
+ * pre-configured exception."
1737
+ *
1738
+ * Operators can wire both to the same handler for unified UX, but the type
1739
+ * signatures are distinct so the handler can route on context.
1740
+ */
1741
+ onExceptionApprovalRequired(callback: ExceptionApprovalFn): void;
1742
+ /**
1743
+ * Single per-runner wiring path for BOTH the global add-agent/monitor runners
1744
+ * and the per-workspace B runners (B5b-Phase-2 consolidation — _initWorkspaceAgent
1745
+ * previously kept a hand-maintained "non-clobbering subset" of this, which is
1746
+ * exactly how finding-callback / validator config drifts across sites). The
1747
+ * only thing that varies is the anchoring root: global callers omit it and get
1748
+ * the daemon-global this._workspaceRoot; the B-path passes the per-workspace
1749
+ * root so the anchoring set at construction is RE-ASSERTED with the same value,
1750
+ * not clobbered by the global root.
1751
+ */
1752
+ private wireRunner;
1753
+ /**
1754
+ * Enable Ed25519 audit trail signing for a runner's audit trail.
1755
+ * Loads or generates a per-agent keypair, then wires it into the audit trail
1756
+ * so all subsequent entries include signature + signerPublicKey fields.
1757
+ */
1758
+ private enableAuditSigning;
1759
+ /** Mutate finding.kind from informational → actionable if the type is in the promote set. */
1760
+ private applyPromote;
1761
+ private notifyViolation;
1762
+ private static readonly RESTRICTED_ALLOWED_ACTIONS;
1763
+ private requestApproval;
1764
+ private enforceMode;
1765
+ private static readonly ESCALATION_ELIGIBLE_TYPES;
1766
+ /**
1767
+ * Derive the effective block count from the audit log.
1768
+ *
1769
+ * Counts escalation-eligible findings (HIGH/CRITICAL, actionable,
1770
+ * matching ESCALATION_ELIGIBLE_TYPES) since the most recent release
1771
+ * (mode_change to "normal"). If no release exists (genesis case),
1772
+ * counts all eligible findings in the agent's entire audit history.
1773
+ *
1774
+ * Only release-type mode_changes reset the count — restrict and
1775
+ * quarantine transitions do NOT reset it, matching the original
1776
+ * in-memory behavior where only release() called blockCounts.set(0).
1777
+ *
1778
+ * Sprint 16 Prompt 3 — replaces in-memory blockCounts Map to survive
1779
+ * gateway restarts. Same pattern as Sprint 11 sessionCount Shape D fix.
1780
+ */
1781
+ private getEffectiveBlockCount;
1782
+ private maybeEscalate;
1783
+ }
1784
+
1785
+ export { type AgentActivityEvent as A, type BlockResponse as B, type CorrelationFinding as C, type ExceptionApprovalContext as E, type GuideResponse as G, type HookCheckpoint as H, type IntentAlignmentConfig as I, type ModifiableEventFields as M, type OverlayDecisionType as O, type RepoSensitivityMap as R, type SecurityFinding as S, type TaskIntent as T, type AcceptableAction as a, type AdapterConfig as b, type AgentBaseline as c, type AgentMode as d, type AgentRole as e, type AlertChannel as f, type AlertConfig as g, type AllowResponse as h, type AuditEntry as i, type AuditQueryOptions as j, type ExceptionApprovalFn as k, type HookContext as l, type HookHandler as m, type HookRegistration as n, type HookResponse as o, type IntentAlignmentResult as p, type MonitorOptions as q, type ReportOptions as r, type RoleException as s, type SecuritySeverity as t, type SensitivityOverlay as u, Sentinel as v, type SentinelConfig as w };