@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.mjs CHANGED
@@ -380,8 +380,8 @@ function sanitizeConfig(raw) {
380
380
  }
381
381
  }
382
382
  const lines = result.error.issues.map((issue) => {
383
- const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
- return ` \u2022 ${path13}: ${issue.message}`;
383
+ const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
384
+ return ` \u2022 ${path14}: ${issue.message}`;
385
385
  });
386
386
  return {
387
387
  sanitized,
@@ -1001,6 +1001,129 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
1001
1001
  }
1002
1002
  return null;
1003
1003
  }
1004
+ var MAX_REGEX_LENGTH = 256;
1005
+ var REGEX_CACHE_MAX = 500;
1006
+ var regexCache = /* @__PURE__ */ new Map();
1007
+ function validateRegex(pattern) {
1008
+ if (!pattern) return "Pattern is required";
1009
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
1010
+ try {
1011
+ new RegExp(pattern);
1012
+ } catch (e) {
1013
+ return `Invalid regex syntax: ${e.message}`;
1014
+ }
1015
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
1016
+ if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
1017
+ return null;
1018
+ }
1019
+ function getCompiledRegex(pattern, flags = "") {
1020
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
1021
+ const key = `${pattern}\0${flags}`;
1022
+ if (regexCache.has(key)) {
1023
+ const cached = regexCache.get(key);
1024
+ regexCache.delete(key);
1025
+ regexCache.set(key, cached);
1026
+ return cached;
1027
+ }
1028
+ if (validateRegex(pattern) !== null) return null;
1029
+ try {
1030
+ const re = new RegExp(pattern, flags);
1031
+ if (regexCache.size >= REGEX_CACHE_MAX) {
1032
+ const oldest = regexCache.keys().next().value;
1033
+ if (oldest) regexCache.delete(oldest);
1034
+ }
1035
+ regexCache.set(key, re);
1036
+ return re;
1037
+ } catch {
1038
+ return null;
1039
+ }
1040
+ }
1041
+ function matchesPattern(text, patterns) {
1042
+ const p = Array.isArray(patterns) ? patterns : [patterns];
1043
+ if (p.length === 0) return false;
1044
+ const isMatch = pm(p, { nocase: true, dot: true });
1045
+ const target = text.toLowerCase();
1046
+ const directMatch = isMatch(target);
1047
+ if (directMatch) return true;
1048
+ const withoutDotSlash = text.replace(/^\.\//, "");
1049
+ return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1050
+ }
1051
+ var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1052
+ function getNestedValue(obj, path14) {
1053
+ if (!obj || typeof obj !== "object") return null;
1054
+ const segments = path14.split(".");
1055
+ for (const seg of segments) {
1056
+ if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
1057
+ }
1058
+ return segments.reduce((prev, curr) => prev?.[curr], obj);
1059
+ }
1060
+ function evaluateSmartConditions(args, rule) {
1061
+ if (!rule.conditions || rule.conditions.length === 0) return true;
1062
+ const mode = rule.conditionMode ?? "all";
1063
+ const fieldCache = /* @__PURE__ */ new Map();
1064
+ const resolveField = (field) => {
1065
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
1066
+ const rawVal = getNestedValue(args, field);
1067
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
1068
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
1069
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1070
+ fieldCache.set(field, val);
1071
+ return val;
1072
+ };
1073
+ const readingsCache = /* @__PURE__ */ new Map();
1074
+ const resolveFieldReadings = (field) => {
1075
+ const cached = readingsCache.get(field);
1076
+ if (cached) return cached;
1077
+ const primary = resolveField(field);
1078
+ if (primary === null) {
1079
+ readingsCache.set(field, []);
1080
+ return [];
1081
+ }
1082
+ let out = [primary];
1083
+ if (field === "command") {
1084
+ const raw = getNestedValue(args, field);
1085
+ if (typeof raw === "string") {
1086
+ const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
1087
+ out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
1088
+ }
1089
+ }
1090
+ readingsCache.set(field, out);
1091
+ return out;
1092
+ };
1093
+ const results = rule.conditions.map((cond) => {
1094
+ const val = resolveField(cond.field);
1095
+ switch (cond.op) {
1096
+ case "exists":
1097
+ return val !== null && val !== "";
1098
+ case "notExists":
1099
+ return val === null || val === "";
1100
+ case "contains":
1101
+ return val !== null && cond.value ? val.includes(cond.value) : false;
1102
+ case "notContains":
1103
+ return val !== null && cond.value ? !val.includes(cond.value) : true;
1104
+ case "matches": {
1105
+ if (val === null || !cond.value) return false;
1106
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1107
+ if (!reM) return false;
1108
+ return resolveFieldReadings(cond.field).some((v) => reM.test(v));
1109
+ }
1110
+ case "notMatches": {
1111
+ if (!cond.value) return false;
1112
+ if (val === null) return true;
1113
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1114
+ if (!reN) return false;
1115
+ return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
1116
+ }
1117
+ case "matchesGlob":
1118
+ return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
1119
+ case "notMatchesGlob":
1120
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
1121
+ default:
1122
+ return false;
1123
+ }
1124
+ });
1125
+ return mode === "any" ? results.some((r) => r) : results.every((r) => r);
1126
+ }
1004
1127
  var { syntax } = mvdanSh;
1005
1128
  var sharedParser = syntax.NewParser();
1006
1129
  var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -1073,14 +1196,22 @@ function cachedNormalize(command, compute) {
1073
1196
  return result;
1074
1197
  }
1075
1198
  function normalizeCommandForPolicy(command) {
1199
+ return commandReadingsImpl(command).posix;
1200
+ }
1201
+ function commandReadings(command) {
1202
+ const r = commandReadingsImpl(command);
1203
+ return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
1204
+ }
1205
+ function commandReadingsImpl(command) {
1076
1206
  return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
1077
1207
  }
1078
1208
  function normalizeCommandForPolicyImpl(command) {
1079
1209
  const f = parseShared(command);
1080
- if (f === PARSE_FAIL) return command;
1210
+ if (f === PARSE_FAIL) return { posix: command, separator: command };
1081
1211
  try {
1082
1212
  const strips = [];
1083
1213
  const rewrites = [];
1214
+ const quoteOnlyRewrites = [];
1084
1215
  const msgSpans = /* @__PURE__ */ new Set();
1085
1216
  syntax.Walk(f, (node) => {
1086
1217
  if (!node) return false;
@@ -1125,22 +1256,23 @@ function normalizeCommandForPolicyImpl(command) {
1125
1256
  if (resolved === source) continue;
1126
1257
  if (resolved === "" || /\s/.test(resolved)) continue;
1127
1258
  rewrites.push([s, e, resolved]);
1259
+ const quoteOnly = source.replace(/['"]/g, "");
1260
+ if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
1128
1261
  }
1129
1262
  return true;
1130
1263
  });
1131
- const edits = [
1132
- ...strips.map(([s, e]) => [s, e, '""']),
1133
- ...rewrites
1134
- ];
1135
- if (edits.length === 0) return command;
1136
- edits.sort((a, b) => b[0] - a[0]);
1137
- let result = command;
1138
- for (const [s, e, rep] of edits) {
1139
- result = result.slice(0, s) + rep + result.slice(e);
1140
- }
1141
- return result;
1264
+ const stripEdits = strips.map(([s, e]) => [s, e, '""']);
1265
+ const apply = (extra) => {
1266
+ const edits = [...stripEdits, ...extra];
1267
+ if (edits.length === 0) return command;
1268
+ edits.sort((a, b) => b[0] - a[0]);
1269
+ let out = command;
1270
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
1271
+ return out;
1272
+ };
1273
+ return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
1142
1274
  } catch {
1143
- return command;
1275
+ return { posix: command, separator: command };
1144
1276
  }
1145
1277
  }
1146
1278
  function scanArgsForDynamicExec(args, startIdx) {
@@ -1381,6 +1513,208 @@ function chmodHasOpenPermMode(command) {
1381
1513
  }
1382
1514
  return found;
1383
1515
  }
1516
+ function isShellShapedTool(toolName, toolInspection) {
1517
+ if (isBashTool(toolName)) return true;
1518
+ if (!toolInspection) return false;
1519
+ const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
1520
+ return pattern !== void 0 && toolInspection[pattern] === "command";
1521
+ }
1522
+ function toolMatchesRule(toolName, ruleTool, toolInspection) {
1523
+ if (!ruleTool) return true;
1524
+ if (matchesPattern(toolName, ruleTool)) return true;
1525
+ return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
1526
+ }
1527
+ 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;
1528
+ var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
1529
+ "uv",
1530
+ "uvx",
1531
+ "poetry",
1532
+ "pipenv",
1533
+ "pdm",
1534
+ "rye",
1535
+ "hatch",
1536
+ "conda",
1537
+ "mamba",
1538
+ "micromamba",
1539
+ "npx",
1540
+ "pnpm",
1541
+ "yarn",
1542
+ "bunx",
1543
+ "watch",
1544
+ "strace",
1545
+ "ltrace",
1546
+ "chroot",
1547
+ "unshare",
1548
+ "runuser"
1549
+ ]);
1550
+ function isInlineCodeFlag(interp, w) {
1551
+ const lw = w.toLowerCase();
1552
+ if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
1553
+ if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
1554
+ if (!w.startsWith("-") || w.startsWith("--")) return false;
1555
+ const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1556
+ const body = lw.slice(1);
1557
+ const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
1558
+ const cut = body.search(cutAt);
1559
+ const bundle = cut >= 0 ? body.slice(0, cut) : body;
1560
+ return [...codeLetters].some((l) => bundle.includes(l));
1561
+ }
1562
+ var _redirStdinOps = null;
1563
+ function redirStdinOps() {
1564
+ if (_redirStdinOps) return _redirStdinOps;
1565
+ _redirStdinOps = new Set(
1566
+ [
1567
+ deriveRedirOp("cat <<X\nX"),
1568
+ deriveRedirOp("cat <<-X\nX"),
1569
+ deriveRedirOp("cat < f"),
1570
+ deriveRedirOp("cat <<< x")
1571
+ ].filter((op) => op >= 0)
1572
+ );
1573
+ return _redirStdinOps;
1574
+ }
1575
+ function deriveBinaryOp(sample) {
1576
+ try {
1577
+ const f = sharedParser.Parse(sample, "cmd");
1578
+ let op = -1;
1579
+ syntax.Walk(f, (node) => {
1580
+ const n = node;
1581
+ if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
1582
+ return true;
1583
+ });
1584
+ return op;
1585
+ } catch {
1586
+ return -1;
1587
+ }
1588
+ }
1589
+ var _listOps = null;
1590
+ function listOps() {
1591
+ if (_listOps) return _listOps;
1592
+ _listOps = new Set(
1593
+ [deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
1594
+ );
1595
+ return _listOps;
1596
+ }
1597
+ var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
1598
+ var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
1599
+ function unwrapCommandHead(words) {
1600
+ let i = 0;
1601
+ while (i < words.length) {
1602
+ const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
1603
+ if (head === "find") {
1604
+ const x = words.findIndex(
1605
+ (w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
1606
+ );
1607
+ if (x < 0) break;
1608
+ i = x + 1;
1609
+ continue;
1610
+ }
1611
+ if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
1612
+ i++;
1613
+ let targetConsumed = false;
1614
+ while (i < words.length) {
1615
+ const t = words[i];
1616
+ if (t === null) {
1617
+ i++;
1618
+ continue;
1619
+ }
1620
+ const lt = t.toLowerCase();
1621
+ if (/^[A-Za-z_]\w*=/.test(t)) {
1622
+ i++;
1623
+ continue;
1624
+ }
1625
+ if (t.startsWith("-")) {
1626
+ i++;
1627
+ const nxt = words[i];
1628
+ if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
1629
+ i++;
1630
+ continue;
1631
+ }
1632
+ if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
1633
+ i++;
1634
+ continue;
1635
+ }
1636
+ if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
1637
+ targetConsumed = true;
1638
+ i++;
1639
+ continue;
1640
+ }
1641
+ break;
1642
+ }
1643
+ }
1644
+ return i;
1645
+ }
1646
+ function inlineExecStmt(stmt, pipeFed) {
1647
+ const cmd = stmt?.Cmd;
1648
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1649
+ const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1650
+ if (words.length === 0) return false;
1651
+ const headIdx = unwrapCommandHead(words);
1652
+ const rawHead = words[headIdx];
1653
+ if (rawHead == null) return false;
1654
+ const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1655
+ if (!INLINE_INTERPRETER.test(interp)) return false;
1656
+ let args = words.slice(headIdx + 1);
1657
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1658
+ if (INTERP_LEADING_TARGET.has(interp)) {
1659
+ const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1660
+ args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1661
+ }
1662
+ let positionals = 0;
1663
+ let selectedProgram = false;
1664
+ for (const a of args) {
1665
+ if (a == null) {
1666
+ positionals++;
1667
+ selectedProgram = true;
1668
+ continue;
1669
+ }
1670
+ if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
1671
+ if (a === "-m") {
1672
+ selectedProgram = true;
1673
+ continue;
1674
+ }
1675
+ if (a === "-" && !selectedProgram) return true;
1676
+ if (!a.startsWith("-")) {
1677
+ positionals++;
1678
+ selectedProgram = true;
1679
+ }
1680
+ }
1681
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1682
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1683
+ if (positionals === 0 && (stdinFed || pipeFed)) {
1684
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1685
+ }
1686
+ return false;
1687
+ }
1688
+ function detectInlineExec(command) {
1689
+ const f = parseShared(command);
1690
+ if (f === PARSE_FAIL) {
1691
+ 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(
1692
+ command
1693
+ );
1694
+ }
1695
+ let found = false;
1696
+ try {
1697
+ syntax.Walk(f, (node) => {
1698
+ if (!node || found) return false;
1699
+ const n = node;
1700
+ const t = syntax.NodeType(n);
1701
+ if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1702
+ if (inlineExecStmt(n.Y, true)) {
1703
+ found = true;
1704
+ return false;
1705
+ }
1706
+ }
1707
+ if (t === "Stmt" && inlineExecStmt(n, false)) {
1708
+ found = true;
1709
+ return false;
1710
+ }
1711
+ return true;
1712
+ });
1713
+ } catch {
1714
+ return found;
1715
+ }
1716
+ return found;
1717
+ }
1384
1718
  function analyzeChmod777(command) {
1385
1719
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
1386
1720
  if (!chmodHasOpenPermMode(command)) return null;
@@ -2245,150 +2579,12 @@ function extractAllSshHosts(tokens) {
2245
2579
  }
2246
2580
  return [...hosts].filter(Boolean);
2247
2581
  }
