@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.
- package/LICENSE +201 -0
- package/README.md +96 -0
- package/dist/Sentinel-B_sv8Kiy.d.ts +1785 -0
- package/dist/Sentinel-JLQL3YRD.js +10 -0
- package/dist/auditTrailKeys-GKCW5KUD.js +23 -0
- package/dist/chunk-2FFMYSVC.js +428 -0
- package/dist/chunk-3U3PKD4N.js +539 -0
- package/dist/chunk-6MHWJATS.js +1221 -0
- package/dist/chunk-CUJKNIKT.js +62 -0
- package/dist/chunk-FMZWHT4M.js +20 -0
- package/dist/chunk-NUXSUSYY.js +95 -0
- package/dist/chunk-PDWWRZXF.js +238 -0
- package/dist/chunk-QFRDEISP.js +7429 -0
- package/dist/chunk-Z3PWIJKT.js +2268 -0
- package/dist/cli.js +80 -0
- package/dist/gateway/index.d.ts +241 -0
- package/dist/gateway/index.js +10 -0
- package/dist/gatewayDaemon.js +25 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +28 -0
- package/dist/logAdapter-IB6ZDEV2.js +7 -0
- package/dist/mcpAdapter-R47GX2P3.js +178 -0
- package/dist/pidManager-ZYC7SICM.js +15 -0
- package/dist/policyLoader-6KR5VFVV.js +15 -0
- package/dist/webhookReceiver-NAVMQ6N5.js +203 -0
- package/package.json +61 -0
|
@@ -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 };
|