@node9/proxy 1.67.1 → 1.67.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -380,8 +380,8 @@ function sanitizeConfig(raw) {
380
380
  }
381
381
  }
382
382
  const lines = result.error.issues.map((issue) => {
383
- const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
- return ` \u2022 ${path13}: ${issue.message}`;
383
+ const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
+ return ` \u2022 ${path14}: ${issue.message}`;
385
385
  });
386
386
  return {
387
387
  sanitized,
@@ -1001,6 +1001,129 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1001
1001
  }
1002
1002
  return null;
1003
1003
  }
1004
+ var MAX_REGEX_LENGTH = 256;
1005
+ var REGEX_CACHE_MAX = 500;
1006
+ var regexCache = /* @__PURE__ */ new Map();
1007
+ function validateRegex(pattern) {
1008
+ if (!pattern) return "Pattern is required";
1009
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
1010
+ try {
1011
+ new RegExp(pattern);
1012
+ } catch (e) {
1013
+ return `Invalid regex syntax: ${e.message}`;
1014
+ }
1015
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
1016
+ if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
1017
+ return null;
1018
+ }
1019
+ function getCompiledRegex(pattern, flags = "") {
1020
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
1021
+ const key = `${pattern}\0${flags}`;
1022
+ if (regexCache.has(key)) {
1023
+ const cached = regexCache.get(key);
1024
+ regexCache.delete(key);
1025
+ regexCache.set(key, cached);
1026
+ return cached;
1027
+ }
1028
+ if (validateRegex(pattern) !== null) return null;
1029
+ try {
1030
+ const re = new RegExp(pattern, flags);
1031
+ if (regexCache.size >= REGEX_CACHE_MAX) {
1032
+ const oldest = regexCache.keys().next().value;
1033
+ if (oldest) regexCache.delete(oldest);
1034
+ }
1035
+ regexCache.set(key, re);
1036
+ return re;
1037
+ } catch {
1038
+ return null;
1039
+ }
1040
+ }
1041
+ function matchesPattern(text, patterns) {
1042
+ const p = Array.isArray(patterns) ? patterns : [patterns];
1043
+ if (p.length === 0) return false;
1044
+ const isMatch = pm(p, { nocase: true, dot: true });
1045
+ const target = text.toLowerCase();
1046
+ const directMatch = isMatch(target);
1047
+ if (directMatch) return true;
1048
+ const withoutDotSlash = text.replace(/^\.\//, "");
1049
+ return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1050
+ }
1051
+ var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1052
+ function getNestedValue(obj, path14) {
1053
+ if (!obj || typeof obj !== "object") return null;
1054
+ const segments = path14.split(".");
1055
+ for (const seg of segments) {
1056
+ if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1057
+ }
1058
+ return segments.reduce((prev, curr) => prev?.[curr], obj);
1059
+ }
1060
+ function evaluateSmartConditions(args, rule) {
1061
+ if (!rule.conditions || rule.conditions.length === 0) return true;
1062
+ const mode = rule.conditionMode ?? "all";
1063
+ const fieldCache = /* @__PURE__ */ new Map();
1064
+ const resolveField = (field) => {
1065
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
1066
+ const rawVal = getNestedValue(args, field);
1067
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
1068
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
1069
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1070
+ fieldCache.set(field, val);
1071
+ return val;
1072
+ };
1073
+ const readingsCache = /* @__PURE__ */ new Map();
1074
+ const resolveFieldReadings = (field) => {
1075
+ const cached = readingsCache.get(field);
1076
+ if (cached) return cached;
1077
+ const primary = resolveField(field);
1078
+ if (primary === null) {
1079
+ readingsCache.set(field, []);
1080
+ return [];
1081
+ }
1082
+ let out = [primary];
1083
+ if (field === "command") {
1084
+ const raw = getNestedValue(args, field);
1085
+ if (typeof raw === "string") {
1086
+ const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
1087
+ out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
1088
+ }
1089
+ }
1090
+ readingsCache.set(field, out);
1091
+ return out;
1092
+ };
1093
+ const results = rule.conditions.map((cond) => {
1094
+ const val = resolveField(cond.field);
1095
+ switch (cond.op) {
1096
+ case "exists":
1097
+ return val !== null && val !== "";
1098
+ case "notExists":
1099
+ return val === null || val === "";
1100
+ case "contains":
1101
+ return val !== null && cond.value ? val.includes(cond.value) : false;
1102
+ case "notContains":
1103
+ return val !== null && cond.value ? !val.includes(cond.value) : true;
1104
+ case "matches": {
1105
+ if (val === null || !cond.value) return false;
1106
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1107
+ if (!reM) return false;
1108
+ return resolveFieldReadings(cond.field).some((v) => reM.test(v));
1109
+ }
1110
+ case "notMatches": {
1111
+ if (!cond.value) return false;
1112
+ if (val === null) return true;
1113
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1114
+ if (!reN) return false;
1115
+ return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
1116
+ }
1117
+ case "matchesGlob":
1118
+ return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
1119
+ case "notMatchesGlob":
1120
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
1121
+ default:
1122
+ return false;
1123
+ }
1124
+ });
1125
+ return mode === "any" ? results.some((r) => r) : results.every((r) => r);
1126
+ }
1004
1127
  var { syntax } = mvdanSh;
1005
1128
  var sharedParser = syntax.NewParser();
1006
1129
  var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -1073,14 +1196,22 @@ function cachedNormalize(command, compute) {
1073
1196
  return result;
1074
1197
  }
1075
1198
  function normalizeCommandForPolicy(command) {
1199
+ return commandReadingsImpl(command).posix;
1200
+ }
1201
+ function commandReadings(command) {
1202
+ const r = commandReadingsImpl(command);
1203
+ return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
1204
+ }
1205
+ function commandReadingsImpl(command) {
1076
1206
  return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
1077
1207
  }
1078
1208
  function normalizeCommandForPolicyImpl(command) {
1079
1209
  const f = parseShared(command);
1080
- if (f === PARSE_FAIL) return command;
1210
+ if (f === PARSE_FAIL) return { posix: command, separator: command };
1081
1211
  try {
1082
1212
  const strips = [];
1083
1213
  const rewrites = [];
1214
+ const quoteOnlyRewrites = [];
1084
1215
  const msgSpans = /* @__PURE__ */ new Set();
1085
1216
  syntax.Walk(f, (node) => {
1086
1217
  if (!node) return false;
@@ -1125,22 +1256,23 @@ function normalizeCommandForPolicyImpl(command) {
1125
1256
  if (resolved === source) continue;
1126
1257
  if (resolved === "" || /\s/.test(resolved)) continue;
1127
1258
  rewrites.push([s, e, resolved]);
1259
+ const quoteOnly = source.replace(/['"]/g, "");
1260
+ if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
1128
1261
  }
1129
1262
  return true;
1130
1263
  });
1131
- const edits = [
1132
- ...strips.map(([s, e]) => [s, e, '""']),
1133
- ...rewrites
1134
- ];
1135
- if (edits.length === 0) return command;
1136
- edits.sort((a, b) => b[0] - a[0]);
1137
- let result = command;
1138
- for (const [s, e, rep] of edits) {
1139
- result = result.slice(0, s) + rep + result.slice(e);
1140
- }
1141
- return result;
1264
+ const stripEdits = strips.map(([s, e]) => [s, e, '""']);
1265
+ const apply = (extra) => {
1266
+ const edits = [...stripEdits, ...extra];
1267
+ if (edits.length === 0) return command;
1268
+ edits.sort((a, b) => b[0] - a[0]);
1269
+ let out = command;
1270
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
1271
+ return out;
1272
+ };
1273
+ return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
1142
1274
  } catch {
1143
- return command;
1275
+ return { posix: command, separator: command };
1144
1276
  }
1145
1277
  }
1146
1278
  function scanArgsForDynamicExec(args, startIdx) {
@@ -1209,9 +1341,34 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1209
1341
  "vi",
1210
1342
  "emacs",
1211
1343
  "code",
1212
- "type"
1344
+ "type",
1345
+ // — the 22 that were missing —
1346
+ "grep",
1347
+ "egrep",
1348
+ "fgrep",
1349
+ "rg",
1350
+ "ag",
1351
+ "ack",
1352
+ "awk",
1353
+ "gawk",
1354
+ "sed",
1355
+ "cut",
1356
+ "tr",
1357
+ "jq",
1358
+ "yq",
1359
+ "od",
1360
+ "xxd",
1361
+ "hexdump",
1362
+ "strings",
1363
+ "sort",
1364
+ "uniq",
1365
+ "tac",
1366
+ "nl",
1367
+ "dd"
1213
1368
  ]);
1214
- var FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
1369
+ var FS_OP_PRESCREEN_RE = new RegExp(
1370
+ `(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
1371
+ );
1215
1372
  var HOME_CACHE_ALLOWLIST = [
1216
1373
  ".cache",
1217
1374
  ".npm/_npx",
@@ -1250,9 +1407,37 @@ var SENSITIVE_PATH_RULES = [
1250
1407
  // for the canonical test-asserted contract.
1251
1408
  rule: "shield:project-jail:block-read-env",
1252
1409
  reason: "Reading .env files is blocked by project-jail shield",
1253
- match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
1254
- p
1255
- )
1410
+ // Structural, not a list. The previous form enumerated seven suffixes and
1411
+ // anchored on `$`, so `.env.prod`, `.env.ci` and `.env.local.bak` — all
1412
+ // gitignored, all routinely holding real secrets — were never covered. A
1413
+ // hand-written list of what to protect is only ever as complete as the day
1414
+ // it was typed; this says "`.env` plus any suffix chain" and then names the
1415
+ // exceptions, which is the direction that fails safe.
1416
+ //
1417
+ // \.env the segment itself
1418
+ // (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
1419
+ // files. Without it a flat suffix class swallows both.
1420
+ // (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
1421
+ // [\w.-]*$ any suffix chain. Flat class, no nested quantifier —
1422
+ // `(\.[\w-]+)*` reads the same but is rejected by
1423
+ // safe-regex2, and this pattern runs on the hook hot path.
1424
+ //
1425
+ // The two exclusions are NOT the same shape, because the words do not mean
1426
+ // the same thing:
1427
+ //
1428
+ // (?!\.(?:example|sample|template)\b) — "this file is a fixture", and it
1429
+ // stays a fixture whatever follows, so `.env.example.md` is allowed too.
1430
+ // These are checked into git by convention: already public, so blocking
1431
+ // them buys nothing and costs the most common legitimate agent read.
1432
+ //
1433
+ // (?!\.test$) — anchored, because `test` names an ENVIRONMENT, not a
1434
+ // fixture. `.env.test` is the committed template and stays allowed, but
1435
+ // `.env.test.local` is gitignored by the `.env*.local` convention and
1436
+ // holds real values, so it must block. Using `\b` here — the obvious
1437
+ // symmetry — silently exempts every `.env.test.*` file.
1438
+ //
1439
+ // shields.test.ts:983-995 is the canonical contract; keep both in step.
1440
+ match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
1256
1441
  },
1257
1442
  {
1258
1443
  // verdict: 'review' (not 'block') is a deliberate design choice
@@ -1381,6 +1566,208 @@ function chmodHasOpenPermMode(command) {
1381
1566
  }
1382
1567
  return found;
1383
1568
  }
1569
+ function isShellShapedTool(toolName, toolInspection) {
1570
+ if (isBashTool(toolName)) return true;
1571
+ if (!toolInspection) return false;
1572
+ const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
1573
+ return pattern !== void 0 && toolInspection[pattern] === "command";
1574
+ }
1575
+ function toolMatchesRule(toolName, ruleTool, toolInspection) {
1576
+ if (!ruleTool) return true;
1577
+ if (matchesPattern(toolName, ruleTool)) return true;
1578
+ return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
1579
+ }
1580
+ var INLINE_INTERPRETER = /^(python[\d.]*|perl|ruby|node|tsx|ts-node|php|lua|deno|bun|pwsh|powershell(?:\.exe)?|osascript|rscript|irb|bash|sh|zsh|script|su)$/i;
1581
+ var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
1582
+ "uv",
1583
+ "uvx",
1584
+ "poetry",
1585
+ "pipenv",
1586
+ "pdm",
1587
+ "rye",
1588
+ "hatch",
1589
+ "conda",
1590
+ "mamba",
1591
+ "micromamba",
1592
+ "npx",
1593
+ "pnpm",
1594
+ "yarn",
1595
+ "bunx",
1596
+ "watch",
1597
+ "strace",
1598
+ "ltrace",
1599
+ "chroot",
1600
+ "unshare",
1601
+ "runuser"
1602
+ ]);
1603
+ function isInlineCodeFlag(interp, w) {
1604
+ const lw = w.toLowerCase();
1605
+ if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
1606
+ if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
1607
+ if (!w.startsWith("-") || w.startsWith("--")) return false;
1608
+ const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1609
+ const body = lw.slice(1);
1610
+ const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
1611
+ const cut = body.search(cutAt);
1612
+ const bundle = cut >= 0 ? body.slice(0, cut) : body;
1613
+ return [...codeLetters].some((l) => bundle.includes(l));
1614
+ }
1615
+ var _redirStdinOps = null;
1616
+ function redirStdinOps() {
1617
+ if (_redirStdinOps) return _redirStdinOps;
1618
+ _redirStdinOps = new Set(
1619
+ [
1620
+ deriveRedirOp("cat <<X\nX"),
1621
+ deriveRedirOp("cat <<-X\nX"),
1622
+ deriveRedirOp("cat < f"),
1623
+ deriveRedirOp("cat <<< x")
1624
+ ].filter((op) => op >= 0)
1625
+ );
1626
+ return _redirStdinOps;
1627
+ }
1628
+ function deriveBinaryOp(sample) {
1629
+ try {
1630
+ const f = sharedParser.Parse(sample, "cmd");
1631
+ let op = -1;
1632
+ syntax.Walk(f, (node) => {
1633
+ const n = node;
1634
+ if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
1635
+ return true;
1636
+ });
1637
+ return op;
1638
+ } catch {
1639
+ return -1;
1640
+ }
1641
+ }
1642
+ var _listOps = null;
1643
+ function listOps() {
1644
+ if (_listOps) return _listOps;
1645
+ _listOps = new Set(
1646
+ [deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
1647
+ );
1648
+ return _listOps;
1649
+ }
1650
+ var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
1651
+ var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
1652
+ function unwrapCommandHead(words) {
1653
+ let i = 0;
1654
+ while (i < words.length) {
1655
+ const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
1656
+ if (head === "find") {
1657
+ const x = words.findIndex(
1658
+ (w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
1659
+ );
1660
+ if (x < 0) break;
1661
+ i = x + 1;
1662
+ continue;
1663
+ }
1664
+ if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
1665
+ i++;
1666
+ let targetConsumed = false;
1667
+ while (i < words.length) {
1668
+ const t = words[i];
1669
+ if (t === null) {
1670
+ i++;
1671
+ continue;
1672
+ }
1673
+ const lt = t.toLowerCase();
1674
+ if (/^[A-Za-z_]\w*=/.test(t)) {
1675
+ i++;
1676
+ continue;
1677
+ }
1678
+ if (t.startsWith("-")) {
1679
+ i++;
1680
+ const nxt = words[i];
1681
+ if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
1682
+ i++;
1683
+ continue;
1684
+ }
1685
+ if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
1686
+ i++;
1687
+ continue;
1688
+ }
1689
+ if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
1690
+ targetConsumed = true;
1691
+ i++;
1692
+ continue;
1693
+ }
1694
+ break;
1695
+ }
1696
+ }
1697
+ return i;
1698
+ }
1699
+ function inlineExecStmt(stmt, pipeFed) {
1700
+ const cmd = stmt?.Cmd;
1701
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1702
+ const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1703
+ if (words.length === 0) return false;
1704
+ const headIdx = unwrapCommandHead(words);
1705
+ const rawHead = words[headIdx];
1706
+ if (rawHead == null) return false;
1707
+ const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1708
+ if (!INLINE_INTERPRETER.test(interp)) return false;
1709
+ let args = words.slice(headIdx + 1);
1710
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1711
+ if (INTERP_LEADING_TARGET.has(interp)) {
1712
+ const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1713
+ args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1714
+ }
1715
+ let positionals = 0;
1716
+ let selectedProgram = false;
1717
+ for (const a of args) {
1718
+ if (a == null) {
1719
+ positionals++;
1720
+ selectedProgram = true;
1721
+ continue;
1722
+ }
1723
+ if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
1724
+ if (a === "-m") {
1725
+ selectedProgram = true;
1726
+ continue;
1727
+ }
1728
+ if (a === "-" && !selectedProgram) return true;
1729
+ if (!a.startsWith("-")) {
1730
+ positionals++;
1731
+ selectedProgram = true;
1732
+ }
1733
+ }
1734
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1735
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1736
+ if (positionals === 0 && (stdinFed || pipeFed)) {
1737
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1738
+ }
1739
+ return false;
1740
+ }
1741
+ function detectInlineExec(command) {
1742
+ const f = parseShared(command);
1743
+ if (f === PARSE_FAIL) {
1744
+ return /(^|[|;&]|&&)\s*(?:[\w./-]*\/)?(python[\d.]*|perl|ruby|node|php|lua|deno|bun|pwsh|osascript|rscript|bash|sh|zsh)\s+-{1,2}[a-z]*[ceEr]/i.test(
1745
+ command
1746
+ );
1747
+ }
1748
+ let found = false;
1749
+ try {
1750
+ syntax.Walk(f, (node) => {
1751
+ if (!node || found) return false;
1752
+ const n = node;
1753
+ const t = syntax.NodeType(n);
1754
+ if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1755
+ if (inlineExecStmt(n.Y, true)) {
1756
+ found = true;
1757
+ return false;
1758
+ }
1759
+ }
1760
+ if (t === "Stmt" && inlineExecStmt(n, false)) {
1761
+ found = true;
1762
+ return false;
1763
+ }
1764
+ return true;
1765
+ });
1766
+ } catch {
1767
+ return found;
1768
+ }
1769
+ return found;
1770
+ }
1384
1771
  function analyzeChmod777(command) {
1385
1772
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
1386
1773
  if (!chmodHasOpenPermMode(command)) return null;
@@ -2245,150 +2632,12 @@ function extractAllSshHosts(tokens) {
2245
2632
  }
2246
2633
  return [...hosts].filter(Boolean);
2247
2634
  }
2248
- var MAX_REGEX_LENGTH = 100;
2249
- var REGEX_CACHE_MAX = 500;
2250
- var regexCache = /* @__PURE__ */ new Map();
2251
- function validateRegex(pattern) {
2252
- if (!pattern) return "Pattern is required";
2253
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
2254
- try {
2255
- new RegExp(pattern);
2256
- } catch (e) {
2257
- return `Invalid regex syntax: ${e.message}`;
2258
- }
2259
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
2260
- if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
2261
- return null;
2262
- }
2263
- function getCompiledRegex(pattern, flags = "") {
2264
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
2265
- const key = `${pattern}\0${flags}`;
2266
- if (regexCache.has(key)) {
2267
- const cached = regexCache.get(key);
2268
- regexCache.delete(key);
2269
- regexCache.set(key, cached);
2270
- return cached;
2271
- }
2272
- if (validateRegex(pattern) !== null) return null;
2273
- try {
2274
- const re = new RegExp(pattern, flags);
2275
- if (regexCache.size >= REGEX_CACHE_MAX) {
2276
- const oldest = regexCache.keys().next().value;
2277
- if (oldest) regexCache.delete(oldest);
2278
- }
2279
- regexCache.set(key, re);
2280
- return re;
2281
- } catch {
2282
- return null;
2283
- }
2284
- }
2285
- function matchesPattern(text, patterns) {
2286
- const p = Array.isArray(patterns) ? patterns : [patterns];
2287
- if (p.length === 0) return false;
2288
- const isMatch = pm(p, { nocase: true, dot: true });
2289
- const target = text.toLowerCase();
2290
- const directMatch = isMatch(target);
2291
- if (directMatch) return true;
2292
- const withoutDotSlash = text.replace(/^\.\//, "");
2293
- return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2294
- }
2295
- var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2296
- function getNestedValue(obj, path13) {
2297
- if (!obj || typeof obj !== "object") return null;
2298
- const segments = path13.split(".");
2299
- for (const seg of segments) {
2300
- if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2301
- }
2302
- return segments.reduce((prev, curr) => prev?.[curr], obj);
2303
- }
2304
- function evaluateSmartConditions(args, rule) {
2305
- if (!rule.conditions || rule.conditions.length === 0) return true;
2306
- const mode = rule.conditionMode ?? "all";
2307
- const fieldCache = /* @__PURE__ */ new Map();
2308
- const resolveField = (field) => {
2309
- if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
2310
- const rawVal = getNestedValue(args, field);
2311
- const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
2312
- const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
2313
- const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
2314
- fieldCache.set(field, val);
2315
- return val;
2316
- };
2317
- const results = rule.conditions.map((cond) => {
2318
- const val = resolveField(cond.field);
2319
- switch (cond.op) {
2320
- case "exists":
2321
- return val !== null && val !== "";
2322
- case "notExists":
2323
- return val === null || val === "";
2324
- case "contains":
2325
- return val !== null && cond.value ? val.includes(cond.value) : false;
2326
- case "notContains":
2327
- return val !== null && cond.value ? !val.includes(cond.value) : true;
2328
- case "matches": {
2329
- if (val === null || !cond.value) return false;
2330
- const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2331
- if (!reM) return false;
2332
- return reM.test(val);
2333
- }
2334
- case "notMatches": {
2335
- if (!cond.value) return false;
2336
- if (val === null) return true;
2337
- const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2338
- if (!reN) return false;
2339
- return !reN.test(val);
2340
- }
2341
- case "matchesGlob":
2342
- return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
2343
- case "notMatchesGlob":
2344
- return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
2345
- default:
2346
- return false;
2347
- }
2348
- });
2349
- return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2350
- }
2351
2635
  function resolveCheck(v) {
2352
2636
  return v === "off" || v === "block" ? v : "review";
2353
2637
  }
2354
2638
  function resolveCheckTight(v) {
2355
2639
  return v === "block" ? "block" : "review";
2356
2640
  }
2357
- var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2358
- var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2359
- function detectInlineExec(command) {
2360
- const pipeFed = command.includes("|");
2361
- const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2362
- for (const rawSeg of segments) {
2363
- const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2364
- let i = 0;
2365
- while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2366
- if (i >= tokens.length) continue;
2367
- const base = tokens[i].split("/").pop() ?? tokens[i];
2368
- if (!INLINE_INTERP.test(base)) continue;
2369
- const args = tokens.slice(i + 1);
2370
- let hadRedirect = false;
2371
- const positionals = [];
2372
- for (let j = 0; j < args.length; j++) {
2373
- const a = args[j];
2374
- if (a === "-") return true;
2375
- if (a.startsWith("<")) {
2376
- hadRedirect = true;
2377
- if (a === "<" || a === "<<") j++;
2378
- continue;
2379
- }
2380
- if (a.startsWith("-")) {
2381
- if (/^-(c|e|eval)$/i.test(a)) return true;
2382
- continue;
2383
- }
2384
- positionals.push(a);
2385
- }
2386
- if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2387
- return true;
2388
- }
2389
- }
2390
- return false;
2391
- }
2392
2641
  var VERDICT_RANK = {
2393
2642
  allow: 0,
2394
2643
  review: 1,
@@ -2402,6 +2651,12 @@ function resolvePinned(matches) {
2402
2651
  (best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
2403
2652
  );
2404
2653
  }
2654
+ function strictestVerdict(candidates) {
2655
+ if (candidates.length === 0) return void 0;
2656
+ return candidates.reduce(
2657
+ (best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
2658
+ );
2659
+ }
2405
2660
  function tokenize2(toolName) {
2406
2661
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
2407
2662
  }
@@ -2413,6 +2668,11 @@ function extractShellCommand(toolName, args, toolInspection) {
2413
2668
  const value = getNestedValue(args, fieldPath);
2414
2669
  return typeof value === "string" ? value : null;
2415
2670
  }
2671
+ function inspectsShellCommand(toolName, toolInspection) {
2672
+ const patterns = Object.keys(toolInspection);
2673
+ const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
2674
+ return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
2675
+ }
2416
2676
  function isSqlTool(toolName, toolInspection) {
2417
2677
  const patterns = Object.keys(toolInspection);
2418
2678
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -2475,8 +2735,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2475
2735
  };
2476
2736
  }
2477
2737
  }
2478
- if (wouldBeIgnored) return { decision: "allow" };
2479
- const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2738
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2739
+ const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2740
+ const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2741
+ const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2480
2742
  if (bashCommand !== null) {
2481
2743
  const pipeVerdict = pipeChainVerdict(
2482
2744
  bashCommand,
@@ -2526,8 +2788,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2526
2788
  }
2527
2789
  if (config.policy.smartRules.length > 0) {
2528
2790
  const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2791
+ const astSuppressed = (rule) => {
2792
+ if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
2793
+ const knob = rule.name === "review-drop-truncate-shell" ? resolveCheck(config.policy.commandChecks?.sqlDdl) : rule.name === "shield:filesystem:review-chmod-777" ? resolveCheck(config.policy.commandChecks?.chmod) : void 0;
2794
+ if (knob === "off" && rule.pinned) return false;
2795
+ return true;
2796
+ };
2529
2797
  const matches = config.policy.smartRules.filter(
2530
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2798
+ (rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2531
2799
  );
2532
2800
  const matchedRule = resolvePinned(matches);
2533
2801
  if (matchedRule) {
@@ -2558,15 +2826,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2558
2826
  const analyzed = analyzeShellCommand(shellCommand);
2559
2827
  allTokens = analyzed.allTokens;
2560
2828
  pathTokens = analyzed.paths;
2561
- const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2562
- if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2563
- return {
2564
- decision: inlineAction === "block" ? "block" : "review",
2565
- blockedByLabel: "Node9 Standard (Inline Execution)",
2566
- ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2567
- tier: 3
2568
- };
2569
- }
2829
+ const candidates = [];
2570
2830
  const evalVerdict = detectDangerousShellExec(shellCommand);
2571
2831
  if (evalVerdict === "block") {
2572
2832
  return {
@@ -2577,24 +2837,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2577
2837
  tier: 3
2578
2838
  };
2579
2839
  }
2840
+ const ptVerdict = pipeChainVerdict(
2841
+ shellCommand,
2842
+ isTrustedHost2,
2843
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2844
+ );
2845
+ if (ptVerdict?.decision === "allow") return ptVerdict;
2846
+ if (ptVerdict) candidates.push(ptVerdict);
2847
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2848
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2849
+ candidates.push({
2850
+ decision: inlineAction === "block" ? "block" : "review",
2851
+ blockedByLabel: "Node9 Standard (Inline Execution)",
2852
+ ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2853
+ tier: 3
2854
+ });
2855
+ }
2580
2856
  if (evalVerdict === "review") {
2581
- return {
2582
- // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2583
- // block but can never turn this off (eval-remote above is Class A —
2584
- // no knob at all).
2857
+ candidates.push({
2585
2858
  decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2586
2859
  blockedByLabel: "Node9: Eval Dynamic Content",
2587
2860
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2588
2861
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
2589
2862
  tier: 3
2590
- };
2863
+ });
2591
2864
  }
2592
- const ptVerdict = pipeChainVerdict(
2593
- shellCommand,
2594
- isTrustedHost2,
2595
- resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2596
- );
2597
- if (ptVerdict) return ptVerdict;
2865
+ const builtin = strictestVerdict(candidates);
2866
+ if (builtin) return builtin;
2598
2867
  if (config.policy.egress?.enabled) {
2599
2868
  const dests = extractShellDestinations(shellCommand);
2600
2869
  if (dests.length > 0) {
@@ -3645,28 +3914,41 @@ function readShieldOverrides() {
3645
3914
  var MODE_ORDER = ["observe", "audit", "standard", "strict"];
3646
3915
  var EGRESS_MODE_ORDER = ["off", "review", "block"];
3647
3916
  function rankIn(order, value) {
3648
- return order.indexOf(value);
3917
+ return value === void 0 ? -1 : order.indexOf(value);
3649
3918
  }
3650
- function resolveByOrder(order, local, cloud, locked) {
3651
- if (rankIn(order, cloud) === -1) return local;
3652
- if (locked) return cloud;
3919
+ function floorValue(order, local, cloud, opts = {}) {
3920
+ if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
3921
+ if (opts.locked) return cloud;
3922
+ const localSet = opts.localWasSet ?? local !== void 0;
3923
+ if (!localSet || rankIn(order, local) === -1) return cloud;
3653
3924
  return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
3654
3925
  }
3926
+ function strictestOf(order, ...values) {
3927
+ let best;
3928
+ for (const v of values) {
3929
+ if (rankIn(order, v) === -1) continue;
3930
+ if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
3931
+ }
3932
+ return best;
3933
+ }
3934
+ function resolveByOrder(order, local, cloud, locked) {
3935
+ return floorValue(order, local, cloud, { locked }) ?? local;
3936
+ }
3655
3937
  function resolveManagedMode(local, cloud, locked) {
3656
3938
  return resolveByOrder(MODE_ORDER, local, cloud, locked);
3657
3939
  }
3658
- function applyManagedEgress(local, managed, locked) {
3940
+ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
3659
3941
  const next = { ...local };
3660
3942
  if (typeof managed.enabled === "boolean") {
3661
3943
  next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
3662
3944
  }
3663
3945
  if (typeof managed.mode === "string") {
3664
- next.mode = resolveByOrder(
3665
- EGRESS_MODE_ORDER,
3666
- local.mode,
3667
- managed.mode,
3668
- locked.includes("egressMode")
3669
- );
3946
+ next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
3947
+ locked: locked.includes("egressMode"),
3948
+ // The default 'review' is seeded into egress before any merge, so absence
3949
+ // is invisible from `local.mode` alone — the caller tracks it for us.
3950
+ localWasSet: localModeUserSet
3951
+ }) ?? local.mode;
3670
3952
  }
3671
3953
  if (Array.isArray(managed.allow) && managed.allow.length > 0) {
3672
3954
  next.allow = [...managed.allow];
@@ -3686,6 +3968,9 @@ function applyManagedDlp(local, managed, locked) {
3686
3968
  if (typeof managed.enabled === "boolean") {
3687
3969
  next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
3688
3970
  }
3971
+ if (managed.enabled === true) {
3972
+ next.scanIgnoredTools = true;
3973
+ }
3689
3974
  if (typeof managed.pii === "string") {
3690
3975
  next.pii = resolveByOrder(
3691
3976
  DLP_PII_ORDER,
@@ -3719,13 +4004,9 @@ function applyManagedCommandChecks(local, managed, locked) {
3719
4004
  const m = managed[key];
3720
4005
  if (typeof m !== "string") continue;
3721
4006
  const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3722
- const localValue = local[key];
3723
- const resolved = localValue === void 0 ? m : resolveByOrder(
3724
- COMMAND_CHECK_ORDER,
3725
- localValue,
3726
- m,
3727
- locked.includes(lockKey)
3728
- );
4007
+ const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
4008
+ locked: locked.includes(lockKey)
4009
+ });
3729
4010
  if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3730
4011
  next[key] = resolved;
3731
4012
  }
@@ -3751,11 +4032,18 @@ function slug(s) {
3751
4032
  var B = "[\\s/\\\\]";
3752
4033
  var SEP = "[/\\\\]";
3753
4034
  function pathToRegexFragment(rawPath) {
3754
- const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
4035
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
3755
4036
  const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
3756
4037
  if (segments.length === 0) return "";
3757
4038
  return `(^|${B})${segments.join(SEP)}(${B}|$)`;
3758
4039
  }
4040
+ function pathMatchesFragment(candidate, rawPath) {
4041
+ const value = pathToRegexFragment(rawPath);
4042
+ if (!value || !candidate) return false;
4043
+ const re = getCompiledRegex(value);
4044
+ if (!re) return false;
4045
+ return re.test(candidate);
4046
+ }
3759
4047
  function pathRules(rawPath, verdict, reason) {
3760
4048
  const value = pathToRegexFragment(rawPath);
3761
4049
  if (!value) return [];
@@ -3769,12 +4057,28 @@ function pathRules(rawPath, verdict, reason) {
3769
4057
  verdict,
3770
4058
  reason: why
3771
4059
  },
4060
+ // Keep the historical `-anytool` name for the file_path rule: the
4061
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
3772
4062
  {
3773
4063
  name: `${verdict}-path-${s}-anytool`,
3774
4064
  tool: "*",
3775
4065
  conditions: [{ field: "file_path", op: "matches", value }],
3776
4066
  verdict,
3777
4067
  reason: why
4068
+ },
4069
+ {
4070
+ name: `${verdict}-path-${s}-anytool-path`,
4071
+ tool: "*",
4072
+ conditions: [{ field: "path", op: "matches", value }],
4073
+ verdict,
4074
+ reason: why
4075
+ },
4076
+ {
4077
+ name: `${verdict}-path-${s}-anytool-pattern`,
4078
+ tool: "*",
4079
+ conditions: [{ field: "pattern", op: "matches", value }],
4080
+ verdict,
4081
+ reason: why
3778
4082
  }
3779
4083
  ];
3780
4084
  }
@@ -3847,8 +4151,14 @@ var DEFAULT_CONFIG = {
3847
4151
  settings: {
3848
4152
  mode: "standard",
3849
4153
  autoStartDaemon: true,
3850
- enableUndo: true,
3851
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
4154
+ // OFF by default. The snapshot store is a per-project bare git repo with
4155
+ // no size ceiling, and eviction drops the index row without deleting the
4156
+ // objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
4157
+ // from interrupted `git gc`) and filled the disk. A security tool must not
4158
+ // be what fills a customer's disk. Re-enable per install with
4159
+ // `{"settings":{"enableUndo":true}}`; the default flips back when the
4160
+ // bounded copy-store lands (doc/undo-v2-copy-store-design.md).
4161
+ enableUndo: false,
3852
4162
  enableHookLogDebug: true,
3853
4163
  approvalTimeoutMs: 12e4,
3854
4164
  // 120-second auto-deny timeout
@@ -4047,10 +4357,13 @@ var DEFAULT_CONFIG = {
4047
4357
  skillPinning: { enabled: false, mode: "warn", roots: [] },
4048
4358
  trustedHosts: [],
4049
4359
  trustedHostsManaged: false,
4050
- appPermissions: {}
4360
+ appPermissions: {},
4361
+ managedJailPaths: []
4051
4362
  },
4052
4363
  environments: {}
4053
4364
  };
4365
+ var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
4366
+ var VERDICT_ORDER = ["allow", "review", "block"];
4054
4367
  var ADVISORY_SMART_RULES = [
4055
4368
  // ── rm safety ─────────────────────────────────────────────────────────────
4056
4369
  // tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
@@ -4062,12 +4375,7 @@ var ADVISORY_SMART_RULES = [
4062
4375
  conditionMode: "all",
4063
4376
  conditions: [
4064
4377
  { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
4065
- {
4066
- field: "command",
4067
- op: "matches",
4068
- // Matches known-safe build artifact paths in the command.
4069
- value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
4070
- }
4378
+ { field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
4071
4379
  ],
4072
4380
  verdict: "allow",
4073
4381
  reason: "Deleting a known-safe build artifact path"
@@ -4245,10 +4553,15 @@ function getConfig(cwd) {
4245
4553
  // here. A managed list fills this below and flips trustedHostsManaged.
4246
4554
  trustedHosts: [],
4247
4555
  trustedHostsManaged: false,
4248
- appPermissions: {}
4556
+ appPermissions: {},
4557
+ managedJailPaths: []
4249
4558
  };
4250
4559
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
4251
- const applyLayer = (source) => {
4560
+ const rank = (v) => {
4561
+ const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
4562
+ return i === -1 ? 1 : i;
4563
+ };
4564
+ const applyLayer = (source, isProject = false) => {
4252
4565
  if (!source) return;
4253
4566
  const s = source.settings || {};
4254
4567
  const p = source.policy || {};
@@ -4308,7 +4621,9 @@ function getConfig(cwd) {
4308
4621
  };
4309
4622
  for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4310
4623
  const v = src[k];
4311
- if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4624
+ if (v !== "off" && v !== "review" && v !== "block") continue;
4625
+ if (isProject && rank(v) < rank(cc2[k])) continue;
4626
+ cc2[k] = v;
4312
4627
  }
4313
4628
  for (const k of ["evalDynamic", "pipeChainHigh"]) {
4314
4629
  const v = src[k];
@@ -4318,11 +4633,22 @@ function getConfig(cwd) {
4318
4633
  }
4319
4634
  if (p.egress) {
4320
4635
  const e = p.egress;
4321
- if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
4322
- if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
4323
- if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
4636
+ if (e.enabled !== void 0 && !(isProject && e.enabled === false))
4637
+ mergedPolicy.egress.enabled = e.enabled;
4638
+ if (e.mode !== void 0) {
4639
+ const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
4640
+ if (!weaker) {
4641
+ mergedPolicy.egress.mode = e.mode;
4642
+ egressModeUserSet = true;
4643
+ }
4644
+ }
4645
+ if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
4646
+ mergedPolicy.egress.allow.push(...e.allow);
4647
+ }
4648
+ if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
4324
4649
  if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
4325
- if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
4650
+ if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
4651
+ mergedPolicy.egress.allowPrivate = e.allowPrivate;
4326
4652
  }
4327
4653
  if (p.loopDetection) {
4328
4654
  const ld = p.loopDetection;
@@ -4364,11 +4690,21 @@ function getConfig(cwd) {
4364
4690
  }
4365
4691
  }
4366
4692
  };
4693
+ let egressModeUserSet = false;
4694
+ let egressAllowUserSet = false;
4367
4695
  applyLayer(globalConfig);
4368
- applyLayer(projectConfig);
4696
+ applyLayer(
4697
+ projectConfig,
4698
+ /* isProject */
4699
+ true
4700
+ );
4369
4701
  let cloudManagedShields = [];
4702
+ const managedCommandCheckKeys = /* @__PURE__ */ new Set();
4703
+ const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
4370
4704
  let modeCloudControlled = false;
4371
4705
  let modeCloudStaged = false;
4706
+ let cloudMandatesEnforcement = false;
4707
+ let cloudMandatesAppPerm = false;
4372
4708
  {
4373
4709
  const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
4374
4710
  try {
@@ -4403,7 +4739,8 @@ function getConfig(cwd) {
4403
4739
  deny: hosts(mc.egress.deny),
4404
4740
  allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
4405
4741
  },
4406
- locked
4742
+ locked,
4743
+ egressModeUserSet
4407
4744
  );
4408
4745
  }
4409
4746
  if (mc.dlp && typeof mc.dlp === "object") {
@@ -4423,6 +4760,12 @@ function getConfig(cwd) {
4423
4760
  mc.commandChecks,
4424
4761
  locked
4425
4762
  );
4763
+ for (const [key, val] of Object.entries(mc.commandChecks)) {
4764
+ if (typeof val !== "string") continue;
4765
+ managedCommandCheckKeys.add(key);
4766
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4767
+ if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
4768
+ }
4426
4769
  }
4427
4770
  if (mc.approvers && typeof mc.approvers === "object") {
4428
4771
  const bool = (v) => typeof v === "boolean" ? v : void 0;
@@ -4469,12 +4812,13 @@ function getConfig(cwd) {
4469
4812
  }
4470
4813
  if (Array.isArray(mc.jailPaths)) {
4471
4814
  for (const jp of mc.jailPaths) {
4472
- const path13 = typeof jp?.path === "string" ? jp.path.trim() : "";
4473
- if (!path13) continue;
4815
+ const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
4816
+ if (!path14) continue;
4474
4817
  const verdict = jp?.verdict === "review" ? "review" : "block";
4475
- for (const r of pathRules(path13, verdict, "org-managed jail")) {
4818
+ for (const r of pathRules(path14, verdict, "org-managed jail")) {
4476
4819
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
4477
4820
  }
4821
+ mergedPolicy.managedJailPaths.push({ path: path14, verdict });
4478
4822
  }
4479
4823
  }
4480
4824
  if (Array.isArray(mc.trustedHosts)) {
@@ -4492,7 +4836,14 @@ function getConfig(cwd) {
4492
4836
  if (Object.keys(m).length) coerced[srv] = m;
4493
4837
  }
4494
4838
  mergedPolicy.appPermissions = coerced;
4839
+ cloudMandatesAppPerm = Object.values(coerced).some(
4840
+ (tools) => Object.values(tools).some((d) => d === "block" || d === "review")
4841
+ );
4495
4842
  }
4843
+ const on = (v) => !!v && typeof v === "object" && v.enabled === true;
4844
+ cloudMandatesEnforcement = cloudMandatesAppPerm || Array.isArray(mc.jailPaths) && mc.jailPaths.some((jp) => typeof jp?.path === "string" && jp.path.trim() !== "") || on(mc.egress) || on(mc.dlp) || on(mc.injectionScan) || on(mc.skillPinning) || on(mc.loopDetection) || !!mc.commandChecks && typeof mc.commandChecks === "object" && Object.values(mc.commandChecks).some(
4845
+ (v) => typeof v === "string" && v !== "off"
4846
+ );
4496
4847
  }
4497
4848
  if (raw.panicMode === true) {
4498
4849
  mergedSettings.panicMode = true;
@@ -4534,25 +4885,45 @@ function getConfig(cwd) {
4534
4885
  }
4535
4886
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4536
4887
  const cc = mergedPolicy.commandChecks ?? {};
4537
- const advisoryKnob = (name) => {
4538
- if (name === "review-rm") return cc.rmAdvisory;
4539
- if (name?.endsWith("-sql")) return cc.sqlDdl;
4888
+ const advisoryKnobKey = (name) => {
4889
+ if (name === "review-rm") return "rmAdvisory";
4890
+ if (name?.endsWith("-sql")) return "sqlDdl";
4540
4891
  return void 0;
4541
4892
  };
4542
4893
  for (const rule of ADVISORY_SMART_RULES) {
4543
- if (existingAdvisoryNames.has(rule.name)) continue;
4544
- const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4894
+ const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
4895
+ const knob = knobKey ? cc[knobKey] : void 0;
4545
4896
  if (knob === "off") continue;
4546
- mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4897
+ const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
4898
+ const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
4899
+ const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
4900
+ const knobVerdict = knob === "block" ? "block" : rule.verdict;
4901
+ if (!managed) {
4902
+ if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
4903
+ continue;
4904
+ }
4905
+ const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
4906
+ if (twin) {
4907
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4908
+ }
4909
+ const injected = { ...rule, verdict: effective, pinned: true };
4910
+ if (rule.name === "review-rm" && effective !== "block") {
4911
+ injected.conditions = [
4912
+ ...rule.conditions ?? [],
4913
+ { field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
4914
+ ];
4915
+ injected.conditionMode = "all";
4916
+ }
4917
+ mergedPolicy.smartRules.push(injected);
4547
4918
  }
4548
4919
  const envMode = process.env.NODE9_MODE;
4549
4920
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4550
4921
  mergedSettings.mode = envMode;
4551
4922
  }
4552
- if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4923
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4553
4924
  mergedSettings.mode = "standard";
4554
4925
  }
4555
- const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4926
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
4556
4927
  if (modeCloudControlled && mergedSettings.mode === "strict") {
4557
4928
  for (const name of Object.keys(mergedEnvironments)) {
4558
4929
  if (mergedEnvironments[name]?.requireApproval === false) {
@@ -4753,14 +5124,14 @@ function checkProvenance(cmd, cwd) {
4753
5124
  }
4754
5125
 
4755
5126
  // src/policy/index.ts
4756
- async function evaluatePolicy2(toolName, args, agent, cwd) {
5127
+ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
4757
5128
  const config = getConfig();
4758
5129
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
4759
5130
  return evaluatePolicy(
4760
5131
  config,
4761
5132
  toolName,
4762
5133
  args,
4763
- { agent, cwd, activeEnvironment },
5134
+ { agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
4764
5135
  {
4765
5136
  checkProvenance,
4766
5137
  // Managed → match against the org list (frozen with the rest of managed
@@ -5612,6 +5983,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
5612
5983
  }
5613
5984
  }
5614
5985
 
5986
+ // src/shields/jail.ts
5987
+ import fs11 from "fs";
5988
+ import os10 from "os";
5989
+ import path13 from "path";
5990
+ var USER_JAIL_SHIELD = "user-jail";
5991
+ function jailStorePath() {
5992
+ return path13.join(os10.homedir(), ".node9", "jail-paths.json");
5993
+ }
5994
+ function readJailPaths() {
5995
+ let text;
5996
+ try {
5997
+ text = fs11.readFileSync(jailStorePath(), "utf8");
5998
+ } catch (err) {
5999
+ if (err.code === "ENOENT") return [];
6000
+ throw err;
6001
+ }
6002
+ let parsed;
6003
+ try {
6004
+ parsed = JSON.parse(text);
6005
+ } catch {
6006
+ throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
6007
+ }
6008
+ if (!Array.isArray(parsed.paths)) return [];
6009
+ return parsed.paths.filter(
6010
+ (p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
6011
+ );
6012
+ }
6013
+ function findJailedPath(candidate) {
6014
+ return findJailedPathIn(candidate, readJailPaths());
6015
+ }
6016
+ function findJailedPathIn(candidate, paths) {
6017
+ if (!candidate) return null;
6018
+ for (const entry of paths) {
6019
+ if (pathMatchesFragment(candidate, entry.path)) return entry;
6020
+ }
6021
+ return null;
6022
+ }
6023
+
5615
6024
  // src/auth/orchestrator.ts
5616
6025
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
5617
6026
  "write",
@@ -6087,12 +6496,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
6087
6496
  } else if (!taintWarning && !appPermReview) {
6088
6497
  const toolLower = toolName.toLowerCase();
6089
6498
  const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
6090
- if (isFileTool && readActiveShields().includes("project-jail")) {
6499
+ const activeShields = isFileTool ? readActiveShields() : [];
6500
+ const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
6501
+ if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
6091
6502
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
6092
- const filePath = String(
6093
- argsObj.file_path ?? argsObj.path ?? argsObj.pattern ?? argsObj.filename ?? ""
6094
- );
6095
- if (filePath && scanFilePath(filePath)) {
6503
+ const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
6504
+ const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
6505
+ const sensitiveHit = candidates.some((c) => scanFilePath(c));
6506
+ if (jailHit || sensitiveHit) {
6507
+ const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
6508
+ skipIgnoredFastPath: true
6509
+ });
6510
+ if (policyResult.decision === "block") {
6511
+ if (!isManual)
6512
+ appendLocalAudit(
6513
+ toolName,
6514
+ args,
6515
+ "deny",
6516
+ "smart-rule-block",
6517
+ { ...meta, ruleName: policyResult.ruleName },
6518
+ hashAuditArgs
6519
+ );
6520
+ return {
6521
+ approved: false,
6522
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
6523
+ blockedBy: "local-config",
6524
+ blockedByLabel: policyResult.blockedByLabel,
6525
+ ruleHit: policyResult.ruleName
6526
+ };
6527
+ }
6096
6528
  } else {
6097
6529
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
6098
6530
  return { approved: true };
@@ -6155,7 +6587,12 @@ ${appPermReview}`
6155
6587
  }
6156
6588
  let cloudRequestId = null;
6157
6589
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
6158
- const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || void 0;
6590
+ const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
6591
+ // a client-side heuristic the SaaS has no rule for, so without forceReview
6592
+ // its checkRule answers "no org rule matched" → {approved:true}, which is
6593
+ // not an approval of an exfiltration risk. Measured against the live BE:
6594
+ // {approved:true} without this flag, {pending:true} with it.
6595
+ !!taintWarning || void 0;
6159
6596
  if (cloudEnforced) {
6160
6597
  try {
6161
6598
  const initResult = await initNode9SaaS(
@@ -6168,10 +6605,10 @@ ${appPermReview}`
6168
6605
  forceReview
6169
6606
  );
6170
6607
  if (!initResult.pending) {
6171
- if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6608
+ if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6172
6609
  return { approved: true, checkedBy: "cloud" };
6173
6610
  }
6174
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6611
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6175
6612
  return {
6176
6613
  approved: !!initResult.approved,
6177
6614
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),