2248
- var MAX_REGEX_LENGTH = 100;
2249
- var REGEX_CACHE_MAX = 500;
2250
- var regexCache = /* @__PURE__ */ new Map();
2251
- function validateRegex(pattern) {
2252
- if (!pattern) return "Pattern is required";
2253
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
2254
- try {
2255
- new RegExp(pattern);
2256
- } catch (e) {
2257
- return `Invalid regex syntax: ${e.message}`;
2258
- }
2259
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
2260
- if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
2261
- return null;
2262
- }
2263
- function getCompiledRegex(pattern, flags = "") {
2264
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
2265
- const key = `${pattern}\0${flags}`;
2266
- if (regexCache.has(key)) {
2267
- const cached = regexCache.get(key);
2268
- regexCache.delete(key);
2269
- regexCache.set(key, cached);
2270
- return cached;
2271
- }
2272
- if (validateRegex(pattern) !== null) return null;
2273
- try {
2274
- const re = new RegExp(pattern, flags);
2275
- if (regexCache.size >= REGEX_CACHE_MAX) {
2276
- const oldest = regexCache.keys().next().value;
2277
- if (oldest) regexCache.delete(oldest);
2278
- }
2279
- regexCache.set(key, re);
2280
- return re;
2281
- } catch {
2282
- return null;
2283
- }
2284
- }
2285
- function matchesPattern(text, patterns) {
2286
- const p = Array.isArray(patterns) ? patterns : [patterns];
2287
- if (p.length === 0) return false;
2288
- const isMatch = pm(p, { nocase: true, dot: true });
2289
- const target = text.toLowerCase();
2290
- const directMatch = isMatch(target);
2291
- if (directMatch) return true;
2292
- const withoutDotSlash = text.replace(/^\.\//, "");
2293
- return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2294
- }
2295
- var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2296
- function getNestedValue(obj, path13) {
2297
- if (!obj || typeof obj !== "object") return null;
2298
- const segments = path13.split(".");
2299
- for (const seg of segments) {
2300
- if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2301
- }
2302
- return segments.reduce((prev, curr) => prev?.[curr], obj);
2303
- }
2304
- function evaluateSmartConditions(args, rule) {
2305
- if (!rule.conditions || rule.conditions.length === 0) return true;
2306
- const mode = rule.conditionMode ?? "all";
2307
- const fieldCache = /* @__PURE__ */ new Map();
2308
- const resolveField = (field) => {
2309
- if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
2310
- const rawVal = getNestedValue(args, field);
2311
- const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
2312
- const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
2313
- const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
2314
- fieldCache.set(field, val);
2315
- return val;
2316
- };
2317
- const results = rule.conditions.map((cond) => {
2318
- const val = resolveField(cond.field);
2319
- switch (cond.op) {
2320
- case "exists":
2321
- return val !== null && val !== "";
2322
- case "notExists":
2323
- return val === null || val === "";
2324
- case "contains":
2325
- return val !== null && cond.value ? val.includes(cond.value) : false;
2326
- case "notContains":
2327
- return val !== null && cond.value ? !val.includes(cond.value) : true;
2328
- case "matches": {
2329
- if (val === null || !cond.value) return false;
2330
- const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2331
- if (!reM) return false;
2332
- return reM.test(val);
2333
- }
2334
- case "notMatches": {
2335
- if (!cond.value) return false;
2336
- if (val === null) return true;
2337
- const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2338
- if (!reN) return false;
2339
- return !reN.test(val);
2340
- }
2341
- case "matchesGlob":
2342
- return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
2343
- case "notMatchesGlob":
2344
- return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
2345
- default:
2346
- return false;
2347
- }
2348
- });
2349
- return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2350
- }
2351
2582
  function resolveCheck(v) {
2352
2583
  return v === "off" || v === "block" ? v : "review";
2353
2584
  }
2354
2585
  function resolveCheckTight(v) {
2355
2586
  return v === "block" ? "block" : "review";
2356
2587
  }
2357
- var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2358
- var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2359
- function detectInlineExec(command) {
2360
- const pipeFed = command.includes("|");
2361
- const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2362
- for (const rawSeg of segments) {
2363
- const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2364
- let i = 0;
2365
- while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2366
- if (i >= tokens.length) continue;
2367
- const base = tokens[i].split("/").pop() ?? tokens[i];
2368
- if (!INLINE_INTERP.test(base)) continue;
2369
- const args = tokens.slice(i + 1);
2370
- let hadRedirect = false;
2371
- const positionals = [];
2372
- for (let j = 0; j < args.length; j++) {
2373
- const a = args[j];
2374
- if (a === "-") return true;
2375
- if (a.startsWith("<")) {
2376
- hadRedirect = true;
2377
- if (a === "<" || a === "<<") j++;
2378
- continue;
2379
- }
2380
- if (a.startsWith("-")) {
2381
- if (/^-(c|e|eval)$/i.test(a)) return true;
2382
- continue;
2383
- }
2384
- positionals.push(a);
2385
- }
2386
- if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2387
- return true;
2388
- }
2389
- }
2390
- return false;
2391
- }
2392
2588
  var VERDICT_RANK = {
2393
2589
  allow: 0,
2394
2590
  review: 1,
@@ -2402,6 +2598,12 @@ function resolvePinned(matches) {
2402
2598
  (best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
2403
2599
  );
2404
2600
  }
2601
+ function strictestVerdict(candidates) {
2602
+ if (candidates.length === 0) return void 0;
2603
+ return candidates.reduce(
2604
+ (best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
2605
+ );
2606
+ }
2405
2607
  function tokenize2(toolName) {
2406
2608
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
2407
2609
  }
@@ -2413,6 +2615,11 @@ function extractShellCommand(toolName, args, toolInspection) {
2413
2615
  const value = getNestedValue(args, fieldPath);
2414
2616
  return typeof value === "string" ? value : null;
2415
2617
  }
2618
+ function inspectsShellCommand(toolName, toolInspection) {
2619
+ const patterns = Object.keys(toolInspection);
2620
+ const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
2621
+ return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
2622
+ }
2416
2623
  function isSqlTool(toolName, toolInspection) {
2417
2624
  const patterns = Object.keys(toolInspection);
2418
2625
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -2475,8 +2682,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2475
2682
  };
2476
2683
  }
2477
2684
  }
