@node9/proxy 1.67.0 → 1.67.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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) {
@@ -1411,6 +1543,208 @@ function chmodHasOpenPermMode(command) {
1411
1543
  }
1412
1544
  return found;
1413
1545
  }
1546
+ function isShellShapedTool(toolName, toolInspection) {
1547
+ if (isBashTool(toolName)) return true;
1548
+ if (!toolInspection) return false;
1549
+ const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
1550
+ return pattern !== void 0 && toolInspection[pattern] === "command";
1551
+ }
1552
+ function toolMatchesRule(toolName, ruleTool, toolInspection) {
1553
+ if (!ruleTool) return true;
1554
+ if (matchesPattern(toolName, ruleTool)) return true;
1555
+ return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
1556
+ }
1557
+ 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;
1558
+ var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
1559
+ "uv",
1560
+ "uvx",
1561
+ "poetry",
1562
+ "pipenv",
1563
+ "pdm",
1564
+ "rye",
1565
+ "hatch",
1566
+ "conda",
1567
+ "mamba",
1568
+ "micromamba",
1569
+ "npx",
1570
+ "pnpm",
1571
+ "yarn",
1572
+ "bunx",
1573
+ "watch",
1574
+ "strace",
1575
+ "ltrace",
1576
+ "chroot",
1577
+ "unshare",
1578
+ "runuser"
1579
+ ]);
1580
+ function isInlineCodeFlag(interp, w) {
1581
+ const lw = w.toLowerCase();
1582
+ if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
1583
+ if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
1584
+ if (!w.startsWith("-") || w.startsWith("--")) return false;
1585
+ const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1586
+ const body = lw.slice(1);
1587
+ const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
1588
+ const cut = body.search(cutAt);
1589
+ const bundle = cut >= 0 ? body.slice(0, cut) : body;
1590
+ return [...codeLetters].some((l) => bundle.includes(l));
1591
+ }
1592
+ var _redirStdinOps = null;
1593
+ function redirStdinOps() {
1594
+ if (_redirStdinOps) return _redirStdinOps;
1595
+ _redirStdinOps = new Set(
1596
+ [
1597
+ deriveRedirOp("cat <<X\nX"),
1598
+ deriveRedirOp("cat <<-X\nX"),
1599
+ deriveRedirOp("cat < f"),
1600
+ deriveRedirOp("cat <<< x")
1601
+ ].filter((op) => op >= 0)
1602
+ );
1603
+ return _redirStdinOps;
1604
+ }
1605
+ function deriveBinaryOp(sample) {
1606
+ try {
1607
+ const f = sharedParser.Parse(sample, "cmd");
1608
+ let op = -1;
1609
+ syntax.Walk(f, (node) => {
1610
+ const n = node;
1611
+ if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
1612
+ return true;
1613
+ });
1614
+ return op;
1615
+ } catch {
1616
+ return -1;
1617
+ }
1618
+ }
1619
+ var _listOps = null;
1620
+ function listOps() {
1621
+ if (_listOps) return _listOps;
1622
+ _listOps = new Set(
1623
+ [deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
1624
+ );
1625
+ return _listOps;
1626
+ }
1627
+ var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
1628
+ var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
1629
+ function unwrapCommandHead(words) {
1630
+ let i = 0;
1631
+ while (i < words.length) {
1632
+ const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
1633
+ if (head === "find") {
1634
+ const x = words.findIndex(
1635
+ (w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
1636
+ );
1637
+ if (x < 0) break;
1638
+ i = x + 1;
1639
+ continue;
1640
+ }
1641
+ if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
1642
+ i++;
1643
+ let targetConsumed = false;
1644
+ while (i < words.length) {
1645
+ const t = words[i];
1646
+ if (t === null) {
1647
+ i++;
1648
+ continue;
1649
+ }
1650
+ const lt = t.toLowerCase();
1651
+ if (/^[A-Za-z_]\w*=/.test(t)) {
1652
+ i++;
1653
+ continue;
1654
+ }
1655
+ if (t.startsWith("-")) {
1656
+ i++;
1657
+ const nxt = words[i];
1658
+ if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
1659
+ i++;
1660
+ continue;
1661
+ }
1662
+ if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
1663
+ i++;
1664
+ continue;
1665
+ }
1666
+ if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
1667
+ targetConsumed = true;
1668
+ i++;
1669
+ continue;
1670
+ }
1671
+ break;
1672
+ }
1673
+ }
1674
+ return i;
1675
+ }
1676
+ function inlineExecStmt(stmt, pipeFed) {
1677
+ const cmd = stmt?.Cmd;
1678
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1679
+ const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1680
+ if (words.length === 0) return false;
1681
+ const headIdx = unwrapCommandHead(words);
1682
+ const rawHead = words[headIdx];
1683
+ if (rawHead == null) return false;
1684
+ const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1685
+ if (!INLINE_INTERPRETER.test(interp)) return false;
1686
+ let args = words.slice(headIdx + 1);
1687
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1688
+ if (INTERP_LEADING_TARGET.has(interp)) {
1689
+ const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1690
+ args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1691
+ }
1692
+ let positionals = 0;
1693
+ let selectedProgram = false;
1694
+ for (const a of args) {
1695
+ if (a == null) {
1696
+ positionals++;
1697
+ selectedProgram = true;
1698
+ continue;
1699
+ }
1700
+ if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
1701
+ if (a === "-m") {
1702
+ selectedProgram = true;
1703
+ continue;
1704
+ }
1705
+ if (a === "-" && !selectedProgram) return true;
1706
+ if (!a.startsWith("-")) {
1707
+ positionals++;
1708
+ selectedProgram = true;
1709
+ }
1710
+ }
1711
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1712
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1713
+ if (positionals === 0 && (stdinFed || pipeFed)) {
1714
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1715
+ }
1716
+ return false;
1717
+ }
1718
+ function detectInlineExec(command) {
1719
+ const f = parseShared(command);
1720
+ if (f === PARSE_FAIL) {
1721
+ 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(
1722
+ command
1723
+ );
1724
+ }
1725
+ let found = false;
1726
+ try {
1727
+ syntax.Walk(f, (node) => {
1728
+ if (!node || found) return false;
1729
+ const n = node;
1730
+ const t = syntax.NodeType(n);
1731
+ if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1732
+ if (inlineExecStmt(n.Y, true)) {
1733
+ found = true;
1734
+ return false;
1735
+ }
1736
+ }
1737
+ if (t === "Stmt" && inlineExecStmt(n, false)) {
1738
+ found = true;
1739
+ return false;
1740
+ }
1741
+ return true;
1742
+ });
1743
+ } catch {
1744
+ return found;
1745
+ }
1746
+ return found;
1747
+ }
1414
1748
  function analyzeChmod777(command) {
1415
1749
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
1416
1750
  if (!chmodHasOpenPermMode(command)) return null;
@@ -2275,150 +2609,12 @@ function extractAllSshHosts(tokens) {
2275
2609
  }
2276
2610
  return [...hosts].filter(Boolean);
2277
2611
  }
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
2612
  function resolveCheck(v) {
2382
2613
  return v === "off" || v === "block" ? v : "review";
2383
2614
  }
