@tuent/sentinel 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Sentinel-BVoMEF3F.d.ts → Sentinel-CJJ4iYDh.d.ts} +122 -11
- package/dist/Sentinel-XP6NFG6Z.js +10 -0
- package/dist/{chunk-PDWWRZXF.js → chunk-2IPSTUNH.js} +18 -7
- package/dist/{chunk-G74MMDKA.js → chunk-3WT3K5TH.js} +151 -24
- package/dist/{chunk-SSDIBY52.js → chunk-7R6EA7JG.js} +223 -90
- package/dist/chunk-B6S2PBS4.js +47 -0
- package/dist/{chunk-JTR2E7RD.js → chunk-M5EEVMLU.js} +222 -186
- package/dist/{chunk-WLIDSTS4.js → chunk-SKE74CYZ.js} +231 -28
- package/dist/{chunk-2TJ5Z53T.js → chunk-UVNRPML4.js} +59 -20
- package/dist/cli.js +33 -35
- package/dist/gateway/index.d.ts +26 -1
- package/dist/gateway/index.js +4 -4
- package/dist/gatewayDaemon.js +5 -5
- package/dist/index.d.ts +12 -12
- package/dist/index.js +5 -5
- package/dist/logAdapter-WM43W3S7.js +7 -0
- package/dist/{mcpAdapter-R47GX2P3.js → mcpAdapter-WYAXUE7T.js} +2 -2
- package/dist/{policyLoader-KZL2U4M2.js → policyLoader-NUPBBRKH.js} +8 -4
- package/package.json +1 -1
- package/dist/Sentinel-5CQ6HKXS.js +0 -10
- package/dist/chunk-FMZWHT4M.js +0 -20
- package/dist/logAdapter-IB6ZDEV2.js +0 -7
|
@@ -16,6 +16,7 @@ interface SkillNode {
|
|
|
16
16
|
core?: boolean;
|
|
17
17
|
sharable?: boolean;
|
|
18
18
|
files?: string[];
|
|
19
|
+
eventCount?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface DataPoint {
|
|
@@ -29,6 +30,7 @@ interface DataPoint {
|
|
|
29
30
|
files?: string[];
|
|
30
31
|
sharable?: boolean;
|
|
31
32
|
core?: boolean;
|
|
33
|
+
eventCount?: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
interface StoredProfile {
|
|
@@ -89,16 +91,41 @@ declare class ProfileStore {
|
|
|
89
91
|
private petalCount;
|
|
90
92
|
private engine;
|
|
91
93
|
private profile;
|
|
94
|
+
/** Snapshot (label → serialized point) of the on-disk-synced dataPoint set,
|
|
95
|
+
* refreshed on every load/create/write. The base for save()'s three-way merge
|
|
96
|
+
* of concurrent external writes — lets us tell "I deleted this" (in baseline,
|
|
97
|
+
* gone from memory) from "another writer added this" (on disk, not in baseline). */
|
|
98
|
+
private baseline;
|
|
92
99
|
constructor(options: {
|
|
93
100
|
backend: StorageBackend;
|
|
94
101
|
eventBus?: EventBus;
|
|
95
102
|
petalCount?: number;
|
|
96
103
|
});
|
|
97
104
|
load(): Promise<StoredProfile | null>;
|
|
105
|
+
/** Snapshot the on-disk-synced dataPoint set for save()'s three-way merge. */
|
|
106
|
+
private captureBaseline;
|
|
98
107
|
create(name: string): Promise<StoredProfile>;
|
|
99
108
|
addPoint(point: DataPoint): Promise<void>;
|
|
100
109
|
addPoints(points: DataPoint[]): void;
|
|
101
110
|
save(): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Three-way merge of a concurrent external write into the in-memory profile,
|
|
113
|
+
* invoked by save() when the backend reports the file changed since we last
|
|
114
|
+
* synced. Writers in this app are single-user but multi-process (agent scanner,
|
|
115
|
+
* diary watcher, dev server, CLI), so concurrent writes are real but rare.
|
|
116
|
+
*
|
|
117
|
+
* Model (base = the dataPoint set at our last load/write, see `baseline`):
|
|
118
|
+
* - a point present in memory → kept as-is (the SAVING process's adds/edits
|
|
119
|
+
* win on a same-point conflict — save() is an explicit "persist my state");
|
|
120
|
+
* - a disk point whose label is NEW since baseline → another writer's
|
|
121
|
+
* addition → preserved;
|
|
122
|
+
* - a point we dropped (in baseline, absent from memory) → our deletion is
|
|
123
|
+
* honored, UNLESS the other writer modified it (then their version is kept
|
|
124
|
+
* so a concurrent edit is never silently lost).
|
|
125
|
+
* Net: nothing vanishes except a same-point simultaneous edit, resolved
|
|
126
|
+
* last-writer (this save) — a deliberate, documented tie-break.
|
|
127
|
+
*/
|
|
128
|
+
private mergeExternalChanges;
|
|
102
129
|
build(): SkillNode[];
|
|
103
130
|
getProfile(): StoredProfile | null;
|
|
104
131
|
}
|
|
@@ -264,8 +291,16 @@ interface SecurityFinding {
|
|
|
264
291
|
* its dominant real-world cause is a recognized-tool set gone stale after a
|
|
265
292
|
* Claude Code update, not agent misbehavior — counting it would turn every
|
|
266
293
|
* cc update into a restriction ratchet.
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
*
|
|
295
|
+
* `enforcement_error` is likewise non-eligible: it records that Sentinel's
|
|
296
|
+
* OWN enforcement machinery threw an internal error while screening a call
|
|
297
|
+
* (so the call was decided by posture, not by policy). The cause is a
|
|
298
|
+
* Sentinel bug or broken config, not agent misbehavior — counting it would
|
|
299
|
+
* compound a Sentinel-internal failure into an agent lockout. It is HIGH +
|
|
300
|
+
* actionable so it alerts and demands operator attention, but it never
|
|
301
|
+
* moves the escalation ladder.
|
|
302
|
+
*/
|
|
303
|
+
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" | "unknown_tool" | "enforcement_error";
|
|
269
304
|
agentId: string;
|
|
270
305
|
agentName: string;
|
|
271
306
|
description: string;
|
|
@@ -380,7 +415,6 @@ interface SentinelConfig {
|
|
|
380
415
|
mcpLogDir?: string;
|
|
381
416
|
webhookPort?: number;
|
|
382
417
|
webhookApiKey?: string;
|
|
383
|
-
roleDefinitionPath?: string;
|
|
384
418
|
readExisting?: boolean;
|
|
385
419
|
}[];
|
|
386
420
|
alerts?: {
|
|
@@ -400,9 +434,6 @@ interface SentinelConfig {
|
|
|
400
434
|
minKind?: "informational" | "actionable";
|
|
401
435
|
}[];
|
|
402
436
|
};
|
|
403
|
-
alertWebhook?: string;
|
|
404
|
-
baselineWindowDays?: number;
|
|
405
|
-
checkIntervalMs?: number;
|
|
406
437
|
}
|
|
407
438
|
/** A declared task/intent for an agent. */
|
|
408
439
|
interface TaskIntent {
|
|
@@ -447,13 +478,18 @@ interface IntentAlignmentResult {
|
|
|
447
478
|
bypassed?: boolean;
|
|
448
479
|
bypassReason?: string | null;
|
|
449
480
|
}
|
|
450
|
-
/**
|
|
481
|
+
/**
|
|
482
|
+
* Configuration for intent alignment scoring.
|
|
483
|
+
*
|
|
484
|
+
* NOTE: the keywordBoost / acceptableActionBoost / baseScore /
|
|
485
|
+
* missingTaskScore knobs were removed — they were defined, defaulted, and
|
|
486
|
+
* typed but never read by any scoring path (vestiges of a pre-weighted
|
|
487
|
+
* scoring model superseded by SimilarityEngine.keywordSimilarity). They
|
|
488
|
+
* promised tunability that did not exist; the live weights are fixed in
|
|
489
|
+
* SimilarityEngine. defaultTtlMs is the only consumed numeric knob.
|
|
490
|
+
*/
|
|
451
491
|
interface IntentAlignmentConfig {
|
|
452
492
|
defaultTtlMs: number;
|
|
453
|
-
keywordBoost: number;
|
|
454
|
-
acceptableActionBoost: number;
|
|
455
|
-
baseScore: number;
|
|
456
|
-
missingTaskScore: number;
|
|
457
493
|
disableDefaultAcceptable?: boolean;
|
|
458
494
|
denyAcceptable?: string[];
|
|
459
495
|
similarityThreshold?: number;
|
|
@@ -864,6 +900,20 @@ declare class AuditTrail {
|
|
|
864
900
|
* the file when an external append actually happened.
|
|
865
901
|
*/
|
|
866
902
|
private reanchorFromDiskTail;
|
|
903
|
+
/**
|
|
904
|
+
* Scan the on-disk log backward for the last entry carrying a valid (64-hex)
|
|
905
|
+
* hash, SKIPPING torn/corrupt trailing lines, and return that hash (or null
|
|
906
|
+
* when no parseable hashed entry exists).
|
|
907
|
+
*
|
|
908
|
+
* Shared by open() (chain resume) and reanchorFromDiskTail() (live
|
|
909
|
+
* re-anchor). The earlier inline versions inspected only the LAST non-empty
|
|
910
|
+
* line and broke: a torn tail line (a partial write) left the head at genesis
|
|
911
|
+
* (open) or stale (reanchor), so the next append forked a fresh chain from
|
|
912
|
+
* genesis — destroying all prior linkage. Walking back leaves the torn line
|
|
913
|
+
* as a single localized gap that verify() still flags, while the chain head
|
|
914
|
+
* stays anchored to the last good entry.
|
|
915
|
+
*/
|
|
916
|
+
private lastHashOnDisk;
|
|
867
917
|
private readAllEntries;
|
|
868
918
|
private loadCumulativeStats;
|
|
869
919
|
/**
|
|
@@ -916,6 +966,54 @@ interface AgentClassifierConfig {
|
|
|
916
966
|
minDurationMs: number;
|
|
917
967
|
}
|
|
918
968
|
|
|
969
|
+
interface SensitivityRule {
|
|
970
|
+
pattern: string;
|
|
971
|
+
sensitivity: number;
|
|
972
|
+
category: string;
|
|
973
|
+
description: string;
|
|
974
|
+
}
|
|
975
|
+
interface TargetSensitivityResult {
|
|
976
|
+
sensitivity: number;
|
|
977
|
+
category: string;
|
|
978
|
+
description: string;
|
|
979
|
+
matchedRule: string;
|
|
980
|
+
actionMultiplier: number;
|
|
981
|
+
effectiveScore: number;
|
|
982
|
+
groundedInRepoMap: boolean;
|
|
983
|
+
}
|
|
984
|
+
declare class TargetSensitivityScorer {
|
|
985
|
+
private rules;
|
|
986
|
+
private repoMap;
|
|
987
|
+
private repoRoot;
|
|
988
|
+
private overlay;
|
|
989
|
+
constructor(customRules?: SensitivityRule[], repoMap?: RepoSensitivityMap, repoRoot?: string, overlay?: SensitivityOverlay);
|
|
990
|
+
scoreTarget(target: string, action: string): TargetSensitivityResult;
|
|
991
|
+
/** Pattern-only scoring (Tier 2). Used directly by reject decisions. */
|
|
992
|
+
private _scoreByPatterns;
|
|
993
|
+
/**
|
|
994
|
+
* URL-aware component scoring (Sprint 6b W1 fix).
|
|
995
|
+
* Parses the URL, extracts path/query-values/fragment, decodes each once,
|
|
996
|
+
* scores each against forbidden-pattern rules, takes MAX.
|
|
997
|
+
*/
|
|
998
|
+
private _scoreUrlTarget;
|
|
999
|
+
scoreEvent(event: AgentActivityEvent): TargetSensitivityResult;
|
|
1000
|
+
/**
|
|
1001
|
+
* Replaces the active repo sensitivity map. Pass null to clear.
|
|
1002
|
+
* If newRepoRoot is provided, it overrides the map's repoRoot.
|
|
1003
|
+
*/
|
|
1004
|
+
updateRepoMap(newMap: RepoSensitivityMap | null, newRepoRoot?: string): void;
|
|
1005
|
+
/**
|
|
1006
|
+
* Replaces the active sensitivity overlay. Pass null to clear.
|
|
1007
|
+
*/
|
|
1008
|
+
updateOverlay(newOverlay: SensitivityOverlay | null): void;
|
|
1009
|
+
/**
|
|
1010
|
+
* Returns all pattern-based rules that match the target. Does NOT
|
|
1011
|
+
* consult the repo map. Used by RepoSensitivityScanner during
|
|
1012
|
+
* scan-time enumeration.
|
|
1013
|
+
*/
|
|
1014
|
+
getAllMatchingRules(target: string): SensitivityRule[];
|
|
1015
|
+
}
|
|
1016
|
+
|
|
919
1017
|
/** Lazy-init wrapper passed via RoleValidatorOptions so Sentinel can own the cache. */
|
|
920
1018
|
interface ForbiddenInodeCacheRef {
|
|
921
1019
|
getOrBuild: () => Set<bigint>;
|
|
@@ -1229,6 +1327,7 @@ declare class SentinelRunner {
|
|
|
1229
1327
|
private _hookEngine?;
|
|
1230
1328
|
private _maturityConfig?;
|
|
1231
1329
|
private _validatorOptions?;
|
|
1330
|
+
private _sensitivityScorer?;
|
|
1232
1331
|
private intentTracker;
|
|
1233
1332
|
private similarityEngine;
|
|
1234
1333
|
private recentAlignmentScores;
|
|
@@ -1249,6 +1348,12 @@ declare class SentinelRunner {
|
|
|
1249
1348
|
setMaturityConfig(config: Partial<BaselineMaturityConfig>): void;
|
|
1250
1349
|
/** Set RoleValidator options (exception approval fn, audit callback). */
|
|
1251
1350
|
setRoleValidatorOptions(options: RoleValidatorOptions): void;
|
|
1351
|
+
/**
|
|
1352
|
+
* Inject the facade's shared sensitivity scorer (overlay + repo map aware)
|
|
1353
|
+
* so processEvent's validator scores identically to the facade's check().
|
|
1354
|
+
* Must be called BEFORE start() builds the validator.
|
|
1355
|
+
*/
|
|
1356
|
+
setSensitivityScorer(scorer: TargetSensitivityScorer): void;
|
|
1252
1357
|
/** Set the behavioral baseline and start periodic gap checking. */
|
|
1253
1358
|
setBaseline(baseline: AgentBaseline): void;
|
|
1254
1359
|
/** Declare a new task intent for this agent. */
|
|
@@ -1265,6 +1370,12 @@ declare class SentinelRunner {
|
|
|
1265
1370
|
getActiveTask(): TaskIntent | null;
|
|
1266
1371
|
/** Expose the similarity engine for pre-execution intent checks. */
|
|
1267
1372
|
getSimilarityEngine(): SimilarityEngine;
|
|
1373
|
+
/**
|
|
1374
|
+
* Read-only view of the rolling misaligned-score window, for the
|
|
1375
|
+
* pre-execution gate (Sentinel.check) to apply the SAME sustained-drift
|
|
1376
|
+
* severity upgrade as the recording path — without mutating the window.
|
|
1377
|
+
*/
|
|
1378
|
+
getRecentAlignmentScores(): readonly number[];
|
|
1268
1379
|
/**
|
|
1269
1380
|
* Side-effect-free pre-execution gate: evaluate an event and fire pre_execution
|
|
1270
1381
|
* hooks WITHOUT running processEvent's full pipeline.
|
|
@@ -16,6 +16,9 @@ var LogAdapter = class {
|
|
|
16
16
|
pendingFragment = "";
|
|
17
17
|
running = false;
|
|
18
18
|
polling = false;
|
|
19
|
+
/** The in-flight poll (if any), so stop() can let it finish emitting its
|
|
20
|
+
* already-read batch before persisting the cursor. */
|
|
21
|
+
pollInFlight = null;
|
|
19
22
|
pollTimer = null;
|
|
20
23
|
onEvent = null;
|
|
21
24
|
readExisting;
|
|
@@ -33,6 +36,7 @@ var LogAdapter = class {
|
|
|
33
36
|
if (this.running) return;
|
|
34
37
|
this.onEvent = onEvent;
|
|
35
38
|
this.running = true;
|
|
39
|
+
this.pendingFragment = "";
|
|
36
40
|
const savedPosition = await this.loadCursor();
|
|
37
41
|
if (savedPosition !== null) {
|
|
38
42
|
this.lastReadPosition = savedPosition;
|
|
@@ -47,7 +51,9 @@ var LogAdapter = class {
|
|
|
47
51
|
} else {
|
|
48
52
|
this.lastReadPosition = 0;
|
|
49
53
|
}
|
|
50
|
-
this.pollTimer = setInterval(() =>
|
|
54
|
+
this.pollTimer = setInterval(() => {
|
|
55
|
+
this.pollInFlight = this.poll();
|
|
56
|
+
}, this.pollIntervalMs);
|
|
51
57
|
console.log(`LogAdapter watching ${this.logPath} for agent ${this.agentId}`);
|
|
52
58
|
}
|
|
53
59
|
async stop() {
|
|
@@ -55,6 +61,13 @@ var LogAdapter = class {
|
|
|
55
61
|
clearInterval(this.pollTimer);
|
|
56
62
|
this.pollTimer = null;
|
|
57
63
|
}
|
|
64
|
+
if (this.pollInFlight) {
|
|
65
|
+
try {
|
|
66
|
+
await this.pollInFlight;
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
this.pollInFlight = null;
|
|
70
|
+
}
|
|
58
71
|
await this.saveCursor();
|
|
59
72
|
this.running = false;
|
|
60
73
|
this.onEvent = null;
|
|
@@ -66,7 +79,6 @@ var LogAdapter = class {
|
|
|
66
79
|
try {
|
|
67
80
|
const lines = await this.readNewLines();
|
|
68
81
|
for (const line of lines) {
|
|
69
|
-
if (!this.running) break;
|
|
70
82
|
const event = this.parseLine(line);
|
|
71
83
|
if (event) {
|
|
72
84
|
try {
|
|
@@ -194,10 +206,9 @@ var LogAdapter = class {
|
|
|
194
206
|
try {
|
|
195
207
|
const { writeFile, mkdir } = await import("fs/promises");
|
|
196
208
|
await mkdir(this.stateDir, { recursive: true });
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
209
|
+
const fragmentBytes = Buffer.byteLength(this.pendingFragment, "utf-8");
|
|
210
|
+
const position = Math.max(0, this.lastReadPosition - fragmentBytes);
|
|
211
|
+
await writeFile(path, JSON.stringify({ position, logPath: this.logPath }) + "\n");
|
|
201
212
|
} catch (err) {
|
|
202
213
|
console.warn(`Failed to save log cursor: ${err}`);
|
|
203
214
|
}
|
|
@@ -235,4 +246,4 @@ var LogAdapter = class {
|
|
|
235
246
|
export {
|
|
236
247
|
LogAdapter
|
|
237
248
|
};
|
|
238
|
-
//# sourceMappingURL=chunk-
|
|
249
|
+
//# sourceMappingURL=chunk-2IPSTUNH.js.map
|
|
@@ -3,9 +3,8 @@ import {
|
|
|
3
3
|
} from "./chunk-B5QKJHSV.js";
|
|
4
4
|
import {
|
|
5
5
|
discoverPolicy
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-B6S2PBS4.js";
|
|
7
7
|
import {
|
|
8
|
-
DEFAULT_FORBIDDEN_PATTERNS,
|
|
9
8
|
FORBIDDEN_BASENAMES,
|
|
10
9
|
classifyDeny,
|
|
11
10
|
isPositionallySafeMention,
|
|
@@ -14,16 +13,18 @@ import {
|
|
|
14
13
|
scanBashCommand,
|
|
15
14
|
scanContentForForbiddenBasenames,
|
|
16
15
|
scanGlobPattern,
|
|
17
|
-
tokenizePaths
|
|
18
|
-
|
|
16
|
+
tokenizePaths,
|
|
17
|
+
unionWithDefaultForbiddenPatterns
|
|
18
|
+
} from "./chunk-M5EEVMLU.js";
|
|
19
19
|
import {
|
|
20
|
+
DEFAULT_FORBIDDEN_PATTERNS,
|
|
20
21
|
loadPolicy,
|
|
21
22
|
policyToConfig,
|
|
22
23
|
policyToRole
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-SKE74CYZ.js";
|
|
24
25
|
|
|
25
26
|
// src/gateway/workspaceRouter.ts
|
|
26
|
-
import { resolve, dirname } from "path";
|
|
27
|
+
import { resolve, dirname, sep } from "path";
|
|
27
28
|
|
|
28
29
|
// src/mergeRoles.ts
|
|
29
30
|
function isWithinActiveHours(hour, range) {
|
|
@@ -251,15 +252,43 @@ async function resolveWorkspace(cwd, home) {
|
|
|
251
252
|
const policyPath = discoverPolicy(start, home);
|
|
252
253
|
let root = start;
|
|
253
254
|
let workspaceRole = null;
|
|
255
|
+
const warnings = [];
|
|
254
256
|
if (policyPath) {
|
|
255
|
-
|
|
257
|
+
let policy;
|
|
258
|
+
try {
|
|
259
|
+
policy = await loadPolicy(policyPath);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
reason: `the workspace's policy file failed to load (${err instanceof Error ? err.message : String(err)}) \u2014 refusing to serve the workspace without its policy`
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const policyDir = dirname(resolve(policyPath));
|
|
267
|
+
root = policyDir;
|
|
256
268
|
const repoRoot = policyToConfig(policy).repo?.root;
|
|
257
|
-
|
|
269
|
+
if (repoRoot) {
|
|
270
|
+
const declared = resolve(policyDir, repoRoot);
|
|
271
|
+
const homeAbs = resolve(home);
|
|
272
|
+
const declaredPrefix = declared.endsWith(sep) ? declared : declared + sep;
|
|
273
|
+
const containsPolicy = policyDir === declared || policyDir.startsWith(declaredPrefix);
|
|
274
|
+
const aboveHome = declared !== homeAbs && homeAbs.startsWith(declaredPrefix);
|
|
275
|
+
if (!containsPolicy) {
|
|
276
|
+
warnings.push(
|
|
277
|
+
`repo.root "${repoRoot}" (resolves to "${declared}") does not contain the policy file "${policyPath}" \u2014 anchoring to the policy's own directory "${policyDir}" instead`
|
|
278
|
+
);
|
|
279
|
+
} else if (aboveHome) {
|
|
280
|
+
warnings.push(
|
|
281
|
+
`repo.root "${repoRoot}" (resolves to "${declared}") reaches above the home directory \u2014 anchoring to the policy's own directory "${policyDir}" instead`
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
284
|
+
root = declared;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
258
287
|
workspaceRole = policyToRole(policy);
|
|
259
288
|
}
|
|
260
289
|
return {
|
|
261
290
|
ok: true,
|
|
262
|
-
resolution: { root, agentId: deriveAgentId(root), policyPath, workspaceRole }
|
|
291
|
+
resolution: { root, agentId: deriveAgentId(root), policyPath, workspaceRole, warnings }
|
|
263
292
|
};
|
|
264
293
|
}
|
|
265
294
|
function effectiveRole(ceiling, workspaceRole) {
|
|
@@ -347,11 +376,11 @@ var TranslatorRegistry = class {
|
|
|
347
376
|
|
|
348
377
|
// src/gateway/runtimeConstructionResolvers.ts
|
|
349
378
|
var MAX_RECURSION_DEPTH = 3;
|
|
350
|
-
var INTERPRETER_RE = /\b(?:python
|
|
379
|
+
var INTERPRETER_RE = /\b(?:python|node|ruby|perl|php)[0-9.]*\s+(?:\S+\s+)*?-[cer]/;
|
|
351
380
|
var NESTED_ENCODING_RE = /chr\(\d+\)|String\.fromCharCode|\\x[0-9a-fA-F]{2}|\\[0-7]{1,3}|printf\s/;
|
|
352
381
|
var PRINTF_HEX_RE = /printf\s+['"]?[^'"]*\\x[0-9a-fA-F]{2}/;
|
|
353
382
|
var PRINTF_OCT_RE = /printf\s+['"]?[^'"]*\\[0-7]{1,3}/;
|
|
354
|
-
var B1_CONTEXT_RE = /(?:\bbase64\s+(
|
|
383
|
+
var B1_CONTEXT_RE = /(?:\bbase64\s+(?:--decode\b|-[a-zA-Z]*[dD][a-zA-Z]*\b)|\bopenssl\s+(?:enc\s+)?(?:-?base64\s+(?:-\w+\s+)*-d|-d\s+(?:-\w+\s+)*-?base64)\b)/;
|
|
355
384
|
var ECHO_E_HEX_RE = /\becho\s+(?:-\w+\s+)*-\w*e\w*\b[^|;&\n]*\\x[0-9a-fA-F]{2}/;
|
|
356
385
|
var ANSI_C_QUOTE_HEX_RE = /\$'[^']*\\x[0-9a-fA-F]{2}/;
|
|
357
386
|
var ANSI_C_QUOTE_OCT_RE = /\$'[^']*\\[0-7]{1,3}/;
|
|
@@ -1032,17 +1061,25 @@ var ClaudeCodeTranslator = class {
|
|
|
1032
1061
|
};
|
|
1033
1062
|
|
|
1034
1063
|
// src/gateway/grepRewriter.ts
|
|
1064
|
+
function toRelativeExclusionGlob(pattern) {
|
|
1065
|
+
return pattern.startsWith("/") ? "**" + pattern : pattern;
|
|
1066
|
+
}
|
|
1035
1067
|
function buildGrepExclusions(forbiddenPatterns) {
|
|
1036
1068
|
const flags = [];
|
|
1037
1069
|
for (const pattern of forbiddenPatterns) {
|
|
1038
|
-
flags.push("--glob", `!${pattern}`);
|
|
1070
|
+
flags.push("--glob", `!${toRelativeExclusionGlob(pattern)}`);
|
|
1039
1071
|
}
|
|
1040
1072
|
return flags;
|
|
1041
1073
|
}
|
|
1042
1074
|
function isPathItselfForbidden(searchPath, forbiddenPatterns) {
|
|
1043
1075
|
const matched = [];
|
|
1044
1076
|
for (const pattern of forbiddenPatterns) {
|
|
1045
|
-
if (matchGlobInsensitive(pattern, searchPath))
|
|
1077
|
+
if (matchGlobInsensitive(pattern, searchPath) || // A directory-glob (`…/**`, e.g. `**/.ssh/**` or `/etc/**`) does NOT match
|
|
1078
|
+
// the bare directory itself — the trailing `/.*` requires a char after the
|
|
1079
|
+
// slash, so `matchGlob('**/.ssh/**', '/home/u/.ssh')` is false. When the
|
|
1080
|
+
// SEARCH PATH *is* the forbidden directory, treat it as forbidden so it
|
|
1081
|
+
// routes to DENY (no rewrite), not MODIFY.
|
|
1082
|
+
pattern.endsWith("/**") && matchGlobInsensitive(pattern.slice(0, -3), searchPath)) {
|
|
1046
1083
|
matched.push(pattern);
|
|
1047
1084
|
}
|
|
1048
1085
|
}
|
|
@@ -1116,6 +1153,8 @@ var SentinelGateway = class {
|
|
|
1116
1153
|
releaseToken;
|
|
1117
1154
|
/** Item D (F-8): disposition for unknown (non-MCP, unrecognized) tool names. */
|
|
1118
1155
|
unknownTools;
|
|
1156
|
+
/** Posture for an internal wrap() error: allow (fail-open, default) or block. */
|
|
1157
|
+
onInternalError;
|
|
1119
1158
|
/** Daemon-staleness build identity (content hash of the launched-from entry),
|
|
1120
1159
|
* reported via /health. "unknown" when not supplied by the launcher. */
|
|
1121
1160
|
buildId;
|
|
@@ -1134,6 +1173,7 @@ var SentinelGateway = class {
|
|
|
1134
1173
|
this.home = options.home ?? "";
|
|
1135
1174
|
this.releaseToken = options.releaseToken ?? null;
|
|
1136
1175
|
this.unknownTools = options.unknownTools ?? "warn";
|
|
1176
|
+
this.onInternalError = options.onInternalError ?? "fail-open";
|
|
1137
1177
|
this.buildId = options.buildId ?? "unknown";
|
|
1138
1178
|
const internal = options;
|
|
1139
1179
|
if (internal.registry) {
|
|
@@ -1424,6 +1464,9 @@ var SentinelGateway = class {
|
|
|
1424
1464
|
return { ok: false, finding };
|
|
1425
1465
|
}
|
|
1426
1466
|
const { root, agentId, workspaceRole } = resolved.resolution;
|
|
1467
|
+
for (const w of resolved.resolution.warnings) {
|
|
1468
|
+
console.warn(`[SENTINEL GATEWAY] workspace-resolution warning (${agentId}): ${w}`);
|
|
1469
|
+
}
|
|
1427
1470
|
const ceiling = this.operatorCeiling;
|
|
1428
1471
|
if (!ceiling) {
|
|
1429
1472
|
const check = {
|
|
@@ -1450,7 +1493,24 @@ var SentinelGateway = class {
|
|
|
1450
1493
|
name: agentId
|
|
1451
1494
|
});
|
|
1452
1495
|
this.sentinel.touchAgent(agentId);
|
|
1453
|
-
return { ok: true, agentId };
|
|
1496
|
+
return { ok: true, agentId, effectiveRole: merged.role };
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Forbidden-pattern list for ONE request: the gateway's construction-time
|
|
1500
|
+
* list unioned with the request's merged-role patterns (operator-ceiling ∩
|
|
1501
|
+
* workspace yaml). Without this, a workspace's custom forbid targets were
|
|
1502
|
+
* enforced by the role validator inside wrap() but never reached the
|
|
1503
|
+
* gateway's own bash-L1 / Grep layers, which checked only the static list.
|
|
1504
|
+
* Normalization is idempotent (the chokepoint globstar-prepend).
|
|
1505
|
+
*/
|
|
1506
|
+
patternsForRequest(effectiveRole2) {
|
|
1507
|
+
if (!effectiveRole2?.forbiddenTargetPatterns?.length) return this.forbiddenPatterns;
|
|
1508
|
+
return [
|
|
1509
|
+
.../* @__PURE__ */ new Set([
|
|
1510
|
+
...this.forbiddenPatterns,
|
|
1511
|
+
...effectiveRole2.forbiddenTargetPatterns.map(normalizeForbiddenPattern)
|
|
1512
|
+
])
|
|
1513
|
+
];
|
|
1454
1514
|
}
|
|
1455
1515
|
async handlePreToolUse(body, res, translator) {
|
|
1456
1516
|
let payload;
|
|
@@ -1460,12 +1520,43 @@ var SentinelGateway = class {
|
|
|
1460
1520
|
this.sendJson(res, 400, { error: "invalid JSON" });
|
|
1461
1521
|
return;
|
|
1462
1522
|
}
|
|
1463
|
-
|
|
1523
|
+
let event;
|
|
1524
|
+
try {
|
|
1525
|
+
event = translator.translatePreToolUse(payload);
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
console.error("[SENTINEL GATEWAY] translator error (pre-tool-use):", err);
|
|
1528
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1529
|
+
const finding = {
|
|
1530
|
+
severity: "HIGH",
|
|
1531
|
+
kind: "actionable",
|
|
1532
|
+
type: "enforcement_error",
|
|
1533
|
+
agentId: this.agentId,
|
|
1534
|
+
agentName: this.agentId,
|
|
1535
|
+
description: `Enforcement error \u2014 the ${translator.agentType} translator threw while parsing a pre-tool-use payload: ${err instanceof Error ? err.message : String(err)}. The request was answered with an error (HTTP 500), not an allow.`,
|
|
1536
|
+
evidence: {
|
|
1537
|
+
action: "tool_invocation",
|
|
1538
|
+
target: "(untranslatable pre-tool-use payload)",
|
|
1539
|
+
timestamp: ts,
|
|
1540
|
+
baselineComparison: "enforcement_internal_error"
|
|
1541
|
+
},
|
|
1542
|
+
recommendation: "Investigate the gateway daemon log for the underlying translator exception \u2014 the payload could not be screened.",
|
|
1543
|
+
timestamp: ts,
|
|
1544
|
+
decision: "deny"
|
|
1545
|
+
};
|
|
1546
|
+
try {
|
|
1547
|
+
await this.sentinel.logFinding(this.agentId, finding);
|
|
1548
|
+
} catch (logErr) {
|
|
1549
|
+
console.error("[SENTINEL GATEWAY] failed to persist enforcement_error finding:", logErr);
|
|
1550
|
+
}
|
|
1551
|
+
this.sendJson(res, 500, { error: "translator error" });
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1464
1554
|
if (!event) {
|
|
1465
1555
|
this.sendJson(res, 400, { error: "unable to translate payload" });
|
|
1466
1556
|
return;
|
|
1467
1557
|
}
|
|
1468
1558
|
let routingId = this.agentId;
|
|
1559
|
+
let requestForbiddenPatterns = this.forbiddenPatterns;
|
|
1469
1560
|
if (this.workspaceIsolation) {
|
|
1470
1561
|
const routed = await this.resolveBPathRouting(
|
|
1471
1562
|
event.metadata?.cwd,
|
|
@@ -1483,6 +1574,7 @@ var SentinelGateway = class {
|
|
|
1483
1574
|
}
|
|
1484
1575
|
routingId = routed.agentId;
|
|
1485
1576
|
event.agentId = routingId;
|
|
1577
|
+
requestForbiddenPatterns = this.patternsForRequest(routed.effectiveRole);
|
|
1486
1578
|
}
|
|
1487
1579
|
if (event.metadata?._unknownTool === "true") {
|
|
1488
1580
|
const unknownName = event.metadata.ccToolName ?? event.primaryTarget;
|
|
@@ -1555,7 +1647,7 @@ var SentinelGateway = class {
|
|
|
1555
1647
|
let matchedPath = null;
|
|
1556
1648
|
let matchedPattern = null;
|
|
1557
1649
|
for (const tokenPath of allTokenPaths) {
|
|
1558
|
-
for (const pattern of
|
|
1650
|
+
for (const pattern of requestForbiddenPatterns) {
|
|
1559
1651
|
if (matchGlobInsensitive(pattern, tokenPath)) {
|
|
1560
1652
|
matchedPath = tokenPath;
|
|
1561
1653
|
matchedPattern = pattern;
|
|
@@ -1737,7 +1829,7 @@ var SentinelGateway = class {
|
|
|
1737
1829
|
if (ccToolName === "Grep") {
|
|
1738
1830
|
const toolInput = parsedPayload.tool_input ?? {};
|
|
1739
1831
|
const searchPath = typeof toolInput.path === "string" && toolInput.path.length > 0 ? toolInput.path : parsedPayload.cwd ?? ".";
|
|
1740
|
-
const pathCheck = isPathItselfForbidden(searchPath,
|
|
1832
|
+
const pathCheck = isPathItselfForbidden(searchPath, requestForbiddenPatterns);
|
|
1741
1833
|
if (pathCheck.forbidden) {
|
|
1742
1834
|
const finding2 = {
|
|
1743
1835
|
severity: "HIGH",
|
|
@@ -1762,7 +1854,7 @@ var SentinelGateway = class {
|
|
|
1762
1854
|
this.sendJson(res, 200, response);
|
|
1763
1855
|
return;
|
|
1764
1856
|
}
|
|
1765
|
-
const exclusions = buildGrepExclusions(
|
|
1857
|
+
const exclusions = buildGrepExclusions(requestForbiddenPatterns);
|
|
1766
1858
|
const updatedInput = buildModifiedGrepInput(toolInput, exclusions);
|
|
1767
1859
|
const finding = {
|
|
1768
1860
|
severity: "MEDIUM",
|
|
@@ -1785,7 +1877,7 @@ var SentinelGateway = class {
|
|
|
1785
1877
|
await this.sentinel.logFinding(routingId, finding);
|
|
1786
1878
|
this.telemetry.recordToolCall(event.action, "pre", "allowed", 0);
|
|
1787
1879
|
const ccTranslator = translator;
|
|
1788
|
-
const excludedPatterns =
|
|
1880
|
+
const excludedPatterns = requestForbiddenPatterns.join(", ");
|
|
1789
1881
|
const additionalContext = `Sentinel: this Grep search was modified to exclude forbidden paths. Excluded patterns: ${excludedPatterns}. To search specific forbidden files, use Read with explicit approval.`;
|
|
1790
1882
|
if (typeof ccTranslator.formatPreToolUseModifyResponse === "function") {
|
|
1791
1883
|
const response = ccTranslator.formatPreToolUseModifyResponse({
|
|
@@ -1883,7 +1975,37 @@ var SentinelGateway = class {
|
|
|
1883
1975
|
this.sendJson(res, 200, response);
|
|
1884
1976
|
} catch (err) {
|
|
1885
1977
|
console.error("[SENTINEL GATEWAY] wrap() error:", err);
|
|
1886
|
-
const
|
|
1978
|
+
const failClosed = this.onInternalError === "fail-closed";
|
|
1979
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
1980
|
+
const finding = {
|
|
1981
|
+
severity: "HIGH",
|
|
1982
|
+
kind: "actionable",
|
|
1983
|
+
type: "enforcement_error",
|
|
1984
|
+
agentId: routingId,
|
|
1985
|
+
agentName: event.agentName,
|
|
1986
|
+
description: `Enforcement error \u2014 sentinel.wrap() threw while screening "${event.action}": ${errMessage}. The call was ${failClosed ? "BLOCKED" : "ALLOWED WITHOUT SCREENING"} (enforcement.onInternalError: ${this.onInternalError}).`,
|
|
1987
|
+
evidence: {
|
|
1988
|
+
action: event.action,
|
|
1989
|
+
target: event.primaryTarget,
|
|
1990
|
+
timestamp: event.timestamp,
|
|
1991
|
+
baselineComparison: "enforcement_internal_error"
|
|
1992
|
+
},
|
|
1993
|
+
recommendation: `Investigate the gateway daemon log for the underlying exception \u2014 Sentinel's own enforcement failed, so this call was decided by posture, not policy. Set enforcement.onInternalError: "fail-closed" to block instead of allow while the cause is open.`,
|
|
1994
|
+
timestamp: event.timestamp,
|
|
1995
|
+
decision: failClosed ? "deny" : "allow"
|
|
1996
|
+
};
|
|
1997
|
+
try {
|
|
1998
|
+
await this.sentinel.logFinding(routingId, finding);
|
|
1999
|
+
} catch (logErr) {
|
|
2000
|
+
console.error("[SENTINEL GATEWAY] failed to persist enforcement_error finding:", logErr);
|
|
2001
|
+
}
|
|
2002
|
+
this.telemetry.recordToolCall(
|
|
2003
|
+
event.action,
|
|
2004
|
+
"pre",
|
|
2005
|
+
failClosed ? "blocked" : "allowed",
|
|
2006
|
+
Date.now() - start
|
|
2007
|
+
);
|
|
2008
|
+
const response = failClosed ? translator.formatPreToolUseResponse({ blocked: true, finding }) : translator.formatPreToolUseResponse({ blocked: false });
|
|
1887
2009
|
this.sendJson(res, 200, response);
|
|
1888
2010
|
}
|
|
1889
2011
|
}
|
|
@@ -2052,11 +2174,11 @@ async function runGatewayDaemon({
|
|
|
2052
2174
|
port = DEFAULT_PORT,
|
|
2053
2175
|
buildId
|
|
2054
2176
|
}) {
|
|
2055
|
-
const { Sentinel: SentinelClass } = await import("./Sentinel-
|
|
2177
|
+
const { Sentinel: SentinelClass } = await import("./Sentinel-XP6NFG6Z.js");
|
|
2056
2178
|
const { writePidFile, writeReleaseToken } = await import("./pidManager-DOGVN6ZT.js");
|
|
2057
2179
|
const { homedir } = await import("os");
|
|
2058
2180
|
const { randomBytes } = await import("crypto");
|
|
2059
|
-
const { loadPolicy: loadPolicy2, policyToRole: policyToRole2, policyToConfig: policyToConfig2 } = await import("./policyLoader-
|
|
2181
|
+
const { loadPolicy: loadPolicy2, policyToRole: policyToRole2, policyToConfig: policyToConfig2 } = await import("./policyLoader-NUPBBRKH.js");
|
|
2060
2182
|
const sentinel = await SentinelClass.fromPolicy(policyPath);
|
|
2061
2183
|
const baseline = await sentinel.computeBaseline("claude-code");
|
|
2062
2184
|
sentinel.setBaseline("claude-code", baseline);
|
|
@@ -2075,7 +2197,12 @@ async function runGatewayDaemon({
|
|
|
2075
2197
|
releaseToken,
|
|
2076
2198
|
unknownTools: operatorConfig.enforcement?.unknownTools,
|
|
2077
2199
|
allowUnknownTools: operatorConfig.enforcement?.allowUnknownTools,
|
|
2078
|
-
|
|
2200
|
+
onInternalError: operatorConfig.enforcement?.onInternalError,
|
|
2201
|
+
buildId,
|
|
2202
|
+
// The operator policy's custom forbid targets must reach the gateway's own
|
|
2203
|
+
// bash-L1/Grep layers (defaults-as-floor union, same chokepoint as role
|
|
2204
|
+
// construction) — previously the gateway saw only the built-in defaults.
|
|
2205
|
+
forbiddenPatterns: unionWithDefaultForbiddenPatterns(operatorCeiling.forbiddenTargetPatterns)
|
|
2079
2206
|
});
|
|
2080
2207
|
await gateway.start();
|
|
2081
2208
|
const home = homedir();
|
|
@@ -2088,4 +2215,4 @@ export {
|
|
|
2088
2215
|
SentinelGateway,
|
|
2089
2216
|
runGatewayDaemon
|
|
2090
2217
|
};
|
|
2091
|
-
//# sourceMappingURL=chunk-
|
|
2218
|
+
//# sourceMappingURL=chunk-3WT3K5TH.js.map
|