@tuent/sentinel 0.1.0 → 0.1.1
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 +22 -26
- package/SECURITY_MODEL.md +231 -0
- package/dist/Sentinel-QHMQ67W3.js +10 -0
- package/dist/chunk-B5QKJHSV.js +32 -0
- package/dist/{chunk-Z3PWIJKT.js → chunk-IYC5E7RL.js} +99 -422
- package/dist/{chunk-CUJKNIKT.js → chunk-LATQNIRW.js} +33 -1
- package/dist/{chunk-QFRDEISP.js → chunk-NS6ZLMDK.js} +6 -6
- package/dist/{chunk-6MHWJATS.js → chunk-QHE56MEO.js} +510 -18
- package/dist/{chunk-3U3PKD4N.js → chunk-WPTJBRX5.js} +2 -2
- package/dist/cli.js +30 -30
- package/dist/gateway/index.d.ts +14 -0
- package/dist/gateway/index.js +3 -2
- package/dist/gatewayDaemon.js +3 -2
- package/dist/index.js +4 -4
- package/dist/pidManager-DOGVN6ZT.js +23 -0
- package/package.json +3 -2
- package/dist/Sentinel-JLQL3YRD.js +0 -10
- package/dist/pidManager-ZYC7SICM.js +0 -15
|
@@ -1,11 +1,20 @@
|
|
|
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
|
+
isPositionallySafeMention,
|
|
6
11
|
matchGlobInsensitive,
|
|
7
|
-
normalizeForbiddenPattern
|
|
8
|
-
|
|
12
|
+
normalizeForbiddenPattern,
|
|
13
|
+
scanBashCommand,
|
|
14
|
+
scanContentForForbiddenBasenames,
|
|
15
|
+
scanGlobPattern,
|
|
16
|
+
tokenizePaths
|
|
17
|
+
} from "./chunk-QHE56MEO.js";
|
|
9
18
|
import {
|
|
10
19
|
loadPolicy,
|
|
11
20
|
policyToConfig,
|
|
@@ -15,34 +24,6 @@ import {
|
|
|
15
24
|
// src/gateway/workspaceRouter.ts
|
|
16
25
|
import { resolve, dirname } from "path";
|
|
17
26
|
|
|
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
27
|
// src/mergeRoles.ts
|
|
47
28
|
function isWithinActiveHours(hour, range) {
|
|
48
29
|
const [startHour, endHour] = range;
|
|
@@ -363,393 +344,6 @@ var TranslatorRegistry = class {
|
|
|
363
344
|
}
|
|
364
345
|
};
|
|
365
346
|
|
|
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
347
|
// src/gateway/runtimeConstructionResolvers.ts
|
|
754
348
|
var MAX_RECURSION_DEPTH = 3;
|
|
755
349
|
var INTERPRETER_RE = /\b(?:python[23]?|node|ruby|perl|php)\s+(?:-[cer])\s+(.+)/s;
|
|
@@ -1397,9 +991,23 @@ function buildModifiedGrepInput(originalInput, exclusions) {
|
|
|
1397
991
|
}
|
|
1398
992
|
|
|
1399
993
|
// src/gateway/server.ts
|
|
994
|
+
import { timingSafeEqual } from "crypto";
|
|
1400
995
|
var DEFAULT_PORT = 7847;
|
|
1401
996
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
1402
997
|
var GATEWAY_VERSION = "0.1.0";
|
|
998
|
+
function isLoopbackAddress(addr) {
|
|
999
|
+
if (!addr) return false;
|
|
1000
|
+
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1" || addr.startsWith("127.");
|
|
1001
|
+
}
|
|
1002
|
+
function constantTimeEqual(a, b) {
|
|
1003
|
+
const ab = Buffer.from(a, "utf-8");
|
|
1004
|
+
const bb = Buffer.from(b, "utf-8");
|
|
1005
|
+
if (ab.length !== bb.length) {
|
|
1006
|
+
timingSafeEqual(bb, bb);
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
return timingSafeEqual(ab, bb);
|
|
1010
|
+
}
|
|
1403
1011
|
function parseIntentLine(prompt) {
|
|
1404
1012
|
if (typeof prompt !== "string") return null;
|
|
1405
1013
|
const firstNonEmpty = prompt.split("\n").find((line) => line.trim().length > 0);
|
|
@@ -1440,6 +1048,7 @@ var SentinelGateway = class {
|
|
|
1440
1048
|
workspaceIsolation;
|
|
1441
1049
|
operatorCeiling;
|
|
1442
1050
|
home;
|
|
1051
|
+
releaseToken;
|
|
1443
1052
|
server = null;
|
|
1444
1053
|
running = false;
|
|
1445
1054
|
signalHandlersInstalled = false;
|
|
@@ -1453,6 +1062,7 @@ var SentinelGateway = class {
|
|
|
1453
1062
|
this.workspaceIsolation = options.workspaceIsolation ?? process.env.SENTINEL_WORKSPACE_ISOLATION === "1";
|
|
1454
1063
|
this.operatorCeiling = options.operatorCeiling ?? null;
|
|
1455
1064
|
this.home = options.home ?? "";
|
|
1065
|
+
this.releaseToken = options.releaseToken ?? null;
|
|
1456
1066
|
const internal = options;
|
|
1457
1067
|
if (internal.registry) {
|
|
1458
1068
|
this.registry = internal.registry;
|
|
@@ -1580,6 +1190,10 @@ var SentinelGateway = class {
|
|
|
1580
1190
|
return;
|
|
1581
1191
|
}
|
|
1582
1192
|
}
|
|
1193
|
+
if (method === "POST" && url === "/api/sentinel/release") {
|
|
1194
|
+
this.handleReleaseRoute(req, res);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1583
1197
|
if (method === "POST") {
|
|
1584
1198
|
const match = url.match(
|
|
1585
1199
|
/^\/api\/sentinel\/(pre-tool-use|post-tool-use|session-end|user-prompt-submit)\/(.+)$/
|
|
@@ -1606,6 +1220,58 @@ var SentinelGateway = class {
|
|
|
1606
1220
|
}
|
|
1607
1221
|
this.sendJson(res, 404, { error: "not found" });
|
|
1608
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Operator release route (Sprint 0.1.1). Loopback-only + token-gated. On a
|
|
1225
|
+
* valid request, calls sentinel.release() on the LIVE instance — flipping the
|
|
1226
|
+
* in-memory mode, writing mode.json, and logging the signed mode_change anchor
|
|
1227
|
+
* in-process (single writer). Never re-reads mode.json or trusts file content.
|
|
1228
|
+
*/
|
|
1229
|
+
handleReleaseRoute(req, res) {
|
|
1230
|
+
const remote = req.socket?.remoteAddress;
|
|
1231
|
+
if (!isLoopbackAddress(remote)) {
|
|
1232
|
+
console.warn(`[SENTINEL GATEWAY] /release refused non-loopback origin: ${remote ?? "?"}`);
|
|
1233
|
+
this.sendJson(res, 403, { ok: false, error: "release is loopback-only" });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (!this.releaseToken) {
|
|
1237
|
+
this.sendJson(res, 503, { ok: false, error: "release endpoint not configured" });
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const provided = req.headers["x-sentinel-token"];
|
|
1241
|
+
const token = typeof provided === "string" ? provided : "";
|
|
1242
|
+
if (!constantTimeEqual(token, this.releaseToken)) {
|
|
1243
|
+
console.warn(`[SENTINEL GATEWAY] /release rejected: invalid token from ${remote}`);
|
|
1244
|
+
this.sendJson(res, 401, { ok: false, error: "invalid or missing token" });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
this.readBody(req, res, (body) => {
|
|
1248
|
+
let payload;
|
|
1249
|
+
try {
|
|
1250
|
+
payload = JSON.parse(body);
|
|
1251
|
+
} catch {
|
|
1252
|
+
this.sendJson(res, 400, { ok: false, error: "invalid JSON" });
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const p = payload ?? {};
|
|
1256
|
+
const agentId = typeof p.agentId === "string" ? p.agentId : "";
|
|
1257
|
+
if (!agentId) {
|
|
1258
|
+
this.sendJson(res, 400, { ok: false, error: "agentId required" });
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const reason = typeof p.reason === "string" ? p.reason : "operator release (live)";
|
|
1262
|
+
const previousMode = this.sentinel.getMode(agentId);
|
|
1263
|
+
this.sentinel.release(agentId, reason).then(() => {
|
|
1264
|
+
this.sendJson(res, 200, {
|
|
1265
|
+
ok: true,
|
|
1266
|
+
agentId,
|
|
1267
|
+
previousMode,
|
|
1268
|
+
mode: this.sentinel.getMode(agentId)
|
|
1269
|
+
});
|
|
1270
|
+
}).catch((err) => {
|
|
1271
|
+
this.sendJson(res, 500, { ok: false, error: String(err.message) });
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1609
1275
|
// -------------------------------------------------------------------------
|
|
1610
1276
|
// Endpoint handlers
|
|
1611
1277
|
// -------------------------------------------------------------------------
|
|
@@ -1740,7 +1406,13 @@ var SentinelGateway = class {
|
|
|
1740
1406
|
routingId = routed.agentId;
|
|
1741
1407
|
event.agentId = routingId;
|
|
1742
1408
|
}
|
|
1409
|
+
let suppressForbiddenBasename = false;
|
|
1743
1410
|
if (event.action === "command_exec" && event.targets && event.targets.length > 0) {
|
|
1411
|
+
const literalCommand = event.targets[0] ?? "";
|
|
1412
|
+
const decodedImplicated = event.targets.slice(1).some((t) => scanBashCommand(t, FORBIDDEN_BASENAMES).matched);
|
|
1413
|
+
suppressForbiddenBasename = isPositionallySafeMention(literalCommand) && !decodedImplicated;
|
|
1414
|
+
}
|
|
1415
|
+
if (event.action === "command_exec" && event.targets && event.targets.length > 0 && !suppressForbiddenBasename) {
|
|
1744
1416
|
const allL2Hits = [];
|
|
1745
1417
|
for (const scanTarget of event.targets) {
|
|
1746
1418
|
const scan = scanBashCommand(scanTarget, FORBIDDEN_BASENAMES);
|
|
@@ -2239,14 +1911,16 @@ async function runGatewayDaemon({
|
|
|
2239
1911
|
policyPath,
|
|
2240
1912
|
port = DEFAULT_PORT
|
|
2241
1913
|
}) {
|
|
2242
|
-
const { Sentinel: SentinelClass } = await import("./Sentinel-
|
|
2243
|
-
const { writePidFile } = await import("./pidManager-
|
|
1914
|
+
const { Sentinel: SentinelClass } = await import("./Sentinel-QHMQ67W3.js");
|
|
1915
|
+
const { writePidFile, writeReleaseToken } = await import("./pidManager-DOGVN6ZT.js");
|
|
2244
1916
|
const { homedir } = await import("os");
|
|
1917
|
+
const { randomBytes } = await import("crypto");
|
|
2245
1918
|
const { loadPolicy: loadPolicy2, policyToRole: policyToRole2 } = await import("./policyLoader-6KR5VFVV.js");
|
|
2246
1919
|
const sentinel = await SentinelClass.fromPolicy(policyPath);
|
|
2247
1920
|
const baseline = await sentinel.computeBaseline("claude-code");
|
|
2248
1921
|
sentinel.setBaseline("claude-code", baseline);
|
|
2249
1922
|
const operatorCeiling = policyToRole2(await loadPolicy2(policyPath));
|
|
1923
|
+
const releaseToken = randomBytes(32).toString("hex");
|
|
2250
1924
|
const gateway = new SentinelGateway({
|
|
2251
1925
|
port,
|
|
2252
1926
|
sentinel,
|
|
@@ -2254,10 +1928,13 @@ async function runGatewayDaemon({
|
|
|
2254
1928
|
agentId: "claude-code",
|
|
2255
1929
|
workspaceIsolation: process.env.SENTINEL_WORKSPACE_ISOLATION !== "0",
|
|
2256
1930
|
operatorCeiling,
|
|
2257
|
-
home: homedir()
|
|
1931
|
+
home: homedir(),
|
|
1932
|
+
releaseToken
|
|
2258
1933
|
});
|
|
2259
1934
|
await gateway.start();
|
|
2260
|
-
|
|
1935
|
+
const home = homedir();
|
|
1936
|
+
writePidFile(home, process.pid);
|
|
1937
|
+
writeReleaseToken(home, releaseToken, gateway.port);
|
|
2261
1938
|
console.log(`[SENTINEL GATEWAY] PID ${process.pid} written`);
|
|
2262
1939
|
}
|
|
2263
1940
|
|
|
@@ -2265,4 +1942,4 @@ export {
|
|
|
2265
1942
|
SentinelGateway,
|
|
2266
1943
|
runGatewayDaemon
|
|
2267
1944
|
};
|
|
2268
|
-
//# sourceMappingURL=chunk-
|
|
1945
|
+
//# sourceMappingURL=chunk-IYC5E7RL.js.map
|
|
@@ -3,6 +3,34 @@ import { readFileSync, writeFileSync, unlinkSync, renameSync } from "fs";
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
var PID_FILENAME = "sentinel-gateway.pid";
|
|
6
|
+
var TOKEN_FILENAME = "sentinel-gateway.token";
|
|
7
|
+
function releaseTokenPath(home) {
|
|
8
|
+
return join(home, ".dahlia", TOKEN_FILENAME);
|
|
9
|
+
}
|
|
10
|
+
function writeReleaseToken(home, token, port) {
|
|
11
|
+
const p = releaseTokenPath(home);
|
|
12
|
+
const tmp = p + `.tmp.${process.pid}`;
|
|
13
|
+
writeFileSync(tmp, JSON.stringify({ token, port }) + "\n", { mode: 384 });
|
|
14
|
+
renameSync(tmp, p);
|
|
15
|
+
}
|
|
16
|
+
function readReleaseToken(home) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = readFileSync(releaseTokenPath(home), "utf-8").trim();
|
|
19
|
+
const j = JSON.parse(raw);
|
|
20
|
+
if (typeof j.token === "string" && j.token.length > 0 && typeof j.port === "number") {
|
|
21
|
+
return { token: j.token, port: j.port };
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function removeReleaseToken(home) {
|
|
29
|
+
try {
|
|
30
|
+
unlinkSync(releaseTokenPath(home));
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
6
34
|
function readPidFile(home) {
|
|
7
35
|
try {
|
|
8
36
|
const raw = readFileSync(join(home, ".dahlia", PID_FILENAME), "utf-8").trim();
|
|
@@ -53,10 +81,14 @@ function acquireGatewayLock(home) {
|
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
export {
|
|
84
|
+
releaseTokenPath,
|
|
85
|
+
writeReleaseToken,
|
|
86
|
+
readReleaseToken,
|
|
87
|
+
removeReleaseToken,
|
|
56
88
|
readPidFile,
|
|
57
89
|
writePidFile,
|
|
58
90
|
removePidFile,
|
|
59
91
|
verifyPidIsGateway,
|
|
60
92
|
acquireGatewayLock
|
|
61
93
|
};
|
|
62
|
-
//# sourceMappingURL=chunk-
|
|
94
|
+
//# sourceMappingURL=chunk-LATQNIRW.js.map
|