2384
2615
  function resolveCheckTight(v) {
2385
2616
  return v === "block" ? "block" : "review";
2386
2617
  }
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
2618
  var VERDICT_RANK = {
2423
2619
  allow: 0,
2424
2620
  review: 1,
@@ -2432,6 +2628,12 @@ function resolvePinned(matches) {
2432
2628
  (best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
2433
2629
  );
2434
2630
  }
2631
+ function strictestVerdict(candidates) {
2632
+ if (candidates.length === 0) return void 0;
2633
+ return candidates.reduce(
2634
+ (best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
2635
+ );
2636
+ }
2435
2637
  function tokenize2(toolName) {
2436
2638
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
2437
2639
  }
@@ -2443,6 +2645,11 @@ function extractShellCommand(toolName, args, toolInspection) {
2443
2645
  const value = getNestedValue(args, fieldPath);
2444
2646
  return typeof value === "string" ? value : null;
2445
2647
  }
2648
+ function inspectsShellCommand(toolName, toolInspection) {
2649
+ const patterns = Object.keys(toolInspection);
2650
+ const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
2651
+ return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
2652
+ }
2446
2653
  function isSqlTool(toolName, toolInspection) {
2447
2654
  const patterns = Object.keys(toolInspection);
2448
2655
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -2505,8 +2712,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2505
2712
  };
2506
2713
  }
2507
2714
  }
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;
2715
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2716
+ const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2717
+ const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2718
+ const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2510
2719
  if (bashCommand !== null) {
2511
2720
  const pipeVerdict = pipeChainVerdict(
2512
2721
  bashCommand,
@@ -2556,8 +2765,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2556
2765
  }
2557
2766
  if (config.policy.smartRules.length > 0) {
2558
2767
  const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2768
+ const astSuppressed = (rule) => {
2769
+ if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
2770
+ 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;
2771
+ if (knob === "off" && rule.pinned) return false;
2772
+ return true;
2773
+ };
2559
2774
  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)
2775
+ (rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2561
2776
  );
2562
2777
  const matchedRule = resolvePinned(matches);
2563
2778
  if (matchedRule) {
@@ -2588,15 +2803,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2588
2803
  const analyzed = analyzeShellCommand(shellCommand);
2589
2804
  allTokens = analyzed.allTokens;
2590
2805
  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
- }
2806
+ const candidates = [];
2600
2807
  const evalVerdict = detectDangerousShellExec(shellCommand);
2601
2808
  if (evalVerdict === "block") {
2602
2809
  return {
@@ -2607,24 +2814,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2607
2814
  tier: 3
2608
2815
  };
2609
2816
  }
2817
+ const ptVerdict = pipeChainVerdict(
2818
+ shellCommand,
2819
+ isTrustedHost2,
2820
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2821
+ );
2822
+ if (ptVerdict?.decision === "allow") return ptVerdict;
2823
+ if (ptVerdict) candidates.push(ptVerdict);
2824
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2825
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2826
+ candidates.push({
2827
+ decision: inlineAction === "block" ? "block" : "review",
2828
+ blockedByLabel: "Node9 Standard (Inline Execution)",
2829
+ ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2830
+ tier: 3
2831
+ });
2832
+ }
2610
2833
  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).
