@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.js CHANGED
@@ -410,8 +410,8 @@ function sanitizeConfig(raw) {
410
410
  }
411
411
  }
412
412
  const lines = result.error.issues.map((issue) => {
413
- const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
414
- return ` \u2022 ${path13}: ${issue.message}`;
413
+ const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
414
+ return ` \u2022 ${path14}: ${issue.message}`;
415
415
  });
416
416
  return {
417
417
  sanitized,
@@ -1031,6 +1031,129 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1031
1031
  }
1032
1032
  return null;
1033
1033
  }
1034
+ var MAX_REGEX_LENGTH = 256;
1035
+ var REGEX_CACHE_MAX = 500;
1036
+ var regexCache = /* @__PURE__ */ new Map();
1037
+ function validateRegex(pattern) {
1038
+ if (!pattern) return "Pattern is required";
1039
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
1040
+ try {
1041
+ new RegExp(pattern);
1042
+ } catch (e) {
1043
+ return `Invalid regex syntax: ${e.message}`;
1044
+ }
1045
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
1046
+ if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
1047
+ return null;
1048
+ }
1049
+ function getCompiledRegex(pattern, flags = "") {
1050
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
1051
+ const key = `${pattern}\0${flags}`;
1052
+ if (regexCache.has(key)) {
1053
+ const cached = regexCache.get(key);
1054
+ regexCache.delete(key);
1055
+ regexCache.set(key, cached);
1056
+ return cached;
1057
+ }
1058
+ if (validateRegex(pattern) !== null) return null;
1059
+ try {
1060
+ const re = new RegExp(pattern, flags);
1061
+ if (regexCache.size >= REGEX_CACHE_MAX) {
1062
+ const oldest = regexCache.keys().next().value;
1063
+ if (oldest) regexCache.delete(oldest);
1064
+ }
1065
+ regexCache.set(key, re);
1066
+ return re;
1067
+ } catch {
1068
+ return null;
1069
+ }
1070
+ }
1071
+ function matchesPattern(text, patterns) {
1072
+ const p = Array.isArray(patterns) ? patterns : [patterns];
1073
+ if (p.length === 0) return false;
1074
+ const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
1075
+ const target = text.toLowerCase();
1076
+ const directMatch = isMatch(target);
1077
+ if (directMatch) return true;
1078
+ const withoutDotSlash = text.replace(/^\.\//, "");
1079
+ return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1080
+ }
1081
+ var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1082
+ function getNestedValue(obj, path14) {
1083
+ if (!obj || typeof obj !== "object") return null;
1084
+ const segments = path14.split(".");
1085
+ for (const seg of segments) {
1086
+ if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1087
+ }
1088
+ return segments.reduce((prev, curr) => prev?.[curr], obj);
1089
+ }
1090
+ function evaluateSmartConditions(args, rule) {
1091
+ if (!rule.conditions || rule.conditions.length === 0) return true;
1092
+ const mode = rule.conditionMode ?? "all";
1093
+ const fieldCache = /* @__PURE__ */ new Map();
1094
+ const resolveField = (field) => {
1095
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
1096
+ const rawVal = getNestedValue(args, field);
1097
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
1098
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
1099
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1100
+ fieldCache.set(field, val);
1101
+ return val;
1102
+ };
1103
+ const readingsCache = /* @__PURE__ */ new Map();
1104
+ const resolveFieldReadings = (field) => {
1105
+ const cached = readingsCache.get(field);
1106
+ if (cached) return cached;
1107
+ const primary = resolveField(field);
1108
+ if (primary === null) {
1109
+ readingsCache.set(field, []);
1110
+ return [];
1111
+ }
1112
+ let out = [primary];
1113
+ if (field === "command") {
1114
+ const raw = getNestedValue(args, field);
1115
+ if (typeof raw === "string") {
1116
+ const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
1117
+ out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
1118
+ }
1119
+ }
1120
+ readingsCache.set(field, out);
1121
+ return out;
1122
+ };
1123
+ const results = rule.conditions.map((cond) => {
1124
+ const val = resolveField(cond.field);
1125
+ switch (cond.op) {
1126
+ case "exists":
1127
+ return val !== null && val !== "";
1128
+ case "notExists":
1129
+ return val === null || val === "";
1130
+ case "contains":
1131
+ return val !== null && cond.value ? val.includes(cond.value) : false;
1132
+ case "notContains":
1133
+ return val !== null && cond.value ? !val.includes(cond.value) : true;
1134
+ case "matches": {
1135
+ if (val === null || !cond.value) return false;
1136
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1137
+ if (!reM) return false;
1138
+ return resolveFieldReadings(cond.field).some((v) => reM.test(v));
1139
+ }
1140
+ case "notMatches": {
1141
+ if (!cond.value) return false;
1142
+ if (val === null) return true;
1143
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1144
+ if (!reN) return false;
1145
+ return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
1146
+ }
1147
+ case "matchesGlob":
1148
+ return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
1149
+ case "notMatchesGlob":
1150
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
1151
+ default:
1152
+ return false;
1153
+ }
1154
+ });
1155
+ return mode === "any" ? results.some((r) => r) : results.every((r) => r);
1156
+ }
1034
1157
  var { syntax } = import_mvdan_sh.default;
1035
1158
  var sharedParser = syntax.NewParser();
1036
1159
  var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -1103,14 +1226,22 @@ function cachedNormalize(command, compute) {
1103
1226
  return result;
1104
1227
  }
1105
1228
  function normalizeCommandForPolicy(command) {
1229
+ return commandReadingsImpl(command).posix;
1230
+ }
1231
+ function commandReadings(command) {
1232
+ const r = commandReadingsImpl(command);
1233
+ return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
1234
+ }
1235
+ function commandReadingsImpl(command) {
1106
1236
  return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
1107
1237
  }
1108
1238
  function normalizeCommandForPolicyImpl(command) {
1109
1239
  const f = parseShared(command);
1110
- if (f === PARSE_FAIL) return command;
1240
+ if (f === PARSE_FAIL) return { posix: command, separator: command };
1111
1241
  try {
1112
1242
  const strips = [];
1113
1243
  const rewrites = [];
1244
+ const quoteOnlyRewrites = [];
1114
1245
  const msgSpans = /* @__PURE__ */ new Set();
1115
1246
  syntax.Walk(f, (node) => {
1116
1247
  if (!node) return false;
@@ -1155,22 +1286,23 @@ function normalizeCommandForPolicyImpl(command) {
1155
1286
  if (resolved === source) continue;
1156
1287
  if (resolved === "" || /\s/.test(resolved)) continue;
1157
1288
  rewrites.push([s, e, resolved]);
1289
+ const quoteOnly = source.replace(/['"]/g, "");
1290
+ if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
1158
1291
  }
1159
1292
  return true;
1160
1293
  });
1161
- const edits = [
1162
- ...strips.map(([s, e]) => [s, e, '""']),
1163
- ...rewrites
1164
- ];
1165
- if (edits.length === 0) return command;
1166
- edits.sort((a, b) => b[0] - a[0]);
1167
- let result = command;
1168
- for (const [s, e, rep] of edits) {
1169
- result = result.slice(0, s) + rep + result.slice(e);
1170
- }
1171
- return result;
1294
+ const stripEdits = strips.map(([s, e]) => [s, e, '""']);
1295
+ const apply = (extra) => {
1296
+ const edits = [...stripEdits, ...extra];
1297
+ if (edits.length === 0) return command;
1298
+ edits.sort((a, b) => b[0] - a[0]);
1299
+ let out = command;
1300
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
1301
+ return out;
1302
+ };
1303
+ return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
1172
1304
  } catch {
1173
- return command;
1305
+ return { posix: command, separator: command };
1174
1306
  }
1175
1307
  }
1176
1308
  function scanArgsForDynamicExec(args, startIdx) {
@@ -1239,9 +1371,34 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1239
1371
  "vi",
1240
1372
  "emacs",
1241
1373
  "code",
1242
- "type"
1374
+ "type",
1375
+ // — the 22 that were missing —
1376
+ "grep",
1377
+ "egrep",
1378
+ "fgrep",
1379
+ "rg",
1380
+ "ag",
1381
+ "ack",
1382
+ "awk",
1383
+ "gawk",
1384
+ "sed",
1385
+ "cut",
1386
+ "tr",
1387
+ "jq",
1388
+ "yq",
1389
+ "od",
1390
+ "xxd",
1391
+ "hexdump",
1392
+ "strings",
1393
+ "sort",
1394
+ "uniq",
1395
+ "tac",
1396
+ "nl",
1397
+ "dd"
1243
1398
  ]);
1244
- var FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
1399
+ var FS_OP_PRESCREEN_RE = new RegExp(
1400
+ `(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
1401
+ );
1245
1402
  var HOME_CACHE_ALLOWLIST = [
1246
1403
  ".cache",
1247
1404
  ".npm/_npx",
@@ -1280,9 +1437,37 @@ var SENSITIVE_PATH_RULES = [
1280
1437
  // for the canonical test-asserted contract.
1281
1438
  rule: "shield:project-jail:block-read-env",
1282
1439
  reason: "Reading .env files is blocked by project-jail shield",
1283
- match: (p) => /(?:^|[\\/])\.env(?:\.(?:local|production|staging|development|production\.local|staging\.local|development\.local))?$/i.test(
1284
- p
1285
- )
1440
+ // Structural, not a list. The previous form enumerated seven suffixes and
1441
+ // anchored on `$`, so `.env.prod`, `.env.ci` and `.env.local.bak` — all
1442
+ // gitignored, all routinely holding real secrets — were never covered. A
1443
+ // hand-written list of what to protect is only ever as complete as the day
1444
+ // it was typed; this says "`.env` plus any suffix chain" and then names the
1445
+ // exceptions, which is the direction that fails safe.
1446
+ //
1447
+ // \.env the segment itself
1448
+ // (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
1449
+ // files. Without it a flat suffix class swallows both.
1450
+ // (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
1451
+ // [\w.-]*$ any suffix chain. Flat class, no nested quantifier —
1452
+ // `(\.[\w-]+)*` reads the same but is rejected by
1453
+ // safe-regex2, and this pattern runs on the hook hot path.
1454
+ //
1455
+ // The two exclusions are NOT the same shape, because the words do not mean
1456
+ // the same thing:
1457
+ //
1458
+ // (?!\.(?:example|sample|template)\b) — "this file is a fixture", and it
1459
+ // stays a fixture whatever follows, so `.env.example.md` is allowed too.
1460
+ // These are checked into git by convention: already public, so blocking
1461
+ // them buys nothing and costs the most common legitimate agent read.
1462
+ //
1463
+ // (?!\.test$) — anchored, because `test` names an ENVIRONMENT, not a
1464
+ // fixture. `.env.test` is the committed template and stays allowed, but
1465
+ // `.env.test.local` is gitignored by the `.env*.local` convention and
1466
+ // holds real values, so it must block. Using `\b` here — the obvious
1467
+ // symmetry — silently exempts every `.env.test.*` file.
1468
+ //
1469
+ // shields.test.ts:983-995 is the canonical contract; keep both in step.
1470
+ match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
1286
1471
  },
1287
1472
  {
1288
1473
  // verdict: 'review' (not 'block') is a deliberate design choice
@@ -1411,6 +1596,208 @@ function chmodHasOpenPermMode(command) {
1411
1596
  }
1412
1597
  return found;
1413
1598
  }
1599
+ function isShellShapedTool(toolName, toolInspection) {
1600
+ if (isBashTool(toolName)) return true;
1601
+ if (!toolInspection) return false;
1602
+ const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
1603
+ return pattern !== void 0 && toolInspection[pattern] === "command";
1604
+ }
1605
+ function toolMatchesRule(toolName, ruleTool, toolInspection) {
1606
+ if (!ruleTool) return true;
1607
+ if (matchesPattern(toolName, ruleTool)) return true;
1608
+ return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
1609
+ }
1610
+ 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;
1611
+ var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
1612
+ "uv",
1613
+ "uvx",
1614
+ "poetry",
1615
+ "pipenv",
1616
+ "pdm",
1617
+ "rye",
1618
+ "hatch",
1619
+ "conda",
1620
+ "mamba",
1621
+ "micromamba",
1622
+ "npx",
1623
+ "pnpm",
1624
+ "yarn",
1625
+ "bunx",
1626
+ "watch",
1627
+ "strace",
1628
+ "ltrace",
1629
+ "chroot",
1630
+ "unshare",
1631
+ "runuser"
1632
+ ]);
1633
+ function isInlineCodeFlag(interp, w) {
1634
+ const lw = w.toLowerCase();
1635
+ if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
1636
+ if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
1637
+ if (!w.startsWith("-") || w.startsWith("--")) return false;
1638
+ const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1639
+ const body = lw.slice(1);
1640
+ const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
1641
+ const cut = body.search(cutAt);
1642
+ const bundle = cut >= 0 ? body.slice(0, cut) : body;
1643
+ return [...codeLetters].some((l) => bundle.includes(l));
1644
+ }
1645
+ var _redirStdinOps = null;
1646
+ function redirStdinOps() {
1647
+ if (_redirStdinOps) return _redirStdinOps;
1648
+ _redirStdinOps = new Set(
1649
+ [
1650
+ deriveRedirOp("cat <<X\nX"),
1651
+ deriveRedirOp("cat <<-X\nX"),
1652
+ deriveRedirOp("cat < f"),
1653
+ deriveRedirOp("cat <<< x")
1654
+ ].filter((op) => op >= 0)
1655
+ );
1656
+ return _redirStdinOps;
1657
+ }
1658
+ function deriveBinaryOp(sample) {
1659
+ try {
1660
+ const f = sharedParser.Parse(sample, "cmd");
1661
+ let op = -1;
1662
+ syntax.Walk(f, (node) => {
1663
+ const n = node;
1664
+ if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
1665
+ return true;
1666
+ });
1667
+ return op;
1668
+ } catch {
1669
+ return -1;
1670
+ }
1671
+ }
1672
+ var _listOps = null;
1673
+ function listOps() {
1674
+ if (_listOps) return _listOps;
1675
+ _listOps = new Set(
1676
+ [deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
1677
+ );
1678
+ return _listOps;
1679
+ }
1680
+ var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
1681
+ var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
1682
+ function unwrapCommandHead(words) {
1683
+ let i = 0;
1684
+ while (i < words.length) {
1685
+ const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
1686
+ if (head === "find") {
1687
+ const x = words.findIndex(
1688
+ (w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
1689
+ );
1690
+ if (x < 0) break;
1691
+ i = x + 1;
1692
+ continue;
1693
+ }
1694
+ if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
1695
+ i++;
1696
+ let targetConsumed = false;
1697
+ while (i < words.length) {
1698
+ const t = words[i];
1699
+ if (t === null) {
1700
+ i++;
1701
+ continue;
1702
+ }
1703
+ const lt = t.toLowerCase();
1704
+ if (/^[A-Za-z_]\w*=/.test(t)) {
1705
+ i++;
1706
+ continue;
1707
+ }
1708
+ if (t.startsWith("-")) {
1709
+ i++;
1710
+ const nxt = words[i];
1711
+ if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
1712
+ i++;
1713
+ continue;
1714
+ }
1715
+ if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
1716
+ i++;
1717
+ continue;
1718
+ }
1719
+ if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
1720
+ targetConsumed = true;
1721
+ i++;
1722
+ continue;
1723
+ }
1724
+ break;
1725
+ }
1726
+ }
1727
+ return i;
1728
+ }
1729
+ function inlineExecStmt(stmt, pipeFed) {
1730
+ const cmd = stmt?.Cmd;
1731
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1732
+ const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1733
+ if (words.length === 0) return false;
1734
+ const headIdx = unwrapCommandHead(words);
1735
+ const rawHead = words[headIdx];
1736
+ if (rawHead == null) return false;
1737
+ const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1738
+ if (!INLINE_INTERPRETER.test(interp)) return false;
1739
+ let args = words.slice(headIdx + 1);
1740
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1741
+ if (INTERP_LEADING_TARGET.has(interp)) {
1742
+ const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1743
+ args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1744
+ }
1745
+ let positionals = 0;
1746
+ let selectedProgram = false;
1747
+ for (const a of args) {
1748
+ if (a == null) {
1749
+ positionals++;
1750
+ selectedProgram = true;
1751
+ continue;
1752
+ }
1753
+ if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
1754
+ if (a === "-m") {
1755
+ selectedProgram = true;
1756
+ continue;
1757
+ }
1758
+ if (a === "-" && !selectedProgram) return true;
1759
+ if (!a.startsWith("-")) {
1760
+ positionals++;
1761
+ selectedProgram = true;
1762
+ }
1763
+ }
1764
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1765
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1766
+ if (positionals === 0 && (stdinFed || pipeFed)) {
1767
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1768
+ }
1769
+ return false;
1770
+ }
1771
+ function detectInlineExec(command) {
1772
+ const f = parseShared(command);
1773
+ if (f === PARSE_FAIL) {
1774
+ 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(
1775
+ command
1776
+ );
1777
+ }
1778
+ let found = false;
1779
+ try {
1780
+ syntax.Walk(f, (node) => {
1781
+ if (!node || found) return false;
1782
+ const n = node;
1783
+ const t = syntax.NodeType(n);
1784
+ if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1785
+ if (inlineExecStmt(n.Y, true)) {
1786
+ found = true;
1787
+ return false;
1788
+ }
1789
+ }
1790
+ if (t === "Stmt" && inlineExecStmt(n, false)) {
1791
+ found = true;
1792
+ return false;
1793
+ }
1794
+ return true;
1795
+ });
1796
+ } catch {
1797
+ return found;
1798
+ }
1799
+ return found;
1800
+ }
1414
1801
  function analyzeChmod777(command) {
1415
1802
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
1416
1803
  if (!chmodHasOpenPermMode(command)) return null;
@@ -2275,150 +2662,12 @@ function extractAllSshHosts(tokens) {
2275
2662
  }
2276
2663
  return [...hosts].filter(Boolean);
2277
2664
  }
2278
- var MAX_REGEX_LENGTH = 100;
2279
- var REGEX_CACHE_MAX = 500;
2280
- var regexCache = /* @__PURE__ */ new Map();
2281
- function validateRegex(pattern) {
2282
- if (!pattern) return "Pattern is required";
2283
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
2284
- try {
2285
- new RegExp(pattern);
2286
- } catch (e) {
2287
- return `Invalid regex syntax: ${e.message}`;
2288
- }
2289
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
2290
- if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
2291
- return null;
2292
- }
2293
- function getCompiledRegex(pattern, flags = "") {
2294
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
2295
- const key = `${pattern}\0${flags}`;
2296
- if (regexCache.has(key)) {
2297
- const cached = regexCache.get(key);
2298
- regexCache.delete(key);
2299
- regexCache.set(key, cached);
2300
- return cached;
2301
- }
2302
- if (validateRegex(pattern) !== null) return null;
2303
- try {
2304
- const re = new RegExp(pattern, flags);
2305
- if (regexCache.size >= REGEX_CACHE_MAX) {
2306
- const oldest = regexCache.keys().next().value;
2307
- if (oldest) regexCache.delete(oldest);
2308
- }
2309
- regexCache.set(key, re);
2310
- return re;
2311
- } catch {
2312
- return null;
2313
- }
2314
- }
2315
- function matchesPattern(text, patterns) {
2316
- const p = Array.isArray(patterns) ? patterns : [patterns];
2317
- if (p.length === 0) return false;
2318
- const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
2319
- const target = text.toLowerCase();
2320
- const directMatch = isMatch(target);
2321
- if (directMatch) return true;
2322
- const withoutDotSlash = text.replace(/^\.\//, "");
2323
- return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2324
- }
2325
- var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2326
- function getNestedValue(obj, path13) {
2327
- if (!obj || typeof obj !== "object") return null;
2328
- const segments = path13.split(".");
2329
- for (const seg of segments) {
2330
- if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2331
- }
2332
- return segments.reduce((prev, curr) => prev?.[curr], obj);
2333
- }
2334
- function evaluateSmartConditions(args, rule) {
2335
- if (!rule.conditions || rule.conditions.length === 0) return true;
2336
- const mode = rule.conditionMode ?? "all";
2337
- const fieldCache = /* @__PURE__ */ new Map();
2338
- const resolveField = (field) => {
2339
- if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
2340
- const rawVal = getNestedValue(args, field);
2341
- const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
2342
- const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
2343
- const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
2344
- fieldCache.set(field, val);
2345
- return val;
2346
- };
2347
- const results = rule.conditions.map((cond) => {
2348
- const val = resolveField(cond.field);
2349
- switch (cond.op) {
2350
- case "exists":
2351
- return val !== null && val !== "";
2352
- case "notExists":
2353
- return val === null || val === "";
2354
- case "contains":
2355
- return val !== null && cond.value ? val.includes(cond.value) : false;
2356
- case "notContains":
2357
- return val !== null && cond.value ? !val.includes(cond.value) : true;
2358
- case "matches": {
2359
- if (val === null || !cond.value) return false;
2360
- const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2361
- if (!reM) return false;
2362
- return reM.test(val);
2363
- }
2364
- case "notMatches": {
2365
- if (!cond.value) return false;
2366
- if (val === null) return true;
2367
- const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2368
- if (!reN) return false;
2369
- return !reN.test(val);
2370
- }
2371
- case "matchesGlob":
2372
- return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
2373
- case "notMatchesGlob":
2374
- return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
2375
- default:
2376
- return false;
2377
- }
2378
- });
2379
- return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2380
- }
2381
2665
  function resolveCheck(v) {
2382
2666
  return v === "off" || v === "block" ? v : "review";
2383
2667
  }
2384
2668
  function resolveCheckTight(v) {
2385
2669
  return v === "block" ? "block" : "review";
2386
2670
  }
2387
- var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2388
- var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2389
- function detectInlineExec(command) {
2390
- const pipeFed = command.includes("|");
2391
- const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2392
- for (const rawSeg of segments) {
2393
- const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2394
- let i = 0;
2395
- while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2396
- if (i >= tokens.length) continue;
2397
- const base = tokens[i].split("/").pop() ?? tokens[i];
2398
- if (!INLINE_INTERP.test(base)) continue;
2399
- const args = tokens.slice(i + 1);
2400
- let hadRedirect = false;
2401
- const positionals = [];
2402
- for (let j = 0; j < args.length; j++) {
2403
- const a = args[j];
2404
- if (a === "-") return true;
2405
- if (a.startsWith("<")) {
2406
- hadRedirect = true;
2407
- if (a === "<" || a === "<<") j++;
2408
- continue;
2409
- }
2410
- if (a.startsWith("-")) {
2411
- if (/^-(c|e|eval)$/i.test(a)) return true;
2412
- continue;
2413
- }
2414
- positionals.push(a);
2415
- }
2416
- if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2417
- return true;
2418
- }
2419
- }
2420
- return false;
2421
- }
2422
2671
  var VERDICT_RANK = {
2423
2672
  allow: 0,
2424
2673
  review: 1,
@@ -2432,6 +2681,12 @@ function resolvePinned(matches) {
2432
2681
  (best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
2433
2682
  );
2434
2683
  }
2684
+ function strictestVerdict(candidates) {
2685
+ if (candidates.length === 0) return void 0;
2686
+ return candidates.reduce(
2687
+ (best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
2688
+ );
2689
+ }
2435
2690
  function tokenize2(toolName) {
2436
2691
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
2437
2692
  }
@@ -2443,6 +2698,11 @@ function extractShellCommand(toolName, args, toolInspection) {
2443
2698
  const value = getNestedValue(args, fieldPath);
2444
2699
  return typeof value === "string" ? value : null;
2445
2700
  }
2701
+ function inspectsShellCommand(toolName, toolInspection) {
2702
+ const patterns = Object.keys(toolInspection);
2703
+ const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
2704
+ return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
2705
+ }
2446
2706
  function isSqlTool(toolName, toolInspection) {
2447
2707
  const patterns = Object.keys(toolInspection);
2448
2708
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -2505,8 +2765,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2505
2765
  };
2506
2766
  }
2507
2767
  }
2508
- if (wouldBeIgnored) return { decision: "allow" };
2509
- const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2768
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2769
+ const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2770
+ const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2771
+ const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2510
2772
  if (bashCommand !== null) {
2511
2773
  const pipeVerdict = pipeChainVerdict(
2512
2774
  bashCommand,
@@ -2556,8 +2818,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2556
2818
  }
2557
2819
  if (config.policy.smartRules.length > 0) {
2558
2820
  const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2821
+ const astSuppressed = (rule) => {
2822
+ if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
2823
+ 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;
2824
+ if (knob === "off" && rule.pinned) return false;
2825
+ return true;
2826
+ };
2559
2827
  const matches = config.policy.smartRules.filter(
2560
- (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)
2828
+ (rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2561
2829
  );
2562
2830
  const matchedRule = resolvePinned(matches);
2563
2831
  if (matchedRule) {
@@ -2588,15 +2856,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2588
2856
  const analyzed = analyzeShellCommand(shellCommand);
2589
2857
  allTokens = analyzed.allTokens;
2590
2858
  pathTokens = analyzed.paths;
2591
- const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2592
- if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2593
- return {
2594
- decision: inlineAction === "block" ? "block" : "review",
2595
- blockedByLabel: "Node9 Standard (Inline Execution)",
2596
- ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2597
- tier: 3
2598
- };
2599
- }
2859
+ const candidates = [];
2600
2860
  const evalVerdict = detectDangerousShellExec(shellCommand);
2601
2861
  if (evalVerdict === "block") {
2602
2862
  return {
@@ -2607,24 +2867,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2607
2867
  tier: 3
2608
2868
  };
2609
2869
  }
2870
+ const ptVerdict = pipeChainVerdict(
2871
+ shellCommand,
2872
+ isTrustedHost2,
2873
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2874
+ );
2875
+ if (ptVerdict?.decision === "allow") return ptVerdict;
2876
+ if (ptVerdict) candidates.push(ptVerdict);
2877
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2878
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2879
+ candidates.push({
2880
+ decision: inlineAction === "block" ? "block" : "review",
2881
+ blockedByLabel: "Node9 Standard (Inline Execution)",
2882
+ ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2883
+ tier: 3
2884
+ });
2885
+ }
2610
2886
  if (evalVerdict === "review") {
2611
- return {
2612
- // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2613
- // block but can never turn this off (eval-remote above is Class A —
2614
- // no knob at all).
2887
+ candidates.push({
2615
2888
  decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2616
2889
  blockedByLabel: "Node9: Eval Dynamic Content",
2617
2890
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2618
2891
  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.",
2619
2892
  tier: 3
2620
- };
2893
+ });
2621
2894
  }
2622
- const ptVerdict = pipeChainVerdict(
2623
- shellCommand,
2624
- isTrustedHost2,
2625
- resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2626
- );
2627
- if (ptVerdict) return ptVerdict;
2895
+ const builtin = strictestVerdict(candidates);
2896
+ if (builtin) return builtin;
2628
2897
  if (config.policy.egress?.enabled) {
2629
2898
  const dests = extractShellDestinations(shellCommand);
2630
2899
  if (dests.length > 0) {
@@ -3675,28 +3944,41 @@ function readShieldOverrides() {
3675
3944
  var MODE_ORDER = ["observe", "audit", "standard", "strict"];
3676
3945
  var EGRESS_MODE_ORDER = ["off", "review", "block"];
3677
3946
  function rankIn(order, value) {
3678
- return order.indexOf(value);
3947
+ return value === void 0 ? -1 : order.indexOf(value);
3679
3948
  }
3680
- function resolveByOrder(order, local, cloud, locked) {
3681
- if (rankIn(order, cloud) === -1) return local;
3682
- if (locked) return cloud;
3949
+ function floorValue(order, local, cloud, opts = {}) {
3950
+ if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
3951
+ if (opts.locked) return cloud;
3952
+ const localSet = opts.localWasSet ?? local !== void 0;
3953
+ if (!localSet || rankIn(order, local) === -1) return cloud;
3683
3954
  return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
3684
3955
  }
3956
+ function strictestOf(order, ...values) {
3957
+ let best;
3958
+ for (const v of values) {
3959
+ if (rankIn(order, v) === -1) continue;
3960
+ if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
3961
+ }
3962
+ return best;
3963
+ }
3964
+ function resolveByOrder(order, local, cloud, locked) {
3965
+ return floorValue(order, local, cloud, { locked }) ?? local;
3966
+ }
3685
3967
  function resolveManagedMode(local, cloud, locked) {
3686
3968
  return resolveByOrder(MODE_ORDER, local, cloud, locked);
3687
3969
  }
3688
- function applyManagedEgress(local, managed, locked) {
3970
+ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
3689
3971
  const next = { ...local };
3690
3972
  if (typeof managed.enabled === "boolean") {
3691
3973
  next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
3692
3974
  }
3693
3975
  if (typeof managed.mode === "string") {
3694
- next.mode = resolveByOrder(
3695
- EGRESS_MODE_ORDER,
3696
- local.mode,
3697
- managed.mode,
3698
- locked.includes("egressMode")
3699
- );
3976
+ next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
3977
+ locked: locked.includes("egressMode"),
3978
+ // The default 'review' is seeded into egress before any merge, so absence
3979
+ // is invisible from `local.mode` alone — the caller tracks it for us.
3980
+ localWasSet: localModeUserSet
3981
+ }) ?? local.mode;
3700
3982
  }
3701
3983
  if (Array.isArray(managed.allow) && managed.allow.length > 0) {
3702
3984
  next.allow = [...managed.allow];
@@ -3716,6 +3998,9 @@ function applyManagedDlp(local, managed, locked) {
3716
3998
  if (typeof managed.enabled === "boolean") {
3717
3999
  next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
3718
4000
  }
4001
+ if (managed.enabled === true) {
4002
+ next.scanIgnoredTools = true;
4003
+ }
3719
4004
  if (typeof managed.pii === "string") {
3720
4005
  next.pii = resolveByOrder(
3721
4006
  DLP_PII_ORDER,
@@ -3749,13 +4034,9 @@ function applyManagedCommandChecks(local, managed, locked) {
3749
4034
  const m = managed[key];
3750
4035
  if (typeof m !== "string") continue;
3751
4036
  const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3752
- const localValue = local[key];
3753
- const resolved = localValue === void 0 ? m : resolveByOrder(
3754
- COMMAND_CHECK_ORDER,
3755
- localValue,
3756
- m,
3757
- locked.includes(lockKey)
3758
- );
4037
+ const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
4038
+ locked: locked.includes(lockKey)
4039
+ });
3759
4040
  if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3760
4041
  next[key] = resolved;
3761
4042
  }
@@ -3781,11 +4062,18 @@ function slug(s) {
3781
4062
  var B = "[\\s/\\\\]";
3782
4063
  var SEP = "[/\\\\]";
3783
4064
  function pathToRegexFragment(rawPath) {
3784
- const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
4065
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
3785
4066
  const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
3786
4067
  if (segments.length === 0) return "";
3787
4068
  return `(^|${B})${segments.join(SEP)}(${B}|$)`;
3788
4069
  }
4070
+ function pathMatchesFragment(candidate, rawPath) {
4071
+ const value = pathToRegexFragment(rawPath);
4072
+ if (!value || !candidate) return false;
4073
+ const re = getCompiledRegex(value);
4074
+ if (!re) return false;
4075
+ return re.test(candidate);
4076
+ }
3789
4077
  function pathRules(rawPath, verdict, reason) {
3790
4078
  const value = pathToRegexFragment(rawPath);
3791
4079
  if (!value) return [];
@@ -3799,12 +4087,28 @@ function pathRules(rawPath, verdict, reason) {
3799
4087
  verdict,
3800
4088
  reason: why
3801
4089
  },
4090
+ // Keep the historical `-anytool` name for the file_path rule: the
4091
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
3802
4092
  {
3803
4093
  name: `${verdict}-path-${s}-anytool`,
3804
4094
  tool: "*",
3805
4095
  conditions: [{ field: "file_path", op: "matches", value }],
3806
4096
  verdict,
3807
4097
  reason: why
4098
+ },
4099
+ {
4100
+ name: `${verdict}-path-${s}-anytool-path`,
4101
+ tool: "*",
4102
+ conditions: [{ field: "path", op: "matches", value }],
4103
+ verdict,
4104
+ reason: why
4105
+ },
4106
+ {
4107
+ name: `${verdict}-path-${s}-anytool-pattern`,
4108
+ tool: "*",
4109
+ conditions: [{ field: "pattern", op: "matches", value }],
4110
+ verdict,
4111
+ reason: why
3808
4112
  }
3809
4113
  ];
3810
4114
  }
@@ -3877,8 +4181,14 @@ var DEFAULT_CONFIG = {
3877
4181
  settings: {
3878
4182
  mode: "standard",
3879
4183
  autoStartDaemon: true,
3880
- enableUndo: true,
3881
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
4184
+ // OFF by default. The snapshot store is a per-project bare git repo with
4185
+ // no size ceiling, and eviction drops the index row without deleting the
4186
+ // objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
4187
+ // from interrupted `git gc`) and filled the disk. A security tool must not
4188
+ // be what fills a customer's disk. Re-enable per install with
4189
+ // `{"settings":{"enableUndo":true}}`; the default flips back when the
4190
+ // bounded copy-store lands (doc/undo-v2-copy-store-design.md).
4191
+ enableUndo: false,
3882
4192
  enableHookLogDebug: true,
3883
4193
  approvalTimeoutMs: 12e4,
3884
4194
  // 120-second auto-deny timeout
@@ -4077,10 +4387,13 @@ var DEFAULT_CONFIG = {
4077
4387
  skillPinning: { enabled: false, mode: "warn", roots: [] },
4078
4388
  trustedHosts: [],
4079
4389
  trustedHostsManaged: false,
4080
- appPermissions: {}
4390
+ appPermissions: {},
4391
+ managedJailPaths: []
4081
4392
  },
4082
4393
  environments: {}
4083
4394
  };
4395
+ var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
4396
+ var VERDICT_ORDER = ["allow", "review", "block"];
4084
4397
  var ADVISORY_SMART_RULES = [
4085
4398
  // ── rm safety ─────────────────────────────────────────────────────────────
4086
4399
  // tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
@@ -4092,12 +4405,7 @@ var ADVISORY_SMART_RULES = [
4092
4405
  conditionMode: "all",
4093
4406
  conditions: [
4094
4407
  { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
4095
- {
4096
- field: "command",
4097
- op: "matches",
4098
- // Matches known-safe build artifact paths in the command.
4099
- value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
4100
- }
4408
+ { field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
4101
4409
  ],
4102
4410
  verdict: "allow",
4103
4411
  reason: "Deleting a known-safe build artifact path"
@@ -4275,10 +4583,15 @@ function getConfig(cwd) {
4275
4583
  // here. A managed list fills this below and flips trustedHostsManaged.
4276
4584
  trustedHosts: [],
4277
4585
  trustedHostsManaged: false,
4278
- appPermissions: {}
4586
+ appPermissions: {},
4587
+ managedJailPaths: []
4279
4588
  };
4280
4589
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
4281
- const applyLayer = (source) => {
4590
+ const rank = (v) => {
4591
+ const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
4592
+ return i === -1 ? 1 : i;
4593
+ };
4594
+ const applyLayer = (source, isProject = false) => {
4282
4595
  if (!source) return;
4283
4596
  const s = source.settings || {};
4284
4597
  const p = source.policy || {};
@@ -4338,7 +4651,9 @@ function getConfig(cwd) {
4338
4651
  };
4339
4652
  for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4340
4653
  const v = src[k];
4341
- if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4654
+ if (v !== "off" && v !== "review" && v !== "block") continue;
4655
+ if (isProject && rank(v) < rank(cc2[k])) continue;
4656
+ cc2[k] = v;
4342
4657
  }
4343
4658
  for (const k of ["evalDynamic", "pipeChainHigh"]) {
4344
4659
  const v = src[k];
@@ -4348,11 +4663,22 @@ function getConfig(cwd) {
4348
4663
  }
4349
4664
  if (p.egress) {
4350
4665
  const e = p.egress;
4351
- if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
4352
- if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
4353
- if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
4666
+ if (e.enabled !== void 0 && !(isProject && e.enabled === false))
4667
+ mergedPolicy.egress.enabled = e.enabled;
4668
+ if (e.mode !== void 0) {
4669
+ const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
4670
+ if (!weaker) {
4671
+ mergedPolicy.egress.mode = e.mode;
4672
+ egressModeUserSet = true;
4673
+ }
4674
+ }
4675
+ if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
4676
+ mergedPolicy.egress.allow.push(...e.allow);
4677
+ }
4678
+ if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
4354
4679
  if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
4355
- if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
4680
+ if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
4681
+ mergedPolicy.egress.allowPrivate = e.allowPrivate;
4356
4682
  }
4357
4683
  if (p.loopDetection) {
4358
4684
  const ld = p.loopDetection;
@@ -4394,11 +4720,21 @@ function getConfig(cwd) {
4394
4720
  }
4395
4721
  }
4396
4722
  };
4723
+ let egressModeUserSet = false;
4724
+ let egressAllowUserSet = false;
4397
4725
  applyLayer(globalConfig);
4398
- applyLayer(projectConfig);
4726
+ applyLayer(
4727
+ projectConfig,
4728
+ /* isProject */
4729
+ true
4730
+ );
4399
4731
  let cloudManagedShields = [];
4732
+ const managedCommandCheckKeys = /* @__PURE__ */ new Set();
4733
+ const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
4400
4734
  let modeCloudControlled = false;
4401
4735
  let modeCloudStaged = false;
4736
+ let cloudMandatesEnforcement = false;
4737
+ let cloudMandatesAppPerm = false;
4402
4738
  {
4403
4739
  const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
4404
4740
  try {
@@ -4433,7 +4769,8 @@ function getConfig(cwd) {
4433
4769
  deny: hosts(mc.egress.deny),
4434
4770
  allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
4435
4771
  },
4436
- locked
4772
+ locked,
4773
+ egressModeUserSet
4437
4774
  );
4438
4775
  }
4439
4776
  if (mc.dlp && typeof mc.dlp === "object") {
@@ -4453,6 +4790,12 @@ function getConfig(cwd) {
4453
4790
  mc.commandChecks,
4454
4791
  locked
4455
4792
  );
4793
+ for (const [key, val] of Object.entries(mc.commandChecks)) {
4794
+ if (typeof val !== "string") continue;
4795
+ managedCommandCheckKeys.add(key);
4796
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4797
+ if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
4798
+ }
4456
4799
  }
4457
4800
  if (mc.approvers && typeof mc.approvers === "object") {
4458
4801
  const bool = (v) => typeof v === "boolean" ? v : void 0;
@@ -4499,12 +4842,13 @@ function getConfig(cwd) {
4499
4842
  }
4500
4843
  if (Array.isArray(mc.jailPaths)) {
4501
4844
  for (const jp of mc.jailPaths) {
4502
- const path13 = typeof jp?.path === "string" ? jp.path.trim() : "";
4503
- if (!path13) continue;
4845
+ const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
4846
+ if (!path14) continue;
4504
4847
  const verdict = jp?.verdict === "review" ? "review" : "block";
4505
- for (const r of pathRules(path13, verdict, "org-managed jail")) {
4848
+ for (const r of pathRules(path14, verdict, "org-managed jail")) {
4506
4849
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
4507
4850
  }
4851
+ mergedPolicy.managedJailPaths.push({ path: path14, verdict });
4508
4852
  }
4509
4853
  }
4510
4854
  if (Array.isArray(mc.trustedHosts)) {
@@ -4522,7 +4866,14 @@ function getConfig(cwd) {
4522
4866
  if (Object.keys(m).length) coerced[srv] = m;
4523
4867
  }
4524
4868
  mergedPolicy.appPermissions = coerced;
4869
+ cloudMandatesAppPerm = Object.values(coerced).some(
4870
+ (tools) => Object.values(tools).some((d) => d === "block" || d === "review")
4871
+ );
4525
4872
  }
4873
+ const on = (v) => !!v && typeof v === "object" && v.enabled === true;
4874
+ 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(
4875
+ (v) => typeof v === "string" && v !== "off"
4876
+ );
4526
4877
  }
4527
4878
  if (raw.panicMode === true) {
4528
4879
  mergedSettings.panicMode = true;
@@ -4564,25 +4915,45 @@ function getConfig(cwd) {
4564
4915
  }
4565
4916
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4566
4917
  const cc = mergedPolicy.commandChecks ?? {};
4567
- const advisoryKnob = (name) => {
4568
- if (name === "review-rm") return cc.rmAdvisory;
4569
- if (name?.endsWith("-sql")) return cc.sqlDdl;
4918
+ const advisoryKnobKey = (name) => {
4919
+ if (name === "review-rm") return "rmAdvisory";
4920
+ if (name?.endsWith("-sql")) return "sqlDdl";
4570
4921
  return void 0;
4571
4922
  };
4572
4923
  for (const rule of ADVISORY_SMART_RULES) {
4573
- if (existingAdvisoryNames.has(rule.name)) continue;
4574
- const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4924
+ const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
4925
+ const knob = knobKey ? cc[knobKey] : void 0;
4575
4926
  if (knob === "off") continue;
4576
- mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4927
+ const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
4928
+ const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
4929
+ const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
4930
+ const knobVerdict = knob === "block" ? "block" : rule.verdict;
4931
+ if (!managed) {
4932
+ if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
4933
+ continue;
4934
+ }
4935
+ const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
4936
+ if (twin) {
4937
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4938
+ }
4939
+ const injected = { ...rule, verdict: effective, pinned: true };
4940
+ if (rule.name === "review-rm" && effective !== "block") {
4941
+ injected.conditions = [
4942
+ ...rule.conditions ?? [],
4943
+ { field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
4944
+ ];
4945
+ injected.conditionMode = "all";
4946
+ }
4947
+ mergedPolicy.smartRules.push(injected);
4577
4948
  }
4578
4949
  const envMode = process.env.NODE9_MODE;
4579
4950
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4580
4951
  mergedSettings.mode = envMode;
4581
4952
  }
4582
- if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4953
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4583
4954
  mergedSettings.mode = "standard";
4584
4955
  }
4585
- const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4956
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
4586
4957
  if (modeCloudControlled && mergedSettings.mode === "strict") {
4587
4958
  for (const name of Object.keys(mergedEnvironments)) {
4588
4959
  if (mergedEnvironments[name]?.requireApproval === false) {
@@ -4783,14 +5154,14 @@ function checkProvenance(cmd, cwd) {
4783
5154
  }
4784
5155
 
4785
5156
  // src/policy/index.ts
4786
- async function evaluatePolicy2(toolName, args, agent, cwd) {
5157
+ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
4787
5158
  const config = getConfig();
4788
5159
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
4789
5160
  return evaluatePolicy(
4790
5161
  config,
4791
5162
  toolName,
4792
5163
  args,
4793
- { agent, cwd, activeEnvironment },
5164
+ { agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
4794
5165
  {
4795
5166
  checkProvenance,
4796
5167
  // Managed → match against the org list (frozen with the rest of managed
@@ -5642,6 +6013,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
5642
6013
  }
5643
6014
  }
5644
6015
 
6016
+ // src/shields/jail.ts
6017
+ var import_fs11 = __toESM(require("fs"));
6018
+ var import_os10 = __toESM(require("os"));
6019
+ var import_path13 = __toESM(require("path"));
6020
+ var USER_JAIL_SHIELD = "user-jail";
6021
+ function jailStorePath() {
6022
+ return import_path13.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
6023
+ }
6024
+ function readJailPaths() {
6025
+ let text;
6026
+ try {
6027
+ text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
6028
+ } catch (err) {
6029
+ if (err.code === "ENOENT") return [];
6030
+ throw err;
6031
+ }
6032
+ let parsed;
6033
+ try {
6034
+ parsed = JSON.parse(text);
6035
+ } catch {
6036
+ throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
6037
+ }
6038
+ if (!Array.isArray(parsed.paths)) return [];
6039
+ return parsed.paths.filter(
6040
+ (p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
6041
+ );
6042
+ }
6043
+ function findJailedPath(candidate) {
6044
+ return findJailedPathIn(candidate, readJailPaths());
6045
+ }
6046
+ function findJailedPathIn(candidate, paths) {
6047
+ if (!candidate) return null;
6048
+ for (const entry of paths) {
6049
+ if (pathMatchesFragment(candidate, entry.path)) return entry;
6050
+ }
6051
+ return null;
6052
+ }
6053
+
5645
6054
  // src/auth/orchestrator.ts
5646
6055
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
5647
6056
  "write",
@@ -6117,12 +6526,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
6117
6526
  } else if (!taintWarning && !appPermReview) {
6118
6527
  const toolLower = toolName.toLowerCase();
6119
6528
  const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
6120
- if (isFileTool && readActiveShields().includes("project-jail")) {
6529
+ const activeShields = isFileTool ? readActiveShields() : [];
6530
+ const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
6531
+ if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
6121
6532
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
6122
- const filePath = String(
6123
- argsObj.file_path ?? argsObj.path ?? argsObj.pattern ?? argsObj.filename ?? ""
6124
- );
6125
- if (filePath && scanFilePath(filePath)) {
6533
+ const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
6534
+ const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
6535
+ const sensitiveHit = candidates.some((c) => scanFilePath(c));
6536
+ if (jailHit || sensitiveHit) {
6537
+ const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
6538
+ skipIgnoredFastPath: true
6539
+ });
6540
+ if (policyResult.decision === "block") {
6541
+ if (!isManual)
6542
+ appendLocalAudit(
6543
+ toolName,
6544
+ args,
6545
+ "deny",
6546
+ "smart-rule-block",
6547
+ { ...meta, ruleName: policyResult.ruleName },
6548
+ hashAuditArgs
6549
+ );
6550
+ return {
6551
+ approved: false,
6552
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
6553
+ blockedBy: "local-config",
6554
+ blockedByLabel: policyResult.blockedByLabel,
6555
+ ruleHit: policyResult.ruleName
6556
+ };
6557
+ }
6126
6558
  } else {
6127
6559
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
6128
6560
  return { approved: true };
@@ -6185,7 +6617,12 @@ ${appPermReview}`
6185
6617
  }
6186
6618
  let cloudRequestId = null;
6187
6619
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
6188
- const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || void 0;
6620
+ const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
6621
+ // a client-side heuristic the SaaS has no rule for, so without forceReview
6622
+ // its checkRule answers "no org rule matched" → {approved:true}, which is
6623
+ // not an approval of an exfiltration risk. Measured against the live BE:
6624
+ // {approved:true} without this flag, {pending:true} with it.
6625
+ !!taintWarning || void 0;
6189
6626
  if (cloudEnforced) {
6190
6627
  try {
6191
6628
  const initResult = await initNode9SaaS(
@@ -6198,10 +6635,10 @@ ${appPermReview}`
6198
6635
  forceReview
6199
6636
  );
6200
6637
  if (!initResult.pending) {
6201
- if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6638
+ if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6202
6639
  return { approved: true, checkedBy: "cloud" };
6203
6640
  }
6204
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6641
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6205
6642
  return {
6206
6643
  approved: !!initResult.approved,
6207
6644
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),