2478
- if (wouldBeIgnored) return { decision: "allow" };
2479
- const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2685
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2686
+ const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2687
+ const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2688
+ const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2480
2689
  if (bashCommand !== null) {
2481
2690
  const pipeVerdict = pipeChainVerdict(
2482
2691
  bashCommand,
@@ -2526,8 +2735,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2526
2735
  }
2527
2736
  if (config.policy.smartRules.length > 0) {
2528
2737
  const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2738
+ const astSuppressed = (rule) => {
2739
+ if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
2740
+ 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;
2741
+ if (knob === "off" && rule.pinned) return false;
2742
+ return true;
2743
+ };
2529
2744
  const matches = config.policy.smartRules.filter(
2530
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2745
+ (rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2531
2746
  );
2532
2747
  const matchedRule = resolvePinned(matches);
2533
2748
  if (matchedRule) {
@@ -2558,15 +2773,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2558
2773
  const analyzed = analyzeShellCommand(shellCommand);
2559
2774
  allTokens = analyzed.allTokens;
2560
2775
  pathTokens = analyzed.paths;
2561
- const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2562
- if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2563
- return {
2564
- decision: inlineAction === "block" ? "block" : "review",
2565
- blockedByLabel: "Node9 Standard (Inline Execution)",
2566
- ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2567
- tier: 3
2568
- };
2569
- }
2776
+ const candidates = [];
2570
2777
  const evalVerdict = detectDangerousShellExec(shellCommand);
2571
2778
  if (evalVerdict === "block") {
2572
2779
  return {
@@ -2577,24 +2784,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2577
2784
  tier: 3
2578
2785
  };
2579
2786
  }
2787
+ const ptVerdict = pipeChainVerdict(
2788
+ shellCommand,
2789
+ isTrustedHost2,
2790
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2791
+ );
2792
+ if (ptVerdict?.decision === "allow") return ptVerdict;
2793
+ if (ptVerdict) candidates.push(ptVerdict);
2794
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2795
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2796
+ candidates.push({
2797
+ decision: inlineAction === "block" ? "block" : "review",
2798
+ blockedByLabel: "Node9 Standard (Inline Execution)",
2799
+ ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2800
+ tier: 3
2801
+ });
2802
+ }
2580
2803
  if (evalVerdict === "review") {
2581
- return {
2582
- // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2583
- // block but can never turn this off (eval-remote above is Class A —
2584
- // no knob at all).
2804
+ candidates.push({
2585
2805
  decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2586
2806
  blockedByLabel: "Node9: Eval Dynamic Content",
2587
2807
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2588
2808
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
2589
2809
  tier: 3
2590
- };
2810
+ });
2591
2811
  }
2592
- const ptVerdict = pipeChainVerdict(
2593
- shellCommand,
2594
- isTrustedHost2,
2595
- resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2596
- );
2597
- if (ptVerdict) return ptVerdict;
2812
+ const builtin = strictestVerdict(candidates);
2813
+ if (builtin) return builtin;
2598
2814
  if (config.policy.egress?.enabled) {
2599
2815
  const dests = extractShellDestinations(shellCommand);
2600
2816
  if (dests.length > 0) {
@@ -3645,28 +3861,41 @@ function readShieldOverrides() {
3645
3861
  var MODE_ORDER = ["observe", "audit", "standard", "strict"];
3646
3862
  var EGRESS_MODE_ORDER = ["off", "review", "block"];
3647
3863
  function rankIn(order, value) {
3648
- return order.indexOf(value);
3864
+ return value === void 0 ? -1 : order.indexOf(value);
3649
3865
  }
3650
- function resolveByOrder(order, local, cloud, locked) {
3651
- if (rankIn(order, cloud) === -1) return local;
3652
- if (locked) return cloud;
3866
+ function floorValue(order, local, cloud, opts = {}) {
3867
+ if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
3868
+ if (opts.locked) return cloud;
3869
+ const localSet = opts.localWasSet ?? local !== void 0;
3870
+ if (!localSet || rankIn(order, local) === -1) return cloud;
3653
3871
  return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
3654
3872
  }
3873
+ function strictestOf(order, ...values) {
3874
+ let best;
3875
+ for (const v of values) {
3876
+ if (rankIn(order, v) === -1) continue;
3877
+ if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
3878
+ }
3879
+ return best;
3880
+ }
3881
+ function resolveByOrder(order, local, cloud, locked) {
3882
+ return floorValue(order, local, cloud, { locked }) ?? local;
3883
+ }
3655
3884
  function resolveManagedMode(local, cloud, locked) {
3656
3885
  return resolveByOrder(MODE_ORDER, local, cloud, locked);
3657
3886
  }
3658
- function applyManagedEgress(local, managed, locked) {
3887
+ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
3659
3888
  const next = { ...local };
3660
3889
  if (typeof managed.enabled === "boolean") {
3661
3890
  next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
3662
3891
  }
3663
3892
  if (typeof managed.mode === "string") {
3664
- next.mode = resolveByOrder(
3665
- EGRESS_MODE_ORDER,
3666
- local.mode,
3667
- managed.mode,
3668
- locked.includes("egressMode")
3669
- );
3893
+ next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
3894
+ locked: locked.includes("egressMode"),
3895
+ // The default 'review' is seeded into egress before any merge, so absence
3896
+ // is invisible from `local.mode` alone — the caller tracks it for us.
3897
+ localWasSet: localModeUserSet
3898
+ }) ?? local.mode;
3670
3899
  }
3671
3900
  if (Array.isArray(managed.allow) && managed.allow.length > 0) {
3672
3901
  next.allow = [...managed.allow];
@@ -3686,6 +3915,9 @@ function applyManagedDlp(local, managed, locked) {
3686
3915
  if (typeof managed.enabled === "boolean") {
3687
3916
  next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
3688
3917
  }
3918
+ if (managed.enabled === true) {
3919
+ next.scanIgnoredTools = true;
3920
+ }
3689
3921
  if (typeof managed.pii === "string") {
3690
3922
  next.pii = resolveByOrder(
3691
3923
  DLP_PII_ORDER,
@@ -3719,12 +3951,9 @@ function applyManagedCommandChecks(local, managed, locked) {
3719
3951
  const m = managed[key];
3720
3952
  if (typeof m !== "string") continue;
3721
3953
  const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3722
- const resolved = resolveByOrder(
3723
- COMMAND_CHECK_ORDER,
3724
- local[key] ?? "review",
3725
- m,
3726
- locked.includes(lockKey)
3727
- );
3954
+ const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
3955
+ locked: locked.includes(lockKey)
3956
+ });
3728
3957
  if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3729
3958
  next[key] = resolved;
3730
3959
  }
@@ -3750,11 +3979,18 @@ function slug(s) {
3750
3979
  var B = "[\\s/\\\\]";
3751
3980
  var SEP = "[/\\\\]";
3752
3981
  function pathToRegexFragment(rawPath) {
3753
- const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
3982
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
3754
3983
  const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
3755
3984
  if (segments.length === 0) return "";
3756
3985
  return `(^|${B})${segments.join(SEP)}(${B}|$)`;
3757
3986
  }
3987
+ function pathMatchesFragment(candidate, rawPath) {
3988
+ const value = pathToRegexFragment(rawPath);
3989
+ if (!value || !candidate) return false;
3990
+ const re = getCompiledRegex(value);
3991
+ if (!re) return false;
3992
+ return re.test(candidate);
3993
+ }
3758
3994
  function pathRules(rawPath, verdict, reason) {
3759
3995
  const value = pathToRegexFragment(rawPath);
3760
3996
  if (!value) return [];
@@ -3768,12 +4004,28 @@ function pathRules(rawPath, verdict, reason) {
3768
4004
  verdict,
3769
4005
  reason: why
3770
4006
  },
4007
+ // Keep the historical `-anytool` name for the file_path rule: the
4008
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
3771
4009
  {
3772
4010
  name: `${verdict}-path-${s}-anytool`,
3773
4011
  tool: "*",
3774
4012
  conditions: [{ field: "file_path", op: "matches", value }],
3775
4013
  verdict,
3776
4014
  reason: why
4015
+ },
4016
+ {
4017
+ name: `${verdict}-path-${s}-anytool-path`,
4018
+ tool: "*",
4019
+ conditions: [{ field: "path", op: "matches", value }],
4020
+ verdict,
4021
+ reason: why
4022
+ },
4023
+ {
4024
+ name: `${verdict}-path-${s}-anytool-pattern`,
4025
+ tool: "*",
4026
+ conditions: [{ field: "pattern", op: "matches", value }],
4027
+ verdict,
4028
+ reason: why
3777
4029
  }
3778
4030
  ];
3779
4031
  }
@@ -3846,8 +4098,14 @@ var DEFAULT_CONFIG = {
3846
4098
  settings: {
3847
4099
  mode: "standard",
3848
4100
  autoStartDaemon: true,
3849
- enableUndo: true,
3850
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
4101
+ // OFF by default. The snapshot store is a per-project bare git repo with
4102
+ // no size ceiling, and eviction drops the index row without deleting the
4103
+ // objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
4104
+ // from interrupted `git gc`) and filled the disk. A security tool must not
4105
+ // be what fills a customer's disk. Re-enable per install with
4106
+ // `{"settings":{"enableUndo":true}}`; the default flips back when the
4107
+ // bounded copy-store lands (doc/undo-v2-copy-store-design.md).
4108
+ enableUndo: false,
3851
4109
  enableHookLogDebug: true,
3852
4110
  approvalTimeoutMs: 12e4,
3853
4111
  // 120-second auto-deny timeout
@@ -4046,10 +4304,13 @@ var DEFAULT_CONFIG = {
4046
4304
  skillPinning: { enabled: false, mode: "warn", roots: [] },
4047
4305
  trustedHosts: [],
4048
4306
  trustedHostsManaged: false,
4049
- appPermissions: {}
4307
+ appPermissions: {},
4308
+ managedJailPaths: []
4050
4309
  },
4051
4310
  environments: {}
4052
4311
  };
4312
+ var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
4313
+ var VERDICT_ORDER = ["allow", "review", "block"];
4053
4314
  var ADVISORY_SMART_RULES = [
4054
4315
  // ── rm safety ─────────────────────────────────────────────────────────────
4055
4316
  // tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
@@ -4061,12 +4322,7 @@ var ADVISORY_SMART_RULES = [
4061
4322
  conditionMode: "all",
4062
4323
  conditions: [
4063
4324
  { field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
4064
- {
4065
- field: "command",
4066
- op: "matches",
4067
- // Matches known-safe build artifact paths in the command.
4068
- value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
4069
- }
4325
+ { field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
4070
4326
  ],
4071
4327
  verdict: "allow",
4072
4328
  reason: "Deleting a known-safe build artifact path"
@@ -4244,10 +4500,15 @@ function getConfig(cwd) {
4244
4500
  // here. A managed list fills this below and flips trustedHostsManaged.
4245
4501
  trustedHosts: [],
4246
4502
  trustedHostsManaged: false,
4247
- appPermissions: {}
4503
+ appPermissions: {},
4504
+ managedJailPaths: []
4248
4505
  };
4249
4506
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
4250
- const applyLayer = (source) => {
4507
+ const rank = (v) => {
4508
+ const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
4509
+ return i === -1 ? 1 : i;
4510
+ };
4511
+ const applyLayer = (source, isProject = false) => {
4251
4512
  if (!source) return;
4252
4513
  const s = source.settings || {};
4253
4514
  const p = source.policy || {};
@@ -4307,7 +4568,9 @@ function getConfig(cwd) {
4307
4568
  };
4308
4569
  for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4309
4570
  const v = src[k];
4310
- if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4571
+ if (v !== "off" && v !== "review" && v !== "block") continue;
4572
+ if (isProject && rank(v) < rank(cc2[k])) continue;
4573
+ cc2[k] = v;
4311
4574
  }
4312
4575
  for (const k of ["evalDynamic", "pipeChainHigh"]) {
4313
4576
  const v = src[k];
@@ -4317,11 +4580,22 @@ function getConfig(cwd) {
4317
4580
  }
4318
4581
  if (p.egress) {
4319
4582
  const e = p.egress;
4320
- if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
4321
- if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
4322
- if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
4583
+ if (e.enabled !== void 0 && !(isProject && e.enabled === false))
4584
+ mergedPolicy.egress.enabled = e.enabled;
4585
+ if (e.mode !== void 0) {
4586
+ const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
4587
+ if (!weaker) {
4588
+ mergedPolicy.egress.mode = e.mode;
4589
+ egressModeUserSet = true;
4590
+ }
4591
+ }
4592
+ if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
4593
+ mergedPolicy.egress.allow.push(...e.allow);
4594
+ }
4595
+ if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
4323
4596
  if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
4324
- if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
4597
+ if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
4598
+ mergedPolicy.egress.allowPrivate = e.allowPrivate;
4325
4599
  }
4326
4600
  if (p.loopDetection) {
4327
4601
  const ld = p.loopDetection;
@@ -4363,11 +4637,21 @@ function getConfig(cwd) {
4363
4637
  }
4364
4638
  }
4365
4639
  };
4640
+ let egressModeUserSet = false;
4641
+ let egressAllowUserSet = false;
4366
4642
  applyLayer(globalConfig);
4367
- applyLayer(projectConfig);
4643
+ applyLayer(
4644
+ projectConfig,
4645
+ /* isProject */
4646
+ true
4647
+ );
4368
4648
  let cloudManagedShields = [];
4649
+ const managedCommandCheckKeys = /* @__PURE__ */ new Set();
4650
+ const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
4369
4651
  let modeCloudControlled = false;
4370
4652
  let modeCloudStaged = false;
4653
+ let cloudMandatesEnforcement = false;
4654
+ let cloudMandatesAppPerm = false;
4371
4655
  {
4372
4656
  const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
4373
4657
  try {
@@ -4402,7 +4686,8 @@ function getConfig(cwd) {
4402
4686
  deny: hosts(mc.egress.deny),
4403
4687
  allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
4404
4688
  },
4405
- locked
4689
+ locked,
4690
+ egressModeUserSet
4406
4691
  );
4407
4692
  }
4408
4693
  if (mc.dlp && typeof mc.dlp === "object") {
@@ -4422,6 +4707,12 @@ function getConfig(cwd) {
4422
4707
  mc.commandChecks,
4423
4708
  locked
4424
4709
  );
4710
+ for (const [key, val] of Object.entries(mc.commandChecks)) {
4711
+ if (typeof val !== "string") continue;
4712
+ managedCommandCheckKeys.add(key);
4713
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4714
+ if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
4715
+ }
4425
4716
  }
4426
4717
  if (mc.approvers && typeof mc.approvers === "object") {
4427
4718
  const bool = (v) => typeof v === "boolean" ? v : void 0;
@@ -4468,12 +4759,13 @@ function getConfig(cwd) {
4468
4759
  }
4469
4760
  if (Array.isArray(mc.jailPaths)) {
4470
4761
  for (const jp of mc.jailPaths) {
4471
- const path13 = typeof jp?.path === "string" ? jp.path.trim() : "";
4472
- if (!path13) continue;
4762
+ const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
4763
+ if (!path14) continue;
4473
4764
  const verdict = jp?.verdict === "review" ? "review" : "block";
4474
- for (const r of pathRules(path13, verdict, "org-managed jail")) {
4765
+ for (const r of pathRules(path14, verdict, "org-managed jail")) {
4475
4766
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
4476
4767
  }
4768
+ mergedPolicy.managedJailPaths.push({ path: path14, verdict });
4477
4769
  }
4478
4770
  }
4479
4771
  if (Array.isArray(mc.trustedHosts)) {
@@ -4491,7 +4783,14 @@ function getConfig(cwd) {
4491
4783
  if (Object.keys(m).length) coerced[srv] = m;
4492
4784
  }
4493
4785
  mergedPolicy.appPermissions = coerced;
4786
+ cloudMandatesAppPerm = Object.values(coerced).some(
4787
+ (tools) => Object.values(tools).some((d) => d === "block" || d === "review")
4788
+ );
4494
4789
  }
4790
+ const on = (v) => !!v && typeof v === "object" && v.enabled === true;
4791
+ 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(
4792
+ (v) => typeof v === "string" && v !== "off"
4793
+ );
4495
4794
  }
4496
4795
  if (raw.panicMode === true) {
4497
4796
  mergedSettings.panicMode = true;
@@ -4533,25 +4832,45 @@ function getConfig(cwd) {
4533
4832
  }
4534
4833
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4535
4834
  const cc = mergedPolicy.commandChecks ?? {};
4536
- const advisoryKnob = (name) => {
4537
- if (name === "review-rm") return cc.rmAdvisory;
4538
- if (name?.endsWith("-sql")) return cc.sqlDdl;
4835
+ const advisoryKnobKey = (name) => {
4836
+ if (name === "review-rm") return "rmAdvisory";
4837
+ if (name?.endsWith("-sql")) return "sqlDdl";
4539
4838
  return void 0;
4540
4839
  };
4541
4840
  for (const rule of ADVISORY_SMART_RULES) {
4542
- if (existingAdvisoryNames.has(rule.name)) continue;
4543
- const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4841
+ const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
4842
+ const knob = knobKey ? cc[knobKey] : void 0;
4544
4843
  if (knob === "off") continue;
4545
- mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4844
+ const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
4845
+ const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
4846
+ const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
4847
+ const knobVerdict = knob === "block" ? "block" : rule.verdict;
4848
+ if (!managed) {
4849
+ if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
4850
+ continue;
4851
+ }
4852
+ const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
4853
+ if (twin) {
4854
+ mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
4855
+ }
4856
+ const injected = { ...rule, verdict: effective, pinned: true };
4857
+ if (rule.name === "review-rm" && effective !== "block") {
4858
+ injected.conditions = [
4859
+ ...rule.conditions ?? [],
4860
+ { field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
4861
+ ];
4862
+ injected.conditionMode = "all";
4863
+ }
4864
+ mergedPolicy.smartRules.push(injected);
4546
4865
  }
4547
4866
  const envMode = process.env.NODE9_MODE;
4548
4867
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
4549
4868
  mergedSettings.mode = envMode;
4550
4869
  }
4551
- if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4870
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
4552
4871
  mergedSettings.mode = "standard";
4553
4872
  }
4554
- const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
4873
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
4555
4874
  if (modeCloudControlled && mergedSettings.mode === "strict") {
4556
4875
  for (const name of Object.keys(mergedEnvironments)) {
4557
4876
  if (mergedEnvironments[name]?.requireApproval === false) {
@@ -4752,14 +5071,14 @@ function checkProvenance(cmd, cwd) {
4752
5071
  }
4753
5072
 
4754
5073
  // src/policy/index.ts
4755
- async function evaluatePolicy2(toolName, args, agent, cwd) {
5074
+ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
4756
5075
  const config = getConfig();
4757
5076
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
4758
5077
  return evaluatePolicy(
4759
5078
  config,
4760
5079
  toolName,
4761
5080
  args,
4762
- { agent, cwd, activeEnvironment },
5081
+ { agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
4763
5082
  {
4764
5083
  checkProvenance,
4765
5084
  // Managed → match against the org list (frozen with the rest of managed
@@ -5611,6 +5930,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
5611
5930
  }
5612
5931
  }
5613
5932
 
5933
+ // src/shields/jail.ts
5934
+ import fs11 from "fs";
5935
+ import os10 from "os";
5936
+ import path13 from "path";
5937
+ var USER_JAIL_SHIELD = "user-jail";
5938
+ function jailStorePath() {
5939
+ return path13.join(os10.homedir(), ".node9", "jail-paths.json");
5940
+ }
5941
+ function readJailPaths() {
5942
+ let text;
5943
+ try {
5944
+ text = fs11.readFileSync(jailStorePath(), "utf8");
5945
+ } catch (err) {
5946
+ if (err.code === "ENOENT") return [];
5947
+ throw err;
5948
+ }
5949
+ let parsed;
5950
+ try {
5951
+ parsed = JSON.parse(text);
5952
+ } catch {
5953
+ throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
5954
+ }
5955
+ if (!Array.isArray(parsed.paths)) return [];
5956
+ return parsed.paths.filter(
5957
+ (p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
5958
+ );
5959
+ }
5960
+ function findJailedPath(candidate) {
5961
+ return findJailedPathIn(candidate, readJailPaths());
5962
+ }
5963
+ function findJailedPathIn(candidate, paths) {
5964
+ if (!candidate) return null;
5965
+ for (const entry of paths) {
5966
+ if (pathMatchesFragment(candidate, entry.path)) return entry;
5967
+ }
5968
+ return null;
5969
+ }
5970
+
5614
5971
  // src/auth/orchestrator.ts
5615
5972
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
5616
5973
  "write",
@@ -6086,12 +6443,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
6086
6443
  } else if (!taintWarning && !appPermReview) {
6087
6444
  const toolLower = toolName.toLowerCase();
6088
6445
  const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
6089
- if (isFileTool && readActiveShields().includes("project-jail")) {
6446
+ const activeShields = isFileTool ? readActiveShields() : [];
6447
+ const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
6448
+ if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
6090
6449
  const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
6091
- const filePath = String(
6092
- argsObj.file_path ?? argsObj.path ?? argsObj.pattern ?? argsObj.filename ?? ""
6093
- );
6094
- if (filePath && scanFilePath(filePath)) {
6450
+ const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
6451
+ const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
6452
+ const sensitiveHit = candidates.some((c) => scanFilePath(c));
6453
+ if (jailHit || sensitiveHit) {
6454
+ const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
6455
+ skipIgnoredFastPath: true
6456
+ });
6457
+ if (policyResult.decision === "block") {
6458
+ if (!isManual)
6459
+ appendLocalAudit(
6460
+ toolName,
6461
+ args,
6462
+ "deny",
6463
+ "smart-rule-block",
6464
+ { ...meta, ruleName: policyResult.ruleName },
6465
+ hashAuditArgs
6466
+ );
6467
+ return {
6468
+ approved: false,
6469
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
6470
+ blockedBy: "local-config",
6471
+ blockedByLabel: policyResult.blockedByLabel,
6472
+ ruleHit: policyResult.ruleName
6473
+ };
6474
+ }
6095
6475
  } else {
6096
6476
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
6097
6477
  return { approved: true };
@@ -6154,7 +6534,12 @@ ${appPermReview}`
6154
6534
  }
6155
6535
  let cloudRequestId = null;
6156
6536
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
6157
- const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || void 0;
6537
+ const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
6538
+ // a client-side heuristic the SaaS has no rule for, so without forceReview
6539
+ // its checkRule answers "no org rule matched" → {approved:true}, which is
6540
+ // not an approval of an exfiltration risk. Measured against the live BE:
6541
+ // {approved:true} without this flag, {pending:true} with it.
6542
+ !!taintWarning || void 0;
6158
6543
  if (cloudEnforced) {
6159
6544
  try {
6160
6545
  const initResult = await initNode9SaaS(
@@ -6167,10 +6552,10 @@ ${appPermReview}`
6167
6552
  forceReview
6168
6553
  );
6169
6554
  if (!initResult.pending) {
6170
- if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6555
+ if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6171
6556
  return { approved: true, checkedBy: "cloud" };
6172
6557
  }
6173
- if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
6558
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
6174
6559
  return {
6175
6560
  approved: !!initResult.approved,
6176
6561
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),