@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.
@@ -234,8 +234,43 @@ var DEFAULT_FORBIDDEN_PATTERNS = [
234
234
  "**/id_ed25519*",
235
235
  "**/*.pem",
236
236
  "**/*.key",
237
- "/etc/**"
237
+ "/etc/**",
238
+ // Sprint 26 FIX 1 (A) — common credential stores. DRIFT: keep in sync with
239
+ // STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts (template↔defaults
240
+ // unification tracked in a separate ticket — do not refactor here).
241
+ "**/.netrc",
242
+ "**/.npmrc",
243
+ "**/.git-credentials",
244
+ "**/.pgpass",
245
+ "**/.zsh_history",
246
+ "**/.config/gh/**",
247
+ "**/.docker/config.json",
248
+ "**/.gnupg/**",
249
+ "**/.config/gcloud/**",
250
+ "**/.kube/**",
251
+ "**/Library/Keychains/**",
252
+ // Sprint 26 FIX 1 (B) — Sentinel's own state dir (current path only; the
253
+ // ~/.dahlia → ~/.sentinel rename is a separate re-arch).
254
+ "**/.dahlia/**",
255
+ // Sprint 26 FIX 3 — the live policy file and cc's hook-wiring settings files.
256
+ // Denies the agent's own tool-writes (policy rewrite / unhook vectors); reads
257
+ // stay allowed via DEFAULT_POLICY_READ_EXCEPTIONS below. The settings glob
258
+ // covers project AND user-level (~/.claude/settings*.json) in one pattern.
259
+ // DRIFT: keep in sync with STARTER_POLICY.forbid.targets in setup/initClaudeCode.ts.
260
+ "**/.sentinel.yaml",
261
+ "**/.claude/settings*.json"
238
262
  ];
263
+ var DEFAULT_POLICY_READ_EXCEPTIONS = [
264
+ { target: "**/.sentinel.yaml", allowedActions: ["file_read"] },
265
+ { target: "**/.claude/settings*.json", allowedActions: ["file_read"] }
266
+ ];
267
+ function withPolicyReadExceptions(existing) {
268
+ const merged = existing ? [...existing] : [];
269
+ for (const exc of DEFAULT_POLICY_READ_EXCEPTIONS) {
270
+ if (!merged.some((e) => e.target === exc.target)) merged.push(exc);
271
+ }
272
+ return merged;
273
+ }
239
274
  var DEFAULT_MEDIUM_DISPOSITION = {
240
275
  network_request: "deny"
241
276
  };
