@tuent/sentinel 0.1.0 → 0.1.2
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/README.md +26 -26
- package/SECURITY_MODEL.md +281 -0
- package/dist/Sentinel-XMSJE4DZ.js +10 -0
- package/dist/{Sentinel-B_sv8Kiy.d.ts → Sentinel-xFCyXH45.d.ts} +31 -1
- package/dist/chunk-B5QKJHSV.js +32 -0
- package/dist/{chunk-3U3PKD4N.js → chunk-FWIISAZZ.js} +119 -8
- package/dist/{chunk-QFRDEISP.js → chunk-GRN5P3H2.js} +67 -23
- package/dist/{chunk-Z3PWIJKT.js → chunk-L4R3LPJS.js} +237 -443
- package/dist/{chunk-CUJKNIKT.js → chunk-LATQNIRW.js} +33 -1
- package/dist/{chunk-6MHWJATS.js → chunk-QIYQWOLO.js} +589 -19
- package/dist/{chunk-2FFMYSVC.js → chunk-WLIDSTS4.js} +18 -2
- package/dist/cli.js +30 -30
- package/dist/gateway/index.d.ts +37 -1
- package/dist/gateway/index.js +4 -3
- package/dist/gatewayDaemon.js +4 -3
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -5
- package/dist/pidManager-DOGVN6ZT.js +23 -0
- package/dist/{policyLoader-6KR5VFVV.js → policyLoader-KZL2U4M2.js} +2 -2
- package/package.json +3 -2
- package/dist/Sentinel-JLQL3YRD.js +0 -10
- package/dist/pidManager-ZYC7SICM.js +0 -15
|
@@ -1,48 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deriveAgentId
|
|
3
|
+
} from "./chunk-B5QKJHSV.js";
|
|
1
4
|
import {
|
|
2
5
|
discoverPolicy
|
|
3
6
|
} from "./chunk-FMZWHT4M.js";
|
|
4
7
|
import {
|
|
5
8
|
DEFAULT_FORBIDDEN_PATTERNS,
|
|
9
|
+
FORBIDDEN_BASENAMES,
|
|
10
|
+
classifyDeny,
|
|
11
|
+
isPositionallySafeMention,
|
|
6
12
|
matchGlobInsensitive,
|
|
7
|
-
normalizeForbiddenPattern
|
|
8
|
-
|
|
13
|
+
normalizeForbiddenPattern,
|
|
14
|
+
scanBashCommand,
|
|
15
|
+
scanContentForForbiddenBasenames,
|
|
16
|
+
scanGlobPattern,
|
|
17
|
+
tokenizePaths
|
|
18
|
+
} from "./chunk-QIYQWOLO.js";
|
|
9
19
|
import {
|
|
10
20
|
loadPolicy,
|
|
11
21
|
policyToConfig,
|
|
12
22
|
policyToRole
|
|
13
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-WLIDSTS4.js";
|
|
14
24
|
|
|
15
25
|
// src/gateway/workspaceRouter.ts
|
|
16
26
|
import { resolve, dirname } from "path";
|
|
17
27
|
|
|
18
|
-
// src/workspaceIdentity.ts
|
|
19
|
-
var AGENT_PREFIX = "claude-code";
|
|
20
|
-
function fnv1a32Hex(s) {
|
|
21
|
-
let h = 2166136261;
|
|
22
|
-
for (let i = 0; i < s.length; i++) {
|
|
23
|
-
h ^= s.charCodeAt(i);
|
|
24
|
-
h = Math.imul(h, 16777619);
|
|
25
|
-
}
|
|
26
|
-
return (h >>> 0).toString(16).padStart(8, "0");
|
|
27
|
-
}
|
|
28
|
-
function lastSegment(path) {
|
|
29
|
-
const parts = path.split("/").filter(Boolean);
|
|
30
|
-
return parts.length > 0 ? parts[parts.length - 1] : "";
|
|
31
|
-
}
|
|
32
|
-
function slugify(s) {
|
|
33
|
-
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
34
|
-
}
|
|
35
|
-
function normalizeRoot(root) {
|
|
36
|
-
if (root === "" || root === "/") return root;
|
|
37
|
-
return root.replace(/\/+$/, "") || "/";
|
|
38
|
-
}
|
|
39
|
-
function deriveAgentId(workspaceRoot) {
|
|
40
|
-
const root = normalizeRoot(workspaceRoot);
|
|
41
|
-
const slug = slugify(lastSegment(root)) || "root";
|
|
42
|
-
const hash = fnv1a32Hex(root);
|
|
43
|
-
return `${AGENT_PREFIX}@${slug}-${hash}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
28
|
// src/mergeRoles.ts
|
|
47
29
|
function isWithinActiveHours(hour, range) {
|
|
48
30
|
const [startHour, endHour] = range;
|
|
@@ -363,393 +345,6 @@ var TranslatorRegistry = class {
|
|
|
363
345
|
}
|
|
364
346
|
};
|
|
365
347
|
|
|
366
|
-
// src/gateway/bashScanner.ts
|
|
367
|
-
import { parse as shellParse } from "shell-quote";
|
|
368
|
-
import { realpathSync } from "fs";
|
|
369
|
-
import { dirname as dirname2, join, basename, normalize } from "path";
|
|
370
|
-
var BRACE_PATTERN_RE = /\{[^}]*,[^}]*\}/;
|
|
371
|
-
var MAX_BRACE_EXPANSION = 64;
|
|
372
|
-
function fnmatchBasename(pattern, candidate) {
|
|
373
|
-
if (pattern.length !== candidate.length) return false;
|
|
374
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
375
|
-
const p = pattern[i].toLowerCase();
|
|
376
|
-
const c = candidate[i].toLowerCase();
|
|
377
|
-
if (p === "?") continue;
|
|
378
|
-
if (p !== c) return false;
|
|
379
|
-
}
|
|
380
|
-
return true;
|
|
381
|
-
}
|
|
382
|
-
function bracketTokenMatchesForbidden(token, forbiddenBasenames) {
|
|
383
|
-
const literals = [];
|
|
384
|
-
let current = "";
|
|
385
|
-
let inBracket = false;
|
|
386
|
-
for (let i = 0; i < token.length; i++) {
|
|
387
|
-
if (token[i] === "[" && !inBracket) {
|
|
388
|
-
if (current) literals.push(current);
|
|
389
|
-
current = "";
|
|
390
|
-
inBracket = true;
|
|
391
|
-
} else if (token[i] === "]" && inBracket) {
|
|
392
|
-
inBracket = false;
|
|
393
|
-
} else if (!inBracket) {
|
|
394
|
-
current += token[i];
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (current) literals.push(current);
|
|
398
|
-
for (const forbidden of forbiddenBasenames) {
|
|
399
|
-
const fl = forbidden.toLowerCase();
|
|
400
|
-
for (const lit of literals) {
|
|
401
|
-
if (lit.length === 0) continue;
|
|
402
|
-
if (fl.includes(lit.toLowerCase())) return forbidden;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
function resolveBraceExpansion(token) {
|
|
408
|
-
const match = token.match(/^(.*?)\{([^}]*,[^}]*)\}(.*)$/);
|
|
409
|
-
if (!match) return null;
|
|
410
|
-
const [, prefix, alternatives, suffix] = match;
|
|
411
|
-
const parts = alternatives.split(",");
|
|
412
|
-
if (parts.length > MAX_BRACE_EXPANSION) return null;
|
|
413
|
-
return parts.map((p) => prefix + p + suffix);
|
|
414
|
-
}
|
|
415
|
-
function wildcardDispatch(token, forbiddenBasenames, metadataField) {
|
|
416
|
-
const result = {
|
|
417
|
-
resolvedBasenames: [],
|
|
418
|
-
unparseable: false,
|
|
419
|
-
metadata: {}
|
|
420
|
-
};
|
|
421
|
-
if (token === "*" || token === "**" || token === "?") {
|
|
422
|
-
return result;
|
|
423
|
-
}
|
|
424
|
-
if (BRACE_PATTERN_RE.test(token)) {
|
|
425
|
-
const expanded = resolveBraceExpansion(token);
|
|
426
|
-
if (expanded === null) {
|
|
427
|
-
result.unparseable = true;
|
|
428
|
-
return result;
|
|
429
|
-
}
|
|
430
|
-
for (const alt of expanded) {
|
|
431
|
-
const hasWildcard = /[?*[]/.test(alt);
|
|
432
|
-
if (hasWildcard) {
|
|
433
|
-
const sub = wildcardDispatch(alt, forbiddenBasenames, metadataField);
|
|
434
|
-
if (sub.resolvedBasenames.length > 0) {
|
|
435
|
-
result.resolvedBasenames.push(...sub.resolvedBasenames);
|
|
436
|
-
result.metadata["resolvedFromBrace"] = token;
|
|
437
|
-
Object.assign(result.metadata, sub.metadata);
|
|
438
|
-
}
|
|
439
|
-
if (sub.unparseable) result.unparseable = true;
|
|
440
|
-
} else {
|
|
441
|
-
const altLower = alt.toLowerCase();
|
|
442
|
-
for (const forbidden of forbiddenBasenames) {
|
|
443
|
-
if (altLower === forbidden.toLowerCase()) {
|
|
444
|
-
result.resolvedBasenames.push(forbidden);
|
|
445
|
-
result.metadata["resolvedFromBrace"] = token;
|
|
446
|
-
break;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
return result;
|
|
452
|
-
}
|
|
453
|
-
const hasStar = token.includes("*");
|
|
454
|
-
const hasQuestion = token.includes("?");
|
|
455
|
-
const hasBracket = token.includes("[");
|
|
456
|
-
if (hasBracket) {
|
|
457
|
-
const matched = bracketTokenMatchesForbidden(token, forbiddenBasenames);
|
|
458
|
-
if (matched) {
|
|
459
|
-
result.resolvedBasenames.push(matched);
|
|
460
|
-
result.metadata["resolvedFromBracket"] = token;
|
|
461
|
-
} else {
|
|
462
|
-
result.unparseable = true;
|
|
463
|
-
}
|
|
464
|
-
return result;
|
|
465
|
-
}
|
|
466
|
-
if (hasStar && !hasQuestion) {
|
|
467
|
-
const matched = starLiteralSubstringCheck(token, forbiddenBasenames);
|
|
468
|
-
if (matched) {
|
|
469
|
-
result.resolvedBasenames.push(matched);
|
|
470
|
-
result.metadata[metadataField] = token;
|
|
471
|
-
}
|
|
472
|
-
return result;
|
|
473
|
-
}
|
|
474
|
-
if (hasQuestion && !hasStar) {
|
|
475
|
-
for (const forbidden of forbiddenBasenames) {
|
|
476
|
-
if (fnmatchBasename(token, forbidden)) {
|
|
477
|
-
result.resolvedBasenames.push(forbidden);
|
|
478
|
-
result.metadata[metadataField] = token;
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return result;
|
|
483
|
-
}
|
|
484
|
-
if (hasStar && hasQuestion) {
|
|
485
|
-
const starMatch = starLiteralSubstringCheck(token, forbiddenBasenames);
|
|
486
|
-
if (starMatch) {
|
|
487
|
-
result.resolvedBasenames.push(starMatch);
|
|
488
|
-
result.metadata[metadataField] = token;
|
|
489
|
-
return result;
|
|
490
|
-
}
|
|
491
|
-
const segments = token.split("*").filter((s) => s.includes("?"));
|
|
492
|
-
for (const seg of segments) {
|
|
493
|
-
for (const forbidden of forbiddenBasenames) {
|
|
494
|
-
if (fnmatchBasename(seg, forbidden)) {
|
|
495
|
-
result.resolvedBasenames.push(forbidden);
|
|
496
|
-
result.metadata[metadataField] = token;
|
|
497
|
-
return result;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return result;
|
|
502
|
-
}
|
|
503
|
-
return result;
|
|
504
|
-
}
|
|
505
|
-
function starLiteralSubstringCheck(token, forbiddenBasenames) {
|
|
506
|
-
const literals = token.split("*").filter((s) => s.length > 0);
|
|
507
|
-
for (const forbidden of forbiddenBasenames) {
|
|
508
|
-
const fl = forbidden.toLowerCase();
|
|
509
|
-
for (const lit of literals) {
|
|
510
|
-
if (fl.includes(lit.toLowerCase())) return forbidden;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
function shouldDispatchWildcard(token) {
|
|
516
|
-
const hasMetachar = /[?*[{]/.test(token);
|
|
517
|
-
if (!hasMetachar) return false;
|
|
518
|
-
if (isPathShaped(token)) return true;
|
|
519
|
-
if (token.includes("[")) return true;
|
|
520
|
-
if (BRACE_PATTERN_RE.test(token)) return true;
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
var SENSITIVE_BASENAME_RE = /(?:\.env|\.ssh|secrets|credentials|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.pem|\.key)/i;
|
|
524
|
-
var DANGEROUS_COMMAND_TOKENS = /* @__PURE__ */ new Set(["eval"]);
|
|
525
|
-
var COMMAND_SUBSTITUTION_RE = /\$\(|`/;
|
|
526
|
-
var DANGEROUS_RAW_RE = /<<<|<\(|>\(/;
|
|
527
|
-
function isVarMarker(token) {
|
|
528
|
-
return typeof token === "object" && token !== null && "__sentinel_var" in token && typeof token.__sentinel_var === "string";
|
|
529
|
-
}
|
|
530
|
-
function tokenizePaths(command) {
|
|
531
|
-
const result = {
|
|
532
|
-
paths: [],
|
|
533
|
-
unparseable: false,
|
|
534
|
-
hasDangerousConstruct: false
|
|
535
|
-
};
|
|
536
|
-
if (DANGEROUS_RAW_RE.test(command)) {
|
|
537
|
-
result.hasDangerousConstruct = true;
|
|
538
|
-
}
|
|
539
|
-
if (COMMAND_SUBSTITUTION_RE.test(command)) {
|
|
540
|
-
result.hasDangerousConstruct = true;
|
|
541
|
-
}
|
|
542
|
-
let tokens;
|
|
543
|
-
try {
|
|
544
|
-
tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
|
|
545
|
-
} catch {
|
|
546
|
-
result.unparseable = true;
|
|
547
|
-
return result;
|
|
548
|
-
}
|
|
549
|
-
if (!Array.isArray(tokens)) {
|
|
550
|
-
result.unparseable = true;
|
|
551
|
-
return result;
|
|
552
|
-
}
|
|
553
|
-
let prevToken = null;
|
|
554
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
555
|
-
const token = tokens[i];
|
|
556
|
-
if (isVarMarker(token)) {
|
|
557
|
-
const nextToken = tokens[i + 1];
|
|
558
|
-
const nextIsPathRelevant = nextToken === void 0 || // end of tokens — var is complete argument
|
|
559
|
-
typeof nextToken === "object" && nextToken !== null && "op" in nextToken || // followed by operator — var is complete argument
|
|
560
|
-
typeof nextToken === "string" && isPathShaped(nextToken);
|
|
561
|
-
const prevIsPathRelevant = prevToken !== null && isPathShaped(prevToken);
|
|
562
|
-
if (nextIsPathRelevant || prevIsPathRelevant) {
|
|
563
|
-
result.unparseable = true;
|
|
564
|
-
}
|
|
565
|
-
prevToken = null;
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
if (typeof token === "object" && token !== null) {
|
|
569
|
-
if ("pattern" in token) {
|
|
570
|
-
const globPattern = token.pattern;
|
|
571
|
-
const lastSlash = globPattern.lastIndexOf("/");
|
|
572
|
-
const dispatchTarget = lastSlash >= 0 ? globPattern.slice(lastSlash + 1) : globPattern;
|
|
573
|
-
const dispatch = wildcardDispatch(dispatchTarget, FORBIDDEN_BASENAMES, "resolvedFromGlob");
|
|
574
|
-
if (dispatch.resolvedBasenames.length > 0) {
|
|
575
|
-
for (const resolved of dispatch.resolvedBasenames) {
|
|
576
|
-
result.paths.push(resolved);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
if (dispatch.unparseable) {
|
|
580
|
-
result.unparseable = true;
|
|
581
|
-
}
|
|
582
|
-
if (SENSITIVE_BASENAME_RE.test(globPattern)) {
|
|
583
|
-
result.unparseable = true;
|
|
584
|
-
}
|
|
585
|
-
prevToken = null;
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
if ("op" in token) {
|
|
589
|
-
if (token.op === "<(") {
|
|
590
|
-
result.hasDangerousConstruct = true;
|
|
591
|
-
}
|
|
592
|
-
prevToken = null;
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
prevToken = null;
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
if (typeof token !== "string") {
|
|
599
|
-
prevToken = null;
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
if (DANGEROUS_COMMAND_TOKENS.has(token.toLowerCase())) {
|
|
603
|
-
result.hasDangerousConstruct = true;
|
|
604
|
-
}
|
|
605
|
-
if ((prevToken === "sh" || prevToken === "bash" || prevToken === "/bin/sh" || prevToken === "/bin/bash") && token === "-c") {
|
|
606
|
-
result.hasDangerousConstruct = true;
|
|
607
|
-
}
|
|
608
|
-
if (shouldDispatchWildcard(token)) {
|
|
609
|
-
const metaField = "resolvedFromQuotedGlob";
|
|
610
|
-
const dispatch = wildcardDispatch(token, FORBIDDEN_BASENAMES, metaField);
|
|
611
|
-
if (dispatch.resolvedBasenames.length > 0) {
|
|
612
|
-
for (const resolved of dispatch.resolvedBasenames) {
|
|
613
|
-
result.paths.push(resolved);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
if (dispatch.unparseable) {
|
|
617
|
-
result.unparseable = true;
|
|
618
|
-
}
|
|
619
|
-
} else if (isPathShaped(token)) {
|
|
620
|
-
const resolved = resolvePathToken(token);
|
|
621
|
-
result.paths.push(resolved);
|
|
622
|
-
}
|
|
623
|
-
prevToken = token;
|
|
624
|
-
}
|
|
625
|
-
return result;
|
|
626
|
-
}
|
|
627
|
-
function isPathShaped(token) {
|
|
628
|
-
if (token.includes("/")) return true;
|
|
629
|
-
if (token.startsWith(".")) return true;
|
|
630
|
-
if (SENSITIVE_BASENAME_RE.test(token)) return true;
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
function resolvePathToken(token) {
|
|
634
|
-
const normalized = normalize(token);
|
|
635
|
-
try {
|
|
636
|
-
return realpathSync(normalized);
|
|
637
|
-
} catch (err) {
|
|
638
|
-
const code = err.code;
|
|
639
|
-
if (code === "ENOENT") {
|
|
640
|
-
return resolveNonexistentPathToken(normalized);
|
|
641
|
-
}
|
|
642
|
-
return normalized;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
function resolveNonexistentPathToken(normalizedPath) {
|
|
646
|
-
let current = normalizedPath;
|
|
647
|
-
let suffix = "";
|
|
648
|
-
for (let i = 0; i < 50; i++) {
|
|
649
|
-
const parent = dirname2(current);
|
|
650
|
-
if (parent === current) {
|
|
651
|
-
return normalizedPath;
|
|
652
|
-
}
|
|
653
|
-
if (parent === ".") {
|
|
654
|
-
return normalizedPath;
|
|
655
|
-
}
|
|
656
|
-
suffix = suffix ? join(basename(current), suffix) : basename(current);
|
|
657
|
-
current = parent;
|
|
658
|
-
try {
|
|
659
|
-
const resolved = realpathSync(current);
|
|
660
|
-
if (resolved !== current) {
|
|
661
|
-
return join(resolved, suffix);
|
|
662
|
-
}
|
|
663
|
-
return join(resolved, suffix);
|
|
664
|
-
} catch {
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
return normalizedPath;
|
|
669
|
-
}
|
|
670
|
-
var FORBIDDEN_BASENAMES = [
|
|
671
|
-
".env",
|
|
672
|
-
".ssh",
|
|
673
|
-
".aws",
|
|
674
|
-
"secrets",
|
|
675
|
-
"credentials",
|
|
676
|
-
"id_rsa",
|
|
677
|
-
"id_dsa",
|
|
678
|
-
"id_ecdsa",
|
|
679
|
-
"id_ed25519",
|
|
680
|
-
".pem",
|
|
681
|
-
".key"
|
|
682
|
-
];
|
|
683
|
-
function scanBashCommand(command, forbiddenBasenames) {
|
|
684
|
-
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
685
|
-
const hits = [];
|
|
686
|
-
for (const basename2 of basenames) {
|
|
687
|
-
const pattern = buildPattern(basename2);
|
|
688
|
-
if (pattern.test(command)) {
|
|
689
|
-
hits.push(basename2);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
return { matched: hits.length > 0, hits };
|
|
693
|
-
}
|
|
694
|
-
function buildPattern(basename2) {
|
|
695
|
-
const escaped = escapeRegex(basename2);
|
|
696
|
-
if (basename2.startsWith(".") && basename2.length <= 4 && !isAlphaAfterDot(basename2)) {
|
|
697
|
-
return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
|
|
698
|
-
}
|
|
699
|
-
if (basename2.startsWith(".")) {
|
|
700
|
-
return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
|
|
701
|
-
}
|
|
702
|
-
return new RegExp(`\\b${escaped}\\b`, "i");
|
|
703
|
-
}
|
|
704
|
-
function scanContentForForbiddenBasenames(content, forbiddenBasenames) {
|
|
705
|
-
const hits = [];
|
|
706
|
-
for (const basename2 of forbiddenBasenames) {
|
|
707
|
-
const pattern = buildContentPattern(basename2);
|
|
708
|
-
if (pattern.test(content)) {
|
|
709
|
-
hits.push(basename2);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
return { matched: hits.length > 0, hits };
|
|
713
|
-
}
|
|
714
|
-
function buildContentPattern(basename2) {
|
|
715
|
-
const escaped = escapeRegex(basename2);
|
|
716
|
-
if (basename2.startsWith(".") && basename2.length <= 4 && !isAlphaAfterDot(basename2)) {
|
|
717
|
-
return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
|
|
718
|
-
}
|
|
719
|
-
if (basename2.startsWith(".")) {
|
|
720
|
-
return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
|
|
721
|
-
}
|
|
722
|
-
return new RegExp(`(?<=[/\\\\]\\.?)${escaped}\\b`, "i");
|
|
723
|
-
}
|
|
724
|
-
function isAlphaAfterDot(s) {
|
|
725
|
-
return /^\.[a-zA-Z]+$/.test(s);
|
|
726
|
-
}
|
|
727
|
-
function escapeRegex(s) {
|
|
728
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
729
|
-
}
|
|
730
|
-
function scanGlobPattern(pattern, forbiddenBasenames) {
|
|
731
|
-
const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
|
|
732
|
-
const hits = [];
|
|
733
|
-
for (const basename2 of basenames) {
|
|
734
|
-
const re = buildGlobContextPattern(basename2);
|
|
735
|
-
if (re.test(pattern)) {
|
|
736
|
-
hits.push(basename2);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
return { matched: hits.length > 0, hits };
|
|
740
|
-
}
|
|
741
|
-
function buildGlobContextPattern(basename2) {
|
|
742
|
-
const escaped = escapeRegex(basename2);
|
|
743
|
-
const GLOB_DELIM = String.raw`\s;&|<>()\\/'"=.*?{}\[\]`;
|
|
744
|
-
if (basename2.startsWith(".") && basename2.length <= 4 && !isAlphaAfterDot(basename2)) {
|
|
745
|
-
return new RegExp(`[\\w*]${escaped}(?=$|[${GLOB_DELIM}])`, "i");
|
|
746
|
-
}
|
|
747
|
-
if (basename2.startsWith(".")) {
|
|
748
|
-
return new RegExp(`(?:^|[${GLOB_DELIM}])${escaped}(?=$|[${GLOB_DELIM}])`, "i");
|
|
749
|
-
}
|
|
750
|
-
return new RegExp(`\\b${escaped}\\b`, "i");
|
|
751
|
-
}
|
|
752
|
-
|
|
753
348
|
// src/gateway/runtimeConstructionResolvers.ts
|
|
754
349
|
var MAX_RECURSION_DEPTH = 3;
|
|
755
350
|
var INTERPRETER_RE = /\b(?:python[23]?|node|ruby|perl|php)\s+(?:-[cer])\s+(.+)/s;
|
|
@@ -1052,7 +647,37 @@ var TOOL_MAP = {
|
|
|
1052
647
|
WebSearch: { action: "network_request", targetKey: "query" },
|
|
1053
648
|
Task: { action: "tool_invocation", targetKey: "description" },
|
|
1054
649
|
Skill: { action: "tool_invocation", targetKey: "skill" },
|
|
1055
|
-
NotebookEdit: { action: "file_write", targetKey: "notebook_path" }
|
|
650
|
+
NotebookEdit: { action: "file_write", targetKey: "notebook_path" },
|
|
651
|
+
// Sprint 26 Gate-A Item D (F-8) — TOOL_MAP refresh. The 11 names above were
|
|
652
|
+
// a stale subset of cc's native tool set; with the unknown-tool deny consumer
|
|
653
|
+
// live, every missing native name would hard-fail. Inventory taken from a
|
|
654
|
+
// live cc session (2026-06). Conservative mapping: tool_invocation, with a
|
|
655
|
+
// targetKey only where the input schema is known to carry a representative
|
|
656
|
+
// free-text field (scanned by Check 2 / sensitivity like Task.description).
|
|
657
|
+
// Subagent-spawning tools (Agent/Workflow/Task*) are orchestration-only here:
|
|
658
|
+
// each spawned agent's own tool calls hook through PreToolUse individually.
|
|
659
|
+
Agent: { action: "tool_invocation", targetKey: "prompt" },
|
|
660
|
+
SendMessage: { action: "tool_invocation" },
|
|
661
|
+
AskUserQuestion: { action: "tool_invocation" },
|
|
662
|
+
ScheduleWakeup: { action: "tool_invocation", targetKey: "prompt" },
|
|
663
|
+
ToolSearch: { action: "tool_invocation", targetKey: "query" },
|
|
664
|
+
Workflow: { action: "tool_invocation", targetKey: "script" },
|
|
665
|
+
Monitor: { action: "tool_invocation" },
|
|
666
|
+
EnterPlanMode: { action: "tool_invocation" },
|
|
667
|
+
ExitPlanMode: { action: "tool_invocation" },
|
|
668
|
+
EnterWorktree: { action: "tool_invocation" },
|
|
669
|
+
ExitWorktree: { action: "tool_invocation" },
|
|
670
|
+
CronCreate: { action: "tool_invocation" },
|
|
671
|
+
CronDelete: { action: "tool_invocation" },
|
|
672
|
+
CronList: { action: "tool_invocation" },
|
|
673
|
+
TaskCreate: { action: "tool_invocation" },
|
|
674
|
+
TaskGet: { action: "tool_invocation" },
|
|
675
|
+
TaskList: { action: "tool_invocation" },
|
|
676
|
+
TaskOutput: { action: "tool_invocation" },
|
|
677
|
+
TaskStop: { action: "tool_invocation" },
|
|
678
|
+
TaskUpdate: { action: "tool_invocation" },
|
|
679
|
+
PushNotification: { action: "tool_invocation" },
|
|
680
|
+
RemoteTrigger: { action: "tool_invocation" }
|
|
1056
681
|
};
|
|
1057
682
|
var AGENT_ID = "claude-code";
|
|
1058
683
|
var AGENT_NAME = "Claude Code";
|
|
@@ -1145,7 +770,7 @@ function extractTargets(toolName, toolInput, cwd) {
|
|
|
1145
770
|
return extractGrepTargets(toolInput, cwd);
|
|
1146
771
|
default: {
|
|
1147
772
|
const mapping = TOOL_MAP[toolName];
|
|
1148
|
-
if (!mapping) return [toolName];
|
|
773
|
+
if (!mapping || !mapping.targetKey) return [toolName];
|
|
1149
774
|
const val = toolInput[mapping.targetKey];
|
|
1150
775
|
if (typeof val === "string" && val.length > 0) return [val];
|
|
1151
776
|
return [toolName];
|
|
@@ -1184,6 +809,17 @@ function extractMcpTargets(toolName, toolInput) {
|
|
|
1184
809
|
var UNKNOWN_TOOL_REASON = "tool schema unknown \u2014 sensitivity scoring and forbidden target patterns cannot evaluate this event";
|
|
1185
810
|
var ClaudeCodeTranslator = class {
|
|
1186
811
|
agentType = "claude-code";
|
|
812
|
+
/**
|
|
813
|
+
* Sprint 26 Gate-A Item D (F-8) — operator allowlist escape hatch. Names
|
|
814
|
+
* listed in the launch policy's enforcement.allowUnknownTools translate as
|
|
815
|
+
* KNOWN tool_invocation (no _unknownTool marker), so a new cc native tool
|
|
816
|
+
* can be unbricked with a one-line policy edit + daemon restart instead of
|
|
817
|
+
* waiting on a Sentinel release that refreshes TOOL_MAP.
|
|
818
|
+
*/
|
|
819
|
+
allowUnknownTools;
|
|
820
|
+
constructor(options) {
|
|
821
|
+
this.allowUnknownTools = new Set(options?.allowUnknownTools ?? []);
|
|
822
|
+
}
|
|
1187
823
|
translatePreToolUse(payload) {
|
|
1188
824
|
const p = payload;
|
|
1189
825
|
if (!p || typeof p !== "object" || !p.tool_name) return null;
|
|
@@ -1204,7 +840,7 @@ var ClaudeCodeTranslator = class {
|
|
|
1204
840
|
metadata._policyEnforcementBypassed = "true";
|
|
1205
841
|
metadata._policyBypassReason = UNKNOWN_TOOL_REASON;
|
|
1206
842
|
console.warn(
|
|
1207
|
-
`[SENTINEL] Unknown Claude Code tool "${toolName}" \u2014
|
|
843
|
+
`[SENTINEL] Unknown Claude Code tool "${toolName}" \u2014 flagged for gateway disposition (enforcement.unknownTools; default warn). ${UNKNOWN_TOOL_REASON}`
|
|
1208
844
|
);
|
|
1209
845
|
}
|
|
1210
846
|
if (isMcp) {
|
|
@@ -1362,6 +998,14 @@ var ClaudeCodeTranslator = class {
|
|
|
1362
998
|
mcpMutating: mcp.mutating
|
|
1363
999
|
};
|
|
1364
1000
|
}
|
|
1001
|
+
if (this.allowUnknownTools.has(toolName)) {
|
|
1002
|
+
return {
|
|
1003
|
+
action: "tool_invocation",
|
|
1004
|
+
targets: [toolName],
|
|
1005
|
+
isUnknown: false,
|
|
1006
|
+
isMcp: false
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1365
1009
|
return {
|
|
1366
1010
|
action: "tool_invocation",
|
|
1367
1011
|
targets: [toolName],
|
|
@@ -1397,9 +1041,23 @@ function buildModifiedGrepInput(originalInput, exclusions) {
|
|
|
1397
1041
|
}
|
|
1398
1042
|
|
|
1399
1043
|
// src/gateway/server.ts
|
|
1044
|
+
import { timingSafeEqual } from "crypto";
|
|
1400
1045
|
var DEFAULT_PORT = 7847;
|
|
1401
1046
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
1402
1047
|
var GATEWAY_VERSION = "0.1.0";
|
|
1048
|
+
function isLoopbackAddress(addr) {
|
|
1049
|
+
if (!addr) return false;
|
|
1050
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" || addr.startsWith("127.");
|
|
1051
|
+
}
|
|
1052
|
+
function constantTimeEqual(a, b) {
|
|
1053
|
+
const ab = Buffer.from(a, "utf-8");
|
|
1054
|
+
const bb = Buffer.from(b, "utf-8");
|
|
1055
|
+
if (ab.length !== bb.length) {
|
|
1056
|
+
timingSafeEqual(bb, bb);
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
return timingSafeEqual(ab, bb);
|
|
1060
|
+
}
|
|
1403
1061
|
function parseIntentLine(prompt) {
|
|
1404
1062
|
if (typeof prompt !== "string") return null;
|
|
1405
1063
|
const firstNonEmpty = prompt.split("\n").find((line) => line.trim().length > 0);
|
|
@@ -1440,6 +1098,9 @@ var SentinelGateway = class {
|
|
|
1440
1098
|
workspaceIsolation;
|
|
1441
1099
|
operatorCeiling;
|
|
1442
1100
|
home;
|
|
1101
|
+
releaseToken;
|
|
1102
|
+
/** Item D (F-8): disposition for unknown (non-MCP, unrecognized) tool names. */
|
|
1103
|
+
unknownTools;
|
|
1443
1104
|
server = null;
|
|
1444
1105
|
running = false;
|
|
1445
1106
|
signalHandlersInstalled = false;
|
|
@@ -1453,6 +1114,8 @@ var SentinelGateway = class {
|
|
|
1453
1114
|
this.workspaceIsolation = options.workspaceIsolation ?? process.env.SENTINEL_WORKSPACE_ISOLATION === "1";
|
|
1454
1115
|
this.operatorCeiling = options.operatorCeiling ?? null;
|
|
1455
1116
|
this.home = options.home ?? "";
|
|
1117
|
+
this.releaseToken = options.releaseToken ?? null;
|
|
1118
|
+
this.unknownTools = options.unknownTools ?? "warn";
|
|
1456
1119
|
const internal = options;
|
|
1457
1120
|
if (internal.registry) {
|
|
1458
1121
|
this.registry = internal.registry;
|
|
@@ -1461,7 +1124,9 @@ var SentinelGateway = class {
|
|
|
1461
1124
|
this.registry.register(internal.translator);
|
|
1462
1125
|
} else {
|
|
1463
1126
|
this.registry = new TranslatorRegistry();
|
|
1464
|
-
this.registry.register(
|
|
1127
|
+
this.registry.register(
|
|
1128
|
+
new ClaudeCodeTranslator({ allowUnknownTools: options.allowUnknownTools })
|
|
1129
|
+
);
|
|
1465
1130
|
}
|
|
1466
1131
|
}
|
|
1467
1132
|
get port() {
|
|
@@ -1580,6 +1245,10 @@ var SentinelGateway = class {
|
|
|
1580
1245
|
return;
|
|
1581
1246
|
}
|
|
1582
1247
|
}
|
|
1248
|
+
if (method === "POST" && url === "/api/sentinel/release") {
|
|
1249
|
+
this.handleReleaseRoute(req, res);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1583
1252
|
if (method === "POST") {
|
|
1584
1253
|
const match = url.match(
|
|
1585
1254
|
/^\/api\/sentinel\/(pre-tool-use|post-tool-use|session-end|user-prompt-submit)\/(.+)$/
|
|
@@ -1606,6 +1275,58 @@ var SentinelGateway = class {
|
|
|
1606
1275
|
}
|
|
1607
1276
|
this.sendJson(res, 404, { error: "not found" });
|
|
1608
1277
|
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Operator release route (Sprint 0.1.1). Loopback-only + token-gated. On a
|
|
1280
|
+
* valid request, calls sentinel.release() on the LIVE instance — flipping the
|
|
1281
|
+
* in-memory mode, writing mode.json, and logging the signed mode_change anchor
|
|
1282
|
+
* in-process (single writer). Never re-reads mode.json or trusts file content.
|
|
1283
|
+
*/
|
|
1284
|
+
handleReleaseRoute(req, res) {
|
|
1285
|
+
const remote = req.socket?.remoteAddress;
|
|
1286
|
+
if (!isLoopbackAddress(remote)) {
|
|
1287
|
+
console.warn(`[SENTINEL GATEWAY] /release refused non-loopback origin: ${remote ?? "?"}`);
|
|
1288
|
+
this.sendJson(res, 403, { ok: false, error: "release is loopback-only" });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (!this.releaseToken) {
|
|
1292
|
+
this.sendJson(res, 503, { ok: false, error: "release endpoint not configured" });
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const provided = req.headers["x-sentinel-token"];
|
|
1296
|
+
const token = typeof provided === "string" ? provided : "";
|
|
1297
|
+
if (!constantTimeEqual(token, this.releaseToken)) {
|
|
1298
|
+
console.warn(`[SENTINEL GATEWAY] /release rejected: invalid token from ${remote}`);
|
|
1299
|
+
this.sendJson(res, 401, { ok: false, error: "invalid or missing token" });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
this.readBody(req, res, (body) => {
|
|
1303
|
+
let payload;
|
|
1304
|
+
try {
|
|
1305
|
+
payload = JSON.parse(body);
|
|
1306
|
+
} catch {
|
|
1307
|
+
this.sendJson(res, 400, { ok: false, error: "invalid JSON" });
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const p = payload ?? {};
|
|
1311
|
+
const agentId = typeof p.agentId === "string" ? p.agentId : "";
|
|
1312
|
+
if (!agentId) {
|
|
1313
|
+
this.sendJson(res, 400, { ok: false, error: "agentId required" });
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const reason = typeof p.reason === "string" ? p.reason : "operator release (live)";
|
|
1317
|
+
const previousMode = this.sentinel.getMode(agentId);
|
|
1318
|
+
this.sentinel.release(agentId, reason).then(() => {
|
|
1319
|
+
this.sendJson(res, 200, {
|
|
1320
|
+
ok: true,
|
|
1321
|
+
agentId,
|
|
1322
|
+
previousMode,
|
|
1323
|
+
mode: this.sentinel.getMode(agentId)
|
|
1324
|
+
});
|
|
1325
|
+
}).catch((err) => {
|
|
1326
|
+
this.sendJson(res, 500, { ok: false, error: String(err.message) });
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1609
1330
|
// -------------------------------------------------------------------------
|
|
1610
1331
|
// Endpoint handlers
|
|
1611
1332
|
// -------------------------------------------------------------------------
|
|
@@ -1740,29 +1461,26 @@ var SentinelGateway = class {
|
|
|
1740
1461
|
routingId = routed.agentId;
|
|
1741
1462
|
event.agentId = routingId;
|
|
1742
1463
|
}
|
|
1743
|
-
if (event.
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
const scan = scanBashCommand(scanTarget, FORBIDDEN_BASENAMES);
|
|
1747
|
-
if (scan.matched) allL2Hits.push(...scan.hits);
|
|
1748
|
-
}
|
|
1749
|
-
if (allL2Hits.length > 0) {
|
|
1464
|
+
if (event.metadata?._unknownTool === "true") {
|
|
1465
|
+
const unknownName = event.metadata.ccToolName ?? event.primaryTarget;
|
|
1466
|
+
if (this.unknownTools === "deny") {
|
|
1750
1467
|
const finding = {
|
|
1751
1468
|
severity: "HIGH",
|
|
1752
1469
|
kind: "actionable",
|
|
1753
|
-
type: "
|
|
1470
|
+
type: "unknown_tool",
|
|
1754
1471
|
agentId: event.agentId,
|
|
1755
1472
|
agentName: event.agentName,
|
|
1756
|
-
description: `
|
|
1473
|
+
description: `Unknown tool "${unknownName}" denied \u2014 not in Sentinel's recognized tool set, so policy checks cannot evaluate it. If this is a legitimate tool, add it to enforcement.allowUnknownTools in the operator launch policy file and restart the gateway daemon.`,
|
|
1757
1474
|
evidence: {
|
|
1758
1475
|
action: event.action,
|
|
1759
|
-
target:
|
|
1476
|
+
target: unknownName,
|
|
1760
1477
|
timestamp: event.timestamp,
|
|
1761
|
-
baselineComparison: "
|
|
1478
|
+
baselineComparison: "unknown_tool_denied"
|
|
1762
1479
|
},
|
|
1763
|
-
recommendation: "
|
|
1480
|
+
recommendation: `Add "${unknownName}" to enforcement.allowUnknownTools in the operator launch policy file and restart the daemon, or update @tuent/sentinel to a build whose recognized tool set includes it.`,
|
|
1764
1481
|
timestamp: event.timestamp,
|
|
1765
|
-
decision: "deny"
|
|
1482
|
+
decision: "deny",
|
|
1483
|
+
dedupKey: unknownName
|
|
1766
1484
|
};
|
|
1767
1485
|
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1768
1486
|
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
@@ -1770,6 +1488,38 @@ var SentinelGateway = class {
|
|
|
1770
1488
|
this.sendJson(res, 200, response);
|
|
1771
1489
|
return;
|
|
1772
1490
|
}
|
|
1491
|
+
const warnFinding = {
|
|
1492
|
+
severity: "LOW",
|
|
1493
|
+
kind: "informational",
|
|
1494
|
+
type: "unknown_tool",
|
|
1495
|
+
agentId: event.agentId,
|
|
1496
|
+
agentName: event.agentName,
|
|
1497
|
+
description: `Unknown tool "${unknownName}" allowed (enforcement.unknownTools: warn) \u2014 not in Sentinel's recognized tool set; policy checks could not evaluate it.`,
|
|
1498
|
+
evidence: {
|
|
1499
|
+
action: event.action,
|
|
1500
|
+
target: unknownName,
|
|
1501
|
+
timestamp: event.timestamp,
|
|
1502
|
+
baselineComparison: "unknown_tool_allowed_warn"
|
|
1503
|
+
},
|
|
1504
|
+
recommendation: `Add "${unknownName}" to enforcement.allowUnknownTools (or update @tuent/sentinel) to clear this warning, or switch enforcement.unknownTools to deny.`,
|
|
1505
|
+
timestamp: event.timestamp,
|
|
1506
|
+
decision: "allow",
|
|
1507
|
+
dedupKey: unknownName
|
|
1508
|
+
};
|
|
1509
|
+
await this.sentinel.logFinding(routingId, warnFinding);
|
|
1510
|
+
}
|
|
1511
|
+
let suppressForbiddenBasename = false;
|
|
1512
|
+
if (event.action === "command_exec" && event.targets && event.targets.length > 0) {
|
|
1513
|
+
const literalCommand = event.targets[0] ?? "";
|
|
1514
|
+
const decodedImplicated = event.targets.slice(1).some((t) => scanBashCommand(t, FORBIDDEN_BASENAMES).matched);
|
|
1515
|
+
suppressForbiddenBasename = isPositionallySafeMention(literalCommand) && !decodedImplicated;
|
|
1516
|
+
}
|
|
1517
|
+
if (event.action === "command_exec" && event.targets && event.targets.length > 0 && !suppressForbiddenBasename) {
|
|
1518
|
+
const allL2Hits = [];
|
|
1519
|
+
for (const scanTarget of event.targets) {
|
|
1520
|
+
const scan = scanBashCommand(scanTarget, FORBIDDEN_BASENAMES);
|
|
1521
|
+
if (scan.matched) allL2Hits.push(...scan.hits);
|
|
1522
|
+
}
|
|
1773
1523
|
const allTokenPaths = [];
|
|
1774
1524
|
let anyUnparseable = false;
|
|
1775
1525
|
let anyDangerousConstruct = false;
|
|
@@ -1791,6 +1541,38 @@ var SentinelGateway = class {
|
|
|
1791
1541
|
}
|
|
1792
1542
|
if (matchedPath) break;
|
|
1793
1543
|
}
|
|
1544
|
+
if (allL2Hits.length > 0) {
|
|
1545
|
+
const { mentionOnly } = classifyDeny(event.targets[0] ?? "", {
|
|
1546
|
+
l2Hits: allL2Hits,
|
|
1547
|
+
hasL1Hit: matchedPath !== null,
|
|
1548
|
+
unparseable: anyUnparseable,
|
|
1549
|
+
hasDangerousConstruct: anyDangerousConstruct
|
|
1550
|
+
});
|
|
1551
|
+
const finding = {
|
|
1552
|
+
severity: "HIGH",
|
|
1553
|
+
kind: "actionable",
|
|
1554
|
+
type: "unauthorized_target",
|
|
1555
|
+
agentId: event.agentId,
|
|
1556
|
+
agentName: event.agentName,
|
|
1557
|
+
description: `Bash command references forbidden basename: ${allL2Hits.join(", ")}`,
|
|
1558
|
+
evidence: {
|
|
1559
|
+
action: event.action,
|
|
1560
|
+
target: allL2Hits[0],
|
|
1561
|
+
timestamp: event.timestamp,
|
|
1562
|
+
baselineComparison: "credentials_exfil_attempt"
|
|
1563
|
+
},
|
|
1564
|
+
recommendation: "Review the command for credential access or exfiltration. If legitimate, use existing policy exception mechanisms.",
|
|
1565
|
+
timestamp: event.timestamp,
|
|
1566
|
+
decision: "deny",
|
|
1567
|
+
mentionOnly,
|
|
1568
|
+
dedupKey: event.primaryTarget
|
|
1569
|
+
};
|
|
1570
|
+
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1571
|
+
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
1572
|
+
const response = translator.formatPreToolUseResponse({ blocked: true, finding });
|
|
1573
|
+
this.sendJson(res, 200, response);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1794
1576
|
if (matchedPath) {
|
|
1795
1577
|
const finding = {
|
|
1796
1578
|
severity: "HIGH",
|
|
@@ -1807,7 +1589,10 @@ var SentinelGateway = class {
|
|
|
1807
1589
|
},
|
|
1808
1590
|
recommendation: "Review the command for credential or sensitive file access. If legitimate, use existing policy exception mechanisms.",
|
|
1809
1591
|
timestamp: event.timestamp,
|
|
1810
|
-
decision: "deny"
|
|
1592
|
+
decision: "deny",
|
|
1593
|
+
// A resolved path-glob hit is a file target — never a mention.
|
|
1594
|
+
mentionOnly: false,
|
|
1595
|
+
dedupKey: event.primaryTarget
|
|
1811
1596
|
};
|
|
1812
1597
|
await this.sentinel.handleGatewayDeny(routingId, finding);
|
|
1813
1598
|
this.telemetry.recordToolCall(event.action, "pre", "blocked", 0);
|
|
@@ -2239,14 +2024,18 @@ async function runGatewayDaemon({
|
|
|
2239
2024
|
policyPath,
|
|
2240
2025
|
port = DEFAULT_PORT
|
|
2241
2026
|
}) {
|
|
2242
|
-
const { Sentinel: SentinelClass } = await import("./Sentinel-
|
|
2243
|
-
const { writePidFile } = await import("./pidManager-
|
|
2027
|
+
const { Sentinel: SentinelClass } = await import("./Sentinel-XMSJE4DZ.js");
|
|
2028
|
+
const { writePidFile, writeReleaseToken } = await import("./pidManager-DOGVN6ZT.js");
|
|
2244
2029
|
const { homedir } = await import("os");
|
|
2245
|
-
const {
|
|
2030
|
+
const { randomBytes } = await import("crypto");
|
|
2031
|
+
const { loadPolicy: loadPolicy2, policyToRole: policyToRole2, policyToConfig: policyToConfig2 } = await import("./policyLoader-KZL2U4M2.js");
|
|
2246
2032
|
const sentinel = await SentinelClass.fromPolicy(policyPath);
|
|
2247
2033
|
const baseline = await sentinel.computeBaseline("claude-code");
|
|
2248
2034
|
sentinel.setBaseline("claude-code", baseline);
|
|
2249
|
-
const
|
|
2035
|
+
const operatorPolicy = await loadPolicy2(policyPath);
|
|
2036
|
+
const operatorCeiling = policyToRole2(operatorPolicy);
|
|
2037
|
+
const operatorConfig = policyToConfig2(operatorPolicy);
|
|
2038
|
+
const releaseToken = randomBytes(32).toString("hex");
|
|
2250
2039
|
const gateway = new SentinelGateway({
|
|
2251
2040
|
port,
|
|
2252
2041
|
sentinel,
|
|
@@ -2254,10 +2043,15 @@ async function runGatewayDaemon({
|
|
|
2254
2043
|
agentId: "claude-code",
|
|
2255
2044
|
workspaceIsolation: process.env.SENTINEL_WORKSPACE_ISOLATION !== "0",
|
|
2256
2045
|
operatorCeiling,
|
|
2257
|
-
home: homedir()
|
|
2046
|
+
home: homedir(),
|
|
2047
|
+
releaseToken,
|
|
2048
|
+
unknownTools: operatorConfig.enforcement?.unknownTools,
|
|
2049
|
+
allowUnknownTools: operatorConfig.enforcement?.allowUnknownTools
|
|
2258
2050
|
});
|
|
2259
2051
|
await gateway.start();
|
|
2260
|
-
|
|
2052
|
+
const home = homedir();
|
|
2053
|
+
writePidFile(home, process.pid);
|
|
2054
|
+
writeReleaseToken(home, releaseToken, gateway.port);
|
|
2261
2055
|
console.log(`[SENTINEL GATEWAY] PID ${process.pid} written`);
|
|
2262
2056
|
}
|
|
2263
2057
|
|
|
@@ -2265,4 +2059,4 @@ export {
|
|
|
2265
2059
|
SentinelGateway,
|
|
2266
2060
|
runGatewayDaemon
|
|
2267
2061
|
};
|
|
2268
|
-
//# sourceMappingURL=chunk-
|
|
2062
|
+
//# sourceMappingURL=chunk-L4R3LPJS.js.map
|