2834
+ candidates.push({
2615
2835
  decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2616
2836
  blockedByLabel: "Node9: Eval Dynamic Content",
2617
2837
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2618
2838
  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
2839
  tier: 3
2620
- };
2840
+ });
2621
2841
  }
2622
- const ptVerdict = pipeChainVerdict(
2623
- shellCommand,
2624
- isTrustedHost2,
2625
- resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2626
- );
2627
- if (ptVerdict) return ptVerdict;
2842
+ const builtin = strictestVerdict(candidates);
2843
+ if (builtin) return builtin;
2628
2844
  if (config.policy.egress?.enabled) {
2629
2845
  const dests = extractShellDestinations(shellCommand);
2630
2846
  if (dests.length > 0) {
@@ -3675,28 +3891,41 @@ function readShieldOverrides() {
3675
3891
  var MODE_ORDER = ["observe", "audit", "standard", "strict"];
3676
3892
  var EGRESS_MODE_ORDER = ["off", "review", "block"];
3677
3893
  function rankIn(order, value) {
3678
- return order.indexOf(value);
3894
+ return value === void 0 ? -1 : order.indexOf(value);
3679
3895
  }
3680
- function resolveByOrder(order, local, cloud, locked) {
3681
- if (rankIn(order, cloud) === -1) return local;
3682
- if (locked) return cloud;
3896
+ function floorValue(order, local, cloud, opts = {}) {
3897
+ if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
3898
+ if (opts.locked) return cloud;
3899
+ const localSet = opts.localWasSet ?? local !== void 0;
3900
+ if (!localSet || rankIn(order, local) === -1) return cloud;
3683
3901
  return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
3684
3902
  }
3903
+ function strictestOf(order, ...values) {
3904
+ let best;
3905
+ for (const v of values) {
3906
+ if (rankIn(order, v) === -1) continue;
3907
+ if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
3908
+ }
3909
+ return best;
3910
+ }
3911
+ function resolveByOrder(order, local, cloud, locked) {
3912
+ return floorValue(order, local, cloud, { locked }) ?? local;
3913
+ }
3685
3914
  function resolveManagedMode(local, cloud, locked) {
3686
3915
  return resolveByOrder(MODE_ORDER, local, cloud, locked);
3687
3916
  }
3688
- function applyManagedEgress(local, managed, locked) {
3917
+ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
3689
3918
  const next = { ...local };
3690
3919
  if (typeof managed.enabled === "boolean") {
3691
3920
  next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
3692
3921
  }
3693
3922
  if (typeof managed.mode === "string") {
3694
- next.mode = resolveByOrder(
3695
- EGRESS_MODE_ORDER,
3696
- local.mode,
3697
- managed.mode,
3698
- locked.includes("egressMode")
3699
- );
3923
+ next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
3924
+ locked: locked.includes("egressMode"),
3925
+ // The default 'review' is seeded into egress before any merge, so absence
3926
+ // is invisible from `local.mode` alone — the caller tracks it for us.
3927
+ localWasSet: localModeUserSet
3928
+ }) ?? local.mode;
3700
3929
  }
3701
3930
  if (Array.isArray(managed.allow) && managed.allow.length > 0) {
3702
3931
  next.allow = [...managed.allow];
@@ -3716,6 +3945,9 @@ function applyManagedDlp(local, managed, locked) {
3716
3945
  if (typeof managed.enabled === "boolean") {
3717
3946
  next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
3718
3947
  }
3948
+ if (managed.enabled === true) {
3949
+ next.scanIgnoredTools = true;
3950
+ }
3719
3951
  if (typeof managed.pii === "string") {
3720
3952
  next.pii = resolveByOrder(
3721
3953
  DLP_PII_ORDER,
@@ -3749,12 +3981,9 @@ function applyManagedCommandChecks(local, managed, locked) {
3749
3981
  const m = managed[key];
3750
3982
  if (typeof m !== "string") continue;
3751
3983
  const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3752
- const resolved = resolveByOrder(
3753
- COMMAND_CHECK_ORDER,
3754
- local[key] ?? "review",
3755
- m,
3756
- locked.includes(lockKey)
3757
- );
3984
+ const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
3985
+ locked: locked.includes(lockKey)
3986
+ });
3758
3987
  if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3759
3988
  next[key] = resolved;
3760
3989
  }
@@ -3780,11 +4009,18 @@ function slug(s) {
3780
4009
  var B = "[\\s/\\\\]";
3781
4010
  var SEP = "[/\\\\]";
3782
4011
  function pathToRegexFragment(rawPath) {
3783
- const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
4012
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
3784
4013
  const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
3785
4014
  if (segments.length === 0) return "";
3786
4015
  return `(^|${B})${segments.join(SEP)}(${B}|$)`;
3787
4016
  }
4017
+ function pathMatchesFragment(candidate, rawPath) {
4018
+ const value = pathToRegexFragment(rawPath);
4019
+ if (!value || !candidate) return false;
4020
+ const re = getCompiledRegex(value);
4021
+ if (!re) return false;
4022
+ return re.test(candidate);
4023
+ }
3788
4024
  function pathRules(rawPath, verdict, reason) {
3789
4025
  const value = pathToRegexFragment(rawPath);
3790
4026
  if (!value) return [];
@@ -3798,12 +4034,28 @@ function pathRules(rawPath, verdict, reason) {
3798
4034
  verdict,
3799
4035
  reason: why
3800
4036
  },
4037
+ // Keep the historical `-anytool` name for the file_path rule: the
4038
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
3801
4039
  {
3802
4040
  name: `${verdict}-path-${s}-anytool`,
3803
4041
  tool: "*",
3804
4042
  conditions: [{ field: "file_path", op: "matches", value }],
3805
4043
  verdict,
3806
4044
  reason: why
4045
+ },
4046
+ {
4047
+ name: `${verdict}-path-${s}-anytool-path`,
4048
+ tool: "*",
4049
+ conditions: [{ field: "path", op: "matches", value }],
4050
+ verdict,
4051
+ reason: why
4052
+ },
4053
+ {
4054
+ name: `${verdict}-path-${s}-anytool-pattern`,
4055
+ tool: "*",
4056
+ conditions: [{ field: "pattern", op: "matches", value }],
4057
+ verdict,
4058
+ reason: why
3807
4059
  }
3808
4060
  ];
3809
4061
  }
@@ -3876,8 +4128,14 @@ var DEFAULT_CONFIG = {
3876
4128
  settings: {
3877
4129
  mode: "standard",
3878
4130
  autoStartDaemon: true,
3879
- enableUndo: true,
3880
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
4131
+ // OFF by default. The snapshot store is a per-project bare git repo with
4132
+ // no size ceiling, and eviction drops the index row without deleting the
4133
+ // objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
4134
+ // from interrupted `git gc`) and filled the disk. A security tool must not
4135
+ // be what fills a customer's disk. Re-enable per install with
4136
+ // `{"settings":{"enableUndo":true}}`; the default flips back when the
4137
+ // bounded copy-store lands (doc/undo-v2-copy-store-design.md).
4138
+ enableUndo: false,
3881
4139
  enableHookLogDebug: true,
3882
4140
  approvalTimeoutMs: 12e4,
3883
4141
  // 120-second auto-deny timeout
@@ -4076,10 +4334,13 @@ var DEFAULT_CONFIG = {
4076
4334
  skillPinning: { enabled: false, mode: "warn", roots: [] },
4077
4335
  trustedHosts: [],
4078
4336
  trustedHostsManaged: false,
4079
- appPermissions: {}
4337
+ appPermissions: {},
4338
+ managedJailPaths: []
4080
4339
  },
4081
4340
  environments: {}
4082
4341
  };
4342
+ var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
4343
+ var VERDICT_ORDER = ["allow", "review", "block"];
4083
4344
  var ADVISORY_SMART_RULES = [
4084
4345
  // ── rm safety ─────────────────────────────────────────────────────────────
4085
4346
  // tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
@@ -4091,12 +4352,7 @@ var ADVISORY_SMART_RULES = [
4091
4352
  conditionMode: "all",
4092
4353
  conditions: [
4093
4354
  { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
4094
- {
4095
- field: "command",
4096
- op: "matches",
4097
- // Matches known-safe build artifact paths in the command.
4098
- value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
4099
- }
4355
+ { field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
4100
4356
  ],
4101
4357
  verdict: "allow",
4102
4358
  reason: "Deleting a known-safe build artifact path"
@@ -4274,10 +4530,15 @@ function getConfig(cwd) {
4274
4530
  // here. A managed list fills this below and flips trustedHostsManaged.
4275
4531
  trustedHosts: [],
4276
4532
  trustedHostsManaged: false,
4277
- appPermissions: {}
4533
+ appPermissions: {},
4534
+ managedJailPaths: []
4278
4535
  };
4279
4536
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
4280
- const applyLayer = (source) => {
4537
+ const rank = (v) => {
4538
+ const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
4539
+ return i === -1 ? 1 : i;
4540
+ };
4541
+ const applyLayer = (source, isProject = false) => {
4281
4542
  if (!source) return;
4282
4543
  const s = source.settings || {};
4283
4544
  const p = source.policy || {};
@@ -4337,7 +4598,9 @@ function getConfig(cwd) {
4337
4598
  };
4338
4599
  for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4339
4600
  const v = src[k];
4340
- if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4601
+ if (v !== "off" && v !== "review" && v !== "block") continue;
4602
+ if (isProject && rank(v) < rank(cc2[k])) continue;
4603
+ cc2[k] = v;
4341
4604
  }
4342
4605
  for (const k of ["evalDynamic", "pipeChainHigh"]) {
4343
4606
  const v = src[k];
@@ -4347,11 +4610,22 @@ function getConfig(cwd) {
4347
4610
  }
4348
4611
  if (p.egress) {
4349
4612
  const e = p.egress;
4350
- if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
4351
- if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
4352
- if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
4613
+ if (e.enabled !== void 0 && !(isProject && e.enabled === false))
4614
+ mergedPolicy.egress.enabled = e.enabled;
4615
+ if (e.mode !== void 0) {
4616
+ const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
4617
+ if (!weaker) {
4618
+ mergedPolicy.egress.mode = e.mode;
4619
+ egressModeUserSet = true;
4620
+ }
4621
+ }
4622
+ if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
4623
+ mergedPolicy.egress.allow.push(...e.allow);
4624
+ }
4625
+ if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
4353
4626
  if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
4354
- if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
4627
+ if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
4628
+ mergedPolicy.egress.allowPrivate = e.allowPrivate;
4355
4629
  }
4356
4630
  if (p.loopDetection) {
4357
4631
  const ld = p.loopDetection;
@@ -4393,11 +4667,21 @@ function getConfig(cwd) {
4393
4667
  }
4394
4668
  }
4395
4669
  };
4670
+ let egressModeUserSet = false;
4671
+ let egressAllowUserSet = false;
4396
4672
  applyLayer(globalConfig);
4397
- applyLayer(projectConfig);
4673
+ applyLayer(
4674
+ projectConfig,
4675
+ /* isProject */
4676
+ true
4677
+ );
4398
4678
  let cloudManagedShields = [];
4679
+ const managedCommandCheckKeys = /* @__PURE__ */ new Set();
4680
+ const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
4399
4681
  let modeCloudControlled = false;
4400
4682
  let modeCloudStaged = false;
4683
+ let cloudMandatesEnforcement = false;
4684
+ let cloudMandatesAppPerm = false;
4401
4685
  {
4402
4686
  const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
4403
4687
  try {
@@ -4432,7 +4716,8 @@ function getConfig(cwd) {
4432
4716
  deny: hosts(mc.egress.deny),
4433
4717
  allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
4434
4718
  },
4435
- locked
4719
+ locked,
4720
+ egressModeUserSet
4436
4721
  );
4437
4722
  }
4438
4723
  if (mc.dlp && typeof mc.dlp === "object") {
@@ -4452,6 +4737,12 @@ function getConfig(cwd) {
4452
4737
  mc.commandChecks,
4453
4738
  locked
4454
4739
  );
4740
+ for (const [key, val] of Object.entries(mc.commandChecks)) {
4741
+ if (typeof val !== "string") continue;
4742
+ managedCommandCheckKeys.add(key);
4743
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4744
+ if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
4745
+ }
4455
4746
  }
4456
4747
  if (mc.approvers && typeof mc.approvers === "object") {
4457
4748
  const bool = (v) => typeof v === "boolean" ? v : void 0;
@@ -4498,12 +4789,13 @@ function getConfig(cwd) {
4498
4789
  }
4499
4790
  if (Array.isArray(mc.jailPaths)) {
4500
4791
  for (const jp of mc.jailPaths) {
4501
- const path13 = typeof jp?.path === "string" ? jp.path.trim() : "";
4502
- if (!path13) continue;
4792
+ const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
4793
+ if (!path14) continue;
4503
4794
  const verdict = jp?.verdict === "review" ? "review" : "block";
4504
- for (const r of pathRules(path13, verdict, "org-managed jail")) {
4795
+ for (const r of pathRules(path14, verdict, "org-managed jail")) {
4505
4796
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
4506
4797
  }
4798
+ mergedPolicy.managedJailPaths.push({ path: path14, verdict });
4507
4799
  }
4508
4800
  }
4509
4801
  if (Array.isArray(mc.trustedHosts)) {
@@ -4521,7 +4813,14 @@ function getConfig(cwd) {
4521
4813
  if (Object.keys(m).length) coerced[srv] = m;
4522
4814
  }
4523
4815
  mergedPolicy.appPermissions = coerced;
4816
+ cloudMandatesAppPerm = Object.values(coerced).some(
4817
+ (tools) => Object.values(tools).some((d) => d === "block" || d === "review")
4818
+ );
4524
4819
  }
4820
+ const on = (v) => !!v && typeof v === "object" && v.enabled === true;
4821
+ 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(
4822
+ (v) => typeof v === "string" && v !== "off"
4823
+ );
4525
4824
  }
4526
4825
  if (raw.panicMode === true) {
4527
4826
  mergedSettings.panicMode = true;
@@ -4563,25 +4862,45 @@ function getConfig(cwd) {
4563
4862
  }
4564
4863
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4565
4864
  const cc = mergedPolicy.commandChecks ?? {};
4566
- const advisoryKnob = (name) => {
4567
- if (name === "review-rm") return cc.rmAdvisory;
4568
- if (name?.endsWith("-sql")) return cc.sqlDdl;
4865
+ const advisoryKnobKey = (name) => {
4866
+ if (name === "review-rm") return "rmAdvisory";
4867
+ if (name?.endsWith("-sql")) return "sqlDdl";
4569
4868
  return void 0;
4570
4869
  };
4571
4870
  for (const rule of ADVISORY_SMART_RULES) {
4572
- if (existingAdvisoryNames.has(rule.name)) continue;
4573
- const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4871
+ const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
4872
+ const knob = knobKey ? cc[knobKey] : void 0;
4574
4873
  if (knob === "off") continue;
4575
- mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4874
+ const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
4875
+ const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
4876
+ const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
4877
+ const knobVerdict = knob === "block" ? "block" : rule.verdict;
4878
+ if (!managed) {
4879
+ if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
4880
+ continue;
4881
+ }
4882
+ const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
4883
+ if (twin) {
4884
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4885
+ }
4886
+ const injected = { ...rule, verdict: effective, pinned: true };
4887
+ if (rule.name === "review-rm" && effective !== "block") {
4888
+ injected.conditions = [
4889
+ ...rule.conditions ?? [],
4890
+ { field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
4891
+ ];
4892
+ injected.conditionMode = "all";
4893
+ }
4894
+ mergedPolicy.smartRules.push(injected);
4576
4895
  }
4577
4896
  const envMode = process.env.NODE9_MODE;
4578
4897
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4579
4898
  mergedSettings.mode = envMode;
4580
4899
  }
4581
- if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4900
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4582
4901
  mergedSettings.mode = "standard";
4583
4902
  }
4584
- const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4903
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
4585
4904
  if (modeCloudControlled && mergedSettings.mode === "strict") {
4586
4905
  for (const name of Object.keys(mergedEnvironments)) {
4587
4906
  if (mergedEnvironments[name]?.requireApproval === false) {
@@ -4782,14 +5101,14 @@ function checkProvenance(cmd, cwd) {
4782
5101
  }
4783
5102
 
4784
5103
  // src/policy/index.ts
4785
- async function evaluatePolicy2(toolName, args, agent, cwd) {
5104
+ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
4786
5105
  const config = getConfig();
4787
5106
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
4788
5107
  return evaluatePolicy(
4789
5108
  config,
4790
5109
  toolName,
4791
5110
  args,
4792
- { agent, cwd, activeEnvironment },
5111
+ { agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
4793
5112
  {
4794
5113
  checkProvenance,
4795
5114
  // Managed → match against the org list (frozen with the rest of managed
@@ -5641,6 +5960,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
5641
5960
  }
5642
5961
  }
5643
5962
 
5963
+ // src/shields/jail.ts
5964
+ var import_fs11 = __toESM(require("fs"));
5965
+ var import_os10 = __toESM(require("os"));
5966
+ var import_path13 = __toESM(require("path"));
5967
+ var USER_JAIL_SHIELD = "user-jail";
5968
+ function jailStorePath() {
5969
+ return import_path13.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
5970
+ }
5971
+ function readJailPaths() {
5972
+ let text;
5973
+ try {
5974
+ text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
5975
+ } catch (err) {
5976
+ if (err.code === "ENOENT") return [];
5977
+ throw err;
5978
+ }
5979
+ let parsed;
5980
+ try {
5981
+ parsed = JSON.parse(text);
5982
+ } catch {
5983
+ throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
5984
+ }
5985
+ if (!Array.isArray(parsed.paths)) return [];
5986
+ return parsed.paths.filter(
5987
+ (p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
5988
+ );
5989
+ }
5990
+ function findJailedPath(candidate) {
5991
+ return findJailedPathIn(candidate, readJailPaths());
5992
+ }
5993
+ function findJailedPathIn(candidate, paths) {
5994
+ if (!candidate) return null;
5995
+ for (const entry of paths) {
5996
+ if (pathMatchesFragment(candidate, entry.path)) return entry;
5997
+ }
5998
+ return null;
5999
+ }
6000
+
5644
6001
  // src/auth/orchestrator.ts
5645
6002
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
5646
6003
  "write",
@@ -6116,12 +6473,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
6116
6473
  } else if (!taintWarning && !appPermReview) {
6117
6474
  const toolLower = toolName.toLowerCase();
6118
6475
  const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
6119
- if (isFileTool && readActiveShields().includes("project-jail")) {
6476
+ const activeShields = isFileTool ? readActiveShields() : [];
6477
+ const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
6478
+ if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
6120
6479
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
6121
- const filePath = String(
6122
- argsObj.file_path ?? argsObj.path ?? argsObj.pattern ?? argsObj.filename ?? ""
6123
- );
6124
- if (filePath && scanFilePath(filePath)) {
6480
+ const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
6481
+ const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
6482
+ const sensitiveHit = candidates.some((c) => scanFilePath(c));
6483
+ if (jailHit || sensitiveHit) {
6484
+ const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
6485
+ skipIgnoredFastPath: true
6486
+ });
6487
+ if (policyResult.decision === "block") {
6488
+ if (!isManual)
6489
+ appendLocalAudit(
6490
+ toolName,
6491
+ args,
6492
+ "deny",
6493
+ "smart-rule-block",
6494
+ { ...meta, ruleName: policyResult.ruleName },
6495
+ hashAuditArgs
6496
+ );
6497
+ return {
6498
+ approved: false,
6499
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
6500
+ blockedBy: "local-config",
6501
+ blockedByLabel: policyResult.blockedByLabel,
6502
+ ruleHit: policyResult.ruleName
6503
+ };
6504
+ }
6125
6505
  } else {
6126
6506
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
6127
6507
  return { approved: true };
@@ -6184,7 +6564,12 @@ ${appPermReview}`
6184
6564
  }
6185
6565
  let cloudRequestId = null;
6186
6566
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
6187
- const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || void 0;
6567
+ const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
6568
+ // a client-side heuristic the SaaS has no rule for, so without forceReview
6569
+ // its checkRule answers "no org rule matched" → {approved:true}, which is
6570
+ // not an approval of an exfiltration risk. Measured against the live BE:
6571
+ // {approved:true} without this flag, {pending:true} with it.
6572
+ !!taintWarning || void 0;
6188
6573
  if (cloudEnforced) {
6189
6574
  try {
6190
6575
  const initResult = await initNode9SaaS(
@@ -6197,10 +6582,10 @@ ${appPermReview}`
6197
6582
  forceReview
6198
6583
  );
6199
6584
  if (!initResult.pending) {
6200
- if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6585
+ if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6201
6586
  return { approved: true, checkedBy: "cloud" };
6202
6587
  }
6203
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6588
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6204
6589
  return {
6205
6590
  approved: !!initResult.approved,
6206
6591
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),