@@ -258,23 +293,543 @@ var DEFAULT_NETWORK_DENYLIST_CIDRS = [
258
293
  var DEFAULT_DANGEROUS_SCHEMES = ["file:", "data:", "javascript:", "vbscript:"];
259
294
 
260
295
  // src/roleValidator.ts
261
- import { normalize, basename, dirname as dirname3, join as join3 } from "path";
262
- import { lstatSync, readdirSync, realpathSync } from "fs";
296
+ import { normalize as normalize2, basename as basename2, dirname as dirname4, join as join4 } from "path";
297
+ import { lstatSync, readdirSync, realpathSync as realpathSync2 } from "fs";
263
298
  import { homedir as homedir3 } from "os";
299
+
300
+ // src/gateway/bashScanner.ts
301
+ import { parse as shellParse } from "shell-quote";
302
+ import { realpathSync } from "fs";
303
+ import { dirname as dirname3, join as join3, basename, normalize } from "path";
304
+ var BRACE_PATTERN_RE = /\{[^}]*,[^}]*\}/;
305
+ var MAX_BRACE_EXPANSION = 64;
306
+ function fnmatchBasename(pattern, candidate) {
307
+ if (pattern.length !== candidate.length) return false;
308
+ for (let i = 0; i < pattern.length; i++) {
309
+ const p = pattern[i].toLowerCase();
310
+ const c = candidate[i].toLowerCase();
311
+ if (p === "?") continue;
312
+ if (p !== c) return false;
313
+ }
314
+ return true;
315
+ }
316
+ function bracketTokenMatchesForbidden(token, forbiddenBasenames) {
317
+ const literals = [];
318
+ let current = "";
319
+ let inBracket = false;
320
+ for (let i = 0; i < token.length; i++) {
321
+ if (token[i] === "[" && !inBracket) {
322
+ if (current) literals.push(current);
323
+ current = "";
324
+ inBracket = true;
325
+ } else if (token[i] === "]" && inBracket) {
326
+ inBracket = false;
327
+ } else if (!inBracket) {
328
+ current += token[i];
329
+ }
330
+ }
331
+ if (current) literals.push(current);
332
+ for (const forbidden of forbiddenBasenames) {
333
+ const fl = forbidden.toLowerCase();
334
+ for (const lit of literals) {
335
+ if (lit.length === 0) continue;
336
+ if (fl.includes(lit.toLowerCase())) return forbidden;
337
+ }
338
+ }
339
+ return null;
340
+ }
341
+ function resolveBraceExpansion(token) {
342
+ const match = token.match(/^(.*?)\{([^}]*,[^}]*)\}(.*)$/);
343
+ if (!match) return null;
344
+ const [, prefix, alternatives, suffix] = match;
345
+ const parts = alternatives.split(",");
346
+ if (parts.length > MAX_BRACE_EXPANSION) return null;
347
+ return parts.map((p) => prefix + p + suffix);
348
+ }
349
+ function wildcardDispatch(token, forbiddenBasenames, metadataField) {
350
+ const result = {
351
+ resolvedBasenames: [],
352
+ unparseable: false,
353
+ metadata: {}
354
+ };
355
+ if (token === "*" || token === "**" || token === "?") {
356
+ return result;
357
+ }
358
+ if (BRACE_PATTERN_RE.test(token)) {
359
+ const expanded = resolveBraceExpansion(token);
360
+ if (expanded === null) {
361
+ result.unparseable = true;
362
+ return result;
363
+ }
364
+ for (const alt of expanded) {
365
+ const hasWildcard = /[?*[]/.test(alt);
366
+ if (hasWildcard) {
367
+ const sub = wildcardDispatch(alt, forbiddenBasenames, metadataField);
368
+ if (sub.resolvedBasenames.length > 0) {
369
+ result.resolvedBasenames.push(...sub.resolvedBasenames);
370
+ result.metadata["resolvedFromBrace"] = token;
371
+ Object.assign(result.metadata, sub.metadata);
372
+ }
373
+ if (sub.unparseable) result.unparseable = true;
374
+ } else {
375
+ const altLower = alt.toLowerCase();
376
+ for (const forbidden of forbiddenBasenames) {
377
+ if (altLower === forbidden.toLowerCase()) {
378
+ result.resolvedBasenames.push(forbidden);
379
+ result.metadata["resolvedFromBrace"] = token;
380
+ break;
381
+ }
382
+ }
383
+ }
384
+ }
385
+ return result;
386
+ }
387
+ const hasStar = token.includes("*");
388
+ const hasQuestion = token.includes("?");
389
+ const hasBracket = token.includes("[");
390
+ if (hasBracket) {
391
+ const matched = bracketTokenMatchesForbidden(token, forbiddenBasenames);
392
+ if (matched) {
393
+ result.resolvedBasenames.push(matched);
394
+ result.metadata["resolvedFromBracket"] = token;
395
+ } else {
396
+ result.unparseable = true;
397
+ }
398
+ return result;
399
+ }
400
+ if (hasStar && !hasQuestion) {
401
+ const matched = starLiteralSubstringCheck(token, forbiddenBasenames);
402
+ if (matched) {
403
+ result.resolvedBasenames.push(matched);
404
+ result.metadata[metadataField] = token;
405
+ }
406
+ return result;
407
+ }
408
+ if (hasQuestion && !hasStar) {
409
+ for (const forbidden of forbiddenBasenames) {
410
+ if (fnmatchBasename(token, forbidden)) {
411
+ result.resolvedBasenames.push(forbidden);
412
+ result.metadata[metadataField] = token;
413
+ break;
414
+ }
415
+ }
416
+ return result;
417
+ }
418
+ if (hasStar && hasQuestion) {
419
+ const starMatch = starLiteralSubstringCheck(token, forbiddenBasenames);
420
+ if (starMatch) {
421
+ result.resolvedBasenames.push(starMatch);
422
+ result.metadata[metadataField] = token;
423
+ return result;
424
+ }
425
+ const segments = token.split("*").filter((s) => s.includes("?"));
426
+ for (const seg of segments) {
427
+ for (const forbidden of forbiddenBasenames) {
428
+ if (fnmatchBasename(seg, forbidden)) {
429
+ result.resolvedBasenames.push(forbidden);
430
+ result.metadata[metadataField] = token;
431
+ return result;
432
+ }
433
+ }
434
+ }
435
+ return result;
436
+ }
437
+ return result;
438
+ }
439
+ function starLiteralSubstringCheck(token, forbiddenBasenames) {
440
+ const literals = token.split("*").filter((s) => s.length > 0);
441
+ for (const forbidden of forbiddenBasenames) {
442
+ const fl = forbidden.toLowerCase();
443
+ for (const lit of literals) {
444
+ if (fl.includes(lit.toLowerCase())) return forbidden;
445
+ }
446
+ }
447
+ return null;
448
+ }
449
+ function shouldDispatchWildcard(token) {
450
+ const hasMetachar = /[?*[{]/.test(token);
451
+ if (!hasMetachar) return false;
452
+ if (isPathShaped(token)) return true;
453
+ if (token.includes("[")) return true;
454
+ if (BRACE_PATTERN_RE.test(token)) return true;
455
+ return false;
456
+ }
457
+ var SENSITIVE_BASENAME_RE = /(?:\.env|\.ssh|secrets|credentials|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.pem|\.key|\.netrc|\.npmrc|\.pgpass|\.zsh_history|\.gnupg|\.kube|\.dahlia)/i;
458
+ var DANGEROUS_COMMAND_TOKENS = /* @__PURE__ */ new Set(["eval"]);
459
+ var COMMAND_SUBSTITUTION_RE = /\$\(|`/;
460
+ var DANGEROUS_RAW_RE = /<<<|<\(|>\(/;
461
+ function isVarMarker(token) {
462
+ return typeof token === "object" && token !== null && "__sentinel_var" in token && typeof token.__sentinel_var === "string";
463
+ }
464
+ function tokenizePaths(command) {
465
+ const result = {
466
+ paths: [],
467
+ unparseable: false,
468
+ hasDangerousConstruct: false
469
+ };
470
+ if (DANGEROUS_RAW_RE.test(command)) {
471
+ result.hasDangerousConstruct = true;
472
+ }
473
+ if (COMMAND_SUBSTITUTION_RE.test(command)) {
474
+ result.hasDangerousConstruct = true;
475
+ }
476
+ let tokens;
477
+ try {
478
+ tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
479
+ } catch {
480
+ result.unparseable = true;
481
+ return result;
482
+ }
483
+ if (!Array.isArray(tokens)) {
484
+ result.unparseable = true;
485
+ return result;
486
+ }
487
+ let prevToken = null;
488
+ for (let i = 0; i < tokens.length; i++) {
489
+ const token = tokens[i];
490
+ if (isVarMarker(token)) {
491
+ const nextToken = tokens[i + 1];
492
+ const nextIsPathRelevant = nextToken === void 0 || // end of tokens — var is complete argument
493
+ typeof nextToken === "object" && nextToken !== null && "op" in nextToken || // followed by operator — var is complete argument
494
+ typeof nextToken === "string" && isPathShaped(nextToken);
495
+ const prevIsPathRelevant = prevToken !== null && isPathShaped(prevToken);
496
+ if (nextIsPathRelevant || prevIsPathRelevant) {
497
+ result.unparseable = true;
498
+ }
499
+ prevToken = null;
500
+ continue;
501
+ }
502
+ if (typeof token === "object" && token !== null) {
503
+ if ("pattern" in token) {
504
+ const globPattern = token.pattern;
505
+ const lastSlash = globPattern.lastIndexOf("/");
506
+ const dispatchTarget = lastSlash >= 0 ? globPattern.slice(lastSlash + 1) : globPattern;
507
+ const dispatch = wildcardDispatch(dispatchTarget, FORBIDDEN_BASENAMES, "resolvedFromGlob");
508
+ if (dispatch.resolvedBasenames.length > 0) {
509
+ for (const resolved of dispatch.resolvedBasenames) {
510
+ result.paths.push(resolved);
511
+ }
512
+ }
513
+ if (dispatch.unparseable) {
514
+ result.unparseable = true;
515
+ }
516
+ if (SENSITIVE_BASENAME_RE.test(globPattern)) {
517
+ result.unparseable = true;
518
+ }
519
+ prevToken = null;
520
+ continue;
521
+ }
522
+ if ("op" in token) {
523
+ if (token.op === "<(") {
524
+ result.hasDangerousConstruct = true;
525
+ }
526
+ prevToken = null;
527
+ continue;
528
+ }
529
+ prevToken = null;
530
+ continue;
531
+ }
532
+ if (typeof token !== "string") {
533
+ prevToken = null;
534
+ continue;
535
+ }
536
+ if (DANGEROUS_COMMAND_TOKENS.has(token.toLowerCase())) {
537
+ result.hasDangerousConstruct = true;
538
+ }
539
+ if ((prevToken === "sh" || prevToken === "bash" || prevToken === "/bin/sh" || prevToken === "/bin/bash") && token === "-c") {
540
+ result.hasDangerousConstruct = true;
541
+ }
542
+ if (shouldDispatchWildcard(token)) {
543
+ const metaField = "resolvedFromQuotedGlob";
544
+ const dispatch = wildcardDispatch(token, FORBIDDEN_BASENAMES, metaField);
545
+ if (dispatch.resolvedBasenames.length > 0) {
546
+ for (const resolved of dispatch.resolvedBasenames) {
547
+ result.paths.push(resolved);
548
+ }
549
+ }
550
+ if (dispatch.unparseable) {
551
+ result.unparseable = true;
552
+ }
553
+ } else if (isPathShaped(token)) {
554
+ const resolved = resolvePathToken(token);
555
+ result.paths.push(resolved);
556
+ }
557
+ prevToken = token;
558
+ }
559
+ return result;
560
+ }
561
+ function isPathShaped(token) {
562
+ if (token.includes("/")) return true;
563
+ if (token.startsWith(".")) return true;
564
+ if (SENSITIVE_BASENAME_RE.test(token)) return true;
565
+ return false;
566
+ }
567
+ function resolvePathToken(token) {
568
+ const normalized = normalize(token);
569
+ try {
570
+ return realpathSync(normalized);
571
+ } catch (err) {
572
+ const code = err.code;
573
+ if (code === "ENOENT") {
574
+ return resolveNonexistentPathToken(normalized);
575
+ }
576
+ return normalized;
577
+ }
578
+ }
579
+ function resolveNonexistentPathToken(normalizedPath) {
580
+ let current = normalizedPath;
581
+ let suffix = "";
582
+ for (let i = 0; i < 50; i++) {
583
+ const parent = dirname3(current);
584
+ if (parent === current) {
585
+ return normalizedPath;
586
+ }
587
+ if (parent === ".") {
588
+ return normalizedPath;
589
+ }
590
+ suffix = suffix ? join3(basename(current), suffix) : basename(current);
591
+ current = parent;
592
+ try {
593
+ const resolved = realpathSync(current);
594
+ if (resolved !== current) {
595
+ return join3(resolved, suffix);
596
+ }
597
+ return join3(resolved, suffix);
598
+ } catch {
599
+ continue;
600
+ }
601
+ }
602
+ return normalizedPath;
603
+ }
604
+ var FORBIDDEN_BASENAMES = [
605
+ ".env",
606
+ ".ssh",
607
+ ".aws",
608
+ "secrets",
609
+ "credentials",
610
+ "id_rsa",
611
+ "id_dsa",
612
+ "id_ecdsa",
613
+ "id_ed25519",
614
+ ".pem",
615
+ ".key",
616
+ // Sprint 26 FIX 1 (A) — credential-store file basenames (L2 bash deny).
617
+ // `.git-credentials` is already covered by the "credentials" entry above.
618
+ ".netrc",
619
+ ".npmrc",
620
+ ".pgpass",
621
+ ".zsh_history"
622
+ ];
623
+ function scanBashCommand(command, forbiddenBasenames) {
624
+ const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
625
+ const hits = [];
626
+ for (const basename3 of basenames) {
627
+ const pattern = buildPattern(basename3);
628
+ if (pattern.test(command)) {
629
+ hits.push(basename3);
630
+ }
631
+ }
632
+ return { matched: hits.length > 0, hits };
633
+ }
634
+ function buildPattern(basename3) {
635
+ const escaped = escapeRegex(basename3);
636
+ if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
637
+ return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
638
+ }
639
+ if (basename3.startsWith(".")) {
640
+ return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
641
+ }
642
+ return new RegExp(`\\b${escaped}\\b`, "i");
643
+ }
644
+ function scanContentForForbiddenBasenames(content, forbiddenBasenames) {
645
+ const hits = [];
646
+ for (const basename3 of forbiddenBasenames) {
647
+ const pattern = buildContentPattern(basename3);
648
+ if (pattern.test(content)) {
649
+ hits.push(basename3);
650
+ }
651
+ }
652
+ return { matched: hits.length > 0, hits };
653
+ }
654
+ function buildContentPattern(basename3) {
655
+ const escaped = escapeRegex(basename3);
656
+ if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
657
+ return new RegExp(`\\w${escaped}(?=$|[\\s;&|<>()'"=\\/])`, "i");
658
+ }
659
+ if (basename3.startsWith(".")) {
660
+ return new RegExp(`(?:^|[\\s;&|<>()\\/'"=])${escaped}(?=$|[\\s;&|<>()\\/'"=.])`, "i");
661
+ }
662
+ return new RegExp(`(?<=[/\\\\]\\.?)${escaped}\\b`, "i");
663
+ }
664
+ function isAlphaAfterDot(s) {
665
+ return /^\.[a-zA-Z]+$/.test(s);
666
+ }
667
+ function escapeRegex(s) {
668
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
669
+ }
670
+ function scanGlobPattern(pattern, forbiddenBasenames) {
671
+ const basenames = forbiddenBasenames ?? FORBIDDEN_BASENAMES;
672
+ const hits = [];
673
+ for (const basename3 of basenames) {
674
+ const re = buildGlobContextPattern(basename3);
675
+ if (re.test(pattern)) {
676
+ hits.push(basename3);
677
+ }
678
+ }
679
+ return { matched: hits.length > 0, hits };
680
+ }
681
+ function buildGlobContextPattern(basename3) {
682
+ const escaped = escapeRegex(basename3);
683
+ const GLOB_DELIM = String.raw`\s;&|<>()\\/'"=.*?{}\[\]`;
684
+ if (basename3.startsWith(".") && basename3.length <= 4 && !isAlphaAfterDot(basename3)) {
685
+ return new RegExp(`[\\w*]${escaped}(?=$|[${GLOB_DELIM}])`, "i");
686
+ }
687
+ if (basename3.startsWith(".")) {
688
+ return new RegExp(`(?:^|[${GLOB_DELIM}])${escaped}(?=$|[${GLOB_DELIM}])`, "i");
689
+ }
690
+ return new RegExp(`\\b${escaped}\\b`, "i");
691
+ }
692
+ var SAFE_VERBS = /* @__PURE__ */ new Set(["echo", "printf", ":", "true"]);
693
+ var SEGMENT_OPS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "&", "\n"]);
694
+ var REDIRECT_OPS = /* @__PURE__ */ new Set([">", ">>", "<", "&>", ">&", "<&"]);
695
+ function positionallySafeBasenames(command) {
696
+ const safe = /* @__PURE__ */ new Set();
697
+ if (typeof command !== "string" || command.length === 0) return safe;
698
+ if (COMMAND_SUBSTITUTION_RE.test(command) || DANGEROUS_RAW_RE.test(command) || command.includes("<<")) {
699
+ return safe;
700
+ }
701
+ let tokens;
702
+ try {
703
+ tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
704
+ } catch {
705
+ return safe;
706
+ }
707
+ if (!Array.isArray(tokens)) return safe;
708
+ const tally = /* @__PURE__ */ new Map();
709
+ const mark = (hits, isSafe) => {
710
+ for (const b of hits) {
711
+ const e = tally.get(b) ?? { safe: 0, unsafe: 0 };
712
+ if (isSafe) e.safe++;
713
+ else e.unsafe++;
714
+ tally.set(b, e);
715
+ }
716
+ };
717
+ let verb = null;
718
+ let expectVerb = true;
719
+ let prevRedirect = false;
720
+ for (const tok of tokens) {
721
+ if (tok && typeof tok === "object") {
722
+ if (isVarMarker(tok)) {
723
+ if (expectVerb) {
724
+ verb = null;
725
+ expectVerb = false;
726
+ }
727
+ prevRedirect = false;
728
+ continue;
729
+ }
730
+ if ("comment" in tok) {
731
+ const hits2 = scanBashCommand(
732
+ String(tok.comment),
733
+ FORBIDDEN_BASENAMES
734
+ ).hits;
735
+ if (hits2.length) mark(hits2, true);
736
+ continue;
737
+ }
738
+ if ("pattern" in tok) {
739
+ const hits2 = scanGlobPattern(
740
+ String(tok.pattern),
741
+ FORBIDDEN_BASENAMES
742
+ ).hits;
743
+ if (hits2.length) mark(hits2, false);
744
+ prevRedirect = false;
745
+ continue;
746
+ }
747
+ if ("op" in tok) {
748
+ const op = String(tok.op);
749
+ prevRedirect = REDIRECT_OPS.has(op);
750
+ if (SEGMENT_OPS.has(op)) {
751
+ expectVerb = true;
752
+ verb = null;
753
+ }
754
+ continue;
755
+ }
756
+ prevRedirect = false;
757
+ continue;
758
+ }
759
+ if (typeof tok !== "string") {
760
+ prevRedirect = false;
761
+ continue;
762
+ }
763
+ const isRedirectTarget = prevRedirect;
764
+ prevRedirect = false;
765
+ const hits = scanBashCommand(tok, FORBIDDEN_BASENAMES).hits;
766
+ if (expectVerb) {
767
+ verb = tok;
768
+ expectVerb = false;
769
+ if (hits.length) mark(hits, false);
770
+ continue;
771
+ }
772
+ if (hits.length) {
773
+ const safeVerb = verb !== null && SAFE_VERBS.has(verb);
774
+ mark(hits, safeVerb && !isRedirectTarget);
775
+ }
776
+ }
777
+ for (const [b, e] of tally) {
778
+ if (e.unsafe === 0 && e.safe > 0) safe.add(b);
779
+ }
780
+ return safe;
781
+ }
782
+ function isPositionallySafeMention(command) {
783
+ const hits = scanBashCommand(command, FORBIDDEN_BASENAMES).hits;
784
+ if (hits.length === 0) return false;
785
+ const safe = positionallySafeBasenames(command);
786
+ return hits.every((h) => safe.has(h));
787
+ }
788
+ function classifyDeny(command, signals) {
789
+ const { l2Hits, hasL1Hit, unparseable, hasDangerousConstruct } = signals;
790
+ if (hasL1Hit || unparseable || hasDangerousConstruct) return { mentionOnly: false };
791
+ if (l2Hits.length === 0) return { mentionOnly: false };
792
+ let tokens;
793
+ try {
794
+ tokens = shellParse(command, (key) => ({ __sentinel_var: key }));
795
+ } catch {
796
+ return { mentionOnly: false };
797
+ }
798
+ if (!Array.isArray(tokens)) return { mentionOnly: false };
799
+ if (tokens.some((t) => isVarMarker(t))) return { mentionOnly: false };
800
+ const strTokens = tokens.filter((t) => typeof t === "string");
801
+ for (const hit of l2Hits) {
802
+ const h = hit.toLowerCase();
803
+ const confident = strTokens.some((tok) => {
804
+ const low = tok.toLowerCase();
805
+ if (!low.includes(h)) return false;
806
+ if (low.length === h.length) return false;
807
+ const reHits = scanBashCommand(tok, FORBIDDEN_BASENAMES).hits;
808
+ if (reHits.some((rh) => tok.length === rh.length)) return false;
809
+ const reTok = tokenizePaths(tok);
810
+ if (reTok.unparseable || reTok.hasDangerousConstruct) return false;
811
+ return true;
812
+ });
813
+ if (!confident) return { mentionOnly: false };
814
+ }
815
+ return { mentionOnly: true };
816
+ }
817
+
818
+ // src/roleValidator.ts
264
819
  var SUSPICIOUS_BASENAME_RE = /^\.|(\.env|secret|credential|key|config|token)/i;
265
820
  function resolveSymlinks(normalizedPath) {
266
821
  if (!normalizedPath || normalizedPath.includes("://")) return normalizedPath;
267
822
  if (normalizedPath.includes("node_modules/") || normalizedPath.includes("node_modules\\")) {
268
823
  return normalizedPath;
269
824
  }
270
- const base = basename(normalizedPath);
825
+ const base = basename2(normalizedPath);
271
826
  const needsRealpath = SUSPICIOUS_BASENAME_RE.test(base);
272
827
  if (!needsRealpath) {
273
828
  try {
274
829
  const stat = lstatSync(normalizedPath);
275
830
  if (!stat.isSymbolicLink()) {
276
831
  try {
277
- const resolved = realpathSync(normalizedPath);
832
+ const resolved = realpathSync2(normalizedPath);
278
833
  return resolved === normalizedPath ? normalizedPath : resolved;
279
834
  } catch {
280
835
  return normalizedPath;
@@ -289,7 +844,7 @@ function resolveSymlinks(normalizedPath) {
289
844
  }
290
845
  }
291
846
  try {
292
- return realpathSync(normalizedPath);
847
+ return realpathSync2(normalizedPath);
293
848
  } catch (err) {
294
849
  const code = err.code;
295
850
  if (code === "ENOENT") {
@@ -302,19 +857,19 @@ function resolveNonexistentPath(normalizedPath) {
302
857
  let current = normalizedPath;
303
858
  let suffix = "";
304
859
  for (let i = 0; i < 50; i++) {
305
- const parent = dirname3(current);
860
+ const parent = dirname4(current);
306
861
  if (parent === current) {
307
862
  return normalizedPath;
308
863
  }
309
864
  if (parent === ".") {
310
865
  return normalizedPath;
311
866
  }
312
- suffix = suffix ? join3(basename(current), suffix) : basename(current);
867
+ suffix = suffix ? join4(basename2(current), suffix) : basename2(current);
313
868
  current = parent;
314
869
  try {
315
- const resolved = realpathSync(current);
870
+ const resolved = realpathSync2(current);
316
871
  if (resolved !== current) {
317
- return join3(resolved, suffix);
872
+ return join4(resolved, suffix);
318
873
  }
319
874
  return normalizedPath;
320
875
  } catch {
@@ -347,7 +902,12 @@ function normalizeForbiddenPattern(pattern) {
347
902
  if (pattern.startsWith("**/") || pattern.startsWith("/")) return pattern;
348
903
  return "**/" + pattern;
349
904
  }
350
- function isPathShaped(value) {
905
+ function unionWithDefaultForbiddenPatterns(supplied) {
906
+ return [
907
+ ...new Set([...supplied ?? [], ...DEFAULT_FORBIDDEN_PATTERNS].map(normalizeForbiddenPattern))
908
+ ];
909
+ }
910
+ function isPathShaped2(value) {
351
911
  if (value.length === 0 || value.length > 4096) return false;
352
912
  if (/\s/.test(value)) return false;
353
913
  if (value.includes("/") || value.includes("\\")) return true;
@@ -465,7 +1025,7 @@ function walkForbiddenInodeRoots(forbiddenPatterns, cwd) {
465
1025
  collectForbiddenInodes(home, deepPatterns, inodes, false);
466
1026
  const sensitiveDirs = [".ssh", ".aws", ".gnupg", ".config"];
467
1027
  for (const dir of sensitiveDirs) {
468
- collectForbiddenInodes(join3(home, dir), deepPatterns, inodes, true);
1028
+ collectForbiddenInodes(join4(home, dir), deepPatterns, inodes, true);
469
1029
  }
470
1030
  collectForbiddenInodes(cwd, deepPatterns, inodes, true, true);
471
1031
  return inodes;
@@ -479,7 +1039,7 @@ function collectForbiddenInodes(dirPath, forbiddenPatterns, inodes, recursive, s
479
1039
  }
480
1040
  for (const entry of entries) {
481
1041
  if (skipNodeModules && entry === "node_modules") continue;
482
- const fullPath = join3(dirPath, entry);
1042
+ const fullPath = join4(dirPath, entry);
483
1043
  let stat;
484
1044
  try {
485
1045
  stat = lstatSync(fullPath);
@@ -547,7 +1107,7 @@ var RoleValidator = class {
547
1107
  }
548
1108
  validateEvent(event, activeTask) {
549
1109
  const eventTarget = event.primaryTarget;
550
- const pathNormalized = isUrlShaped(eventTarget) ? eventTarget : normalize(eventTarget);
1110
+ const pathNormalized = isUrlShaped(eventTarget) ? eventTarget : normalize2(eventTarget);
551
1111
  const resolvedTarget = resolveSymlinks(pathNormalized);
552
1112
  const normalizedPrimaryTarget = resolvedTarget;
553
1113
  const primaryTargetPaths = pathNormalized !== resolvedTarget ? [resolvedTarget, pathNormalized] : [resolvedTarget];
@@ -570,7 +1130,7 @@ var RoleValidator = class {
570
1130
  for (let i = 1; i < event.targets.length; i++) {
571
1131
  const st = event.targets[i];
572
1132
  if (!isUrlShaped(st)) {
573
- const stNorm = normalize(st);
1133
+ const stNorm = normalize2(st);
574
1134
  inodeTargets.push(resolveSymlinks(stNorm));
575
1135
  }
576
1136
  }
@@ -588,9 +1148,10 @@ var RoleValidator = class {
588
1148
  }
589
1149
  }
590
1150
  }
1151
+ const safeCommandMention = event.action === "command_exec" && isPositionallySafeMention(event.primaryTarget);
591
1152
  for (const pattern of this.role.forbiddenTargetPatterns) {
592
1153
  let matchedTargetValue = null;
593
- if (primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
1154
+ if (!safeCommandMention && primaryTargetPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
594
1155
  matchedTargetValue = normalizedPrimaryTarget;
595
1156
  }
596
1157
  if (!matchedTargetValue) {
@@ -598,7 +1159,7 @@ var RoleValidator = class {
598
1159
  for (let i = 1; i < event.targets.length; i++) {
599
1160
  const st = event.targets[i];
600
1161
  if (mcpTool && /\s/.test(st)) continue;
601
- const stNorm = isUrlShaped(st) ? st : normalize(st);
1162
+ const stNorm = isUrlShaped(st) ? st : normalize2(st);
602
1163
  const stResolved = resolveSymlinks(stNorm);
603
1164
  const stPaths = stNorm !== stResolved ? [stResolved, stNorm] : [stResolved];
604
1165
  if (stPaths.some((tp) => matchGlobInsensitive(pattern, tp))) {
@@ -664,7 +1225,7 @@ var RoleValidator = class {
664
1225
  if (this.sensitivityScorer && event.metadata?._mcpTool === "true") {
665
1226
  for (let i = 1; i < event.targets.length; i++) {
666
1227
  const val = event.targets[i];
667
- if (!(isUrlShaped(val) || isPathShaped(val))) continue;
1228
+ if (!(isUrlShaped(val) || isPathShaped2(val))) continue;
668
1229
  const sens = this.sensitivityScorer.scoreTarget(val, event.action);
669
1230
  if (sens.effectiveScore < 0.6) continue;
670
1231
  return this.makeFinding(event, {
@@ -1208,14 +1769,23 @@ export {
1208
1769
  removeOverlayDecision,
1209
1770
  createEmptyOverlay,
1210
1771
  DEFAULT_FORBIDDEN_PATTERNS,
1772
+ withPolicyReadExceptions,
1211
1773
  DEFAULT_MEDIUM_DISPOSITION,
1212
1774
  TargetSensitivityScorer,
1775
+ tokenizePaths,
1776
+ FORBIDDEN_BASENAMES,
1777
+ scanBashCommand,
1778
+ scanContentForForbiddenBasenames,
1779
+ scanGlobPattern,
1780
+ isPositionallySafeMention,
1781
+ classifyDeny,
1213
1782
  matchGlob,
1214
1783
  normalizeForbiddenPattern,
1784
+ unionWithDefaultForbiddenPatterns,
1215
1785
  matchGlobInsensitive,
1216
1786
  getTargetRecommendation,
1217
1787
  walkForbiddenInodeRoots,
1218
1788
  findMatchingException,
1219
1789
  RoleValidator
1220
1790
  };
1221
- //# sourceMappingURL=chunk-6MHWJATS.js.map
1791
+ //# sourceMappingURL=chunk-QIYQWOLO.js.map