@node9/proxy 1.60.0 → 1.61.1

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/cli.js CHANGED
@@ -973,6 +973,91 @@ function analyzeFsOperation(command) {
973
973
  fsOpCache.set(normalized, computed);
974
974
  return computed;
975
975
  }
976
+ function isSensitiveCleanupName(p) {
977
+ const base = p.replace(/^.*[\\/]/, "");
978
+ return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
979
+ }
980
+ function isWaivableCleanupTarget(p) {
981
+ if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
982
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
983
+ if (/[*?[{]/.test(p)) return false;
984
+ if (isSensitiveCleanupName(p)) return false;
985
+ return true;
986
+ }
987
+ function deriveRedirOp(sample) {
988
+ try {
989
+ const f = sharedParser.Parse(sample, "cmd");
990
+ let op = -1;
991
+ syntax.Walk(f, (node) => {
992
+ const n = node;
993
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
994
+ return true;
995
+ });
996
+ return op;
997
+ } catch {
998
+ return -1;
999
+ }
1000
+ }
1001
+ function collectSameCommandCreations(f) {
1002
+ const created = /* @__PURE__ */ new Set();
1003
+ try {
1004
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
1005
+ for (const stmt of stmts) {
1006
+ if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
1007
+ const redirs = stmt.Redirs || [];
1008
+ if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
1009
+ for (const r of redirs) {
1010
+ if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
1011
+ const w = resolveWordLiteral(r.Word);
1012
+ if (w) created.add(stripDotSlash(w));
1013
+ }
1014
+ }
1015
+ }
1016
+ } catch {
1017
+ return created;
1018
+ }
1019
+ return created;
1020
+ }
1021
+ function isRmCreatedInCommandCleanup(command) {
1022
+ if (!/\brm\b/.test(command)) return false;
1023
+ const f = parseShared(command);
1024
+ if (f === PARSE_FAIL) return false;
1025
+ const created = collectSameCommandCreations(f);
1026
+ if (created.size === 0) return false;
1027
+ let sawRm = false;
1028
+ let ok2 = true;
1029
+ try {
1030
+ syntax.Walk(f, (node) => {
1031
+ if (!node || !ok2) return false;
1032
+ const n = node;
1033
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1034
+ const args = n.Args || [];
1035
+ const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
1036
+ if (name !== "rm") return true;
1037
+ sawRm = true;
1038
+ const { flags, paths } = extractLiteralArgs(n);
1039
+ if (args.length - 1 > flags.length + paths.length) {
1040
+ ok2 = false;
1041
+ return false;
1042
+ }
1043
+ if (paths.length === 0) {
1044
+ ok2 = false;
1045
+ return false;
1046
+ }
1047
+ for (const p of paths) {
1048
+ const np = stripDotSlash(p);
1049
+ if (!created.has(np) || !isWaivableCleanupTarget(np)) {
1050
+ ok2 = false;
1051
+ return false;
1052
+ }
1053
+ }
1054
+ return true;
1055
+ });
1056
+ } catch {
1057
+ return false;
1058
+ }
1059
+ return sawRm && ok2;
1060
+ }
976
1061
  function analyzeFsOperationImpl(command) {
977
1062
  const f = parseShared(command);
978
1063
  if (f === PARSE_FAIL) return null;
@@ -1543,8 +1628,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1543
1628
  }
1544
1629
  }
1545
1630
  if (config.policy.smartRules.length > 0) {
1631
+ const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
1546
1632
  const matches = config.policy.smartRules.filter(
1547
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
1633
+ (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)
1548
1634
  );
1549
1635
  const matchedRule = resolvePinned(matches);
1550
1636
  if (matchedRule) {
@@ -2285,7 +2371,7 @@ function* stringValues(obj, depth = 0) {
2285
2371
  }
2286
2372
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2287
2373
  }
2288
- var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2374
+ var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2289
2375
  var init_dist = __esm({
2290
2376
  "packages/policy-engine/dist/index.mjs"() {
2291
2377
  "use strict";
@@ -2962,7 +3048,7 @@ var init_dist = __esm({
2962
3048
  "mongosh"
2963
3049
  ]);
2964
3050
  SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
2965
- CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
3051
+ CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
2966
3052
  COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
2967
3053
  "sudo",
2968
3054
  "doas",
@@ -3065,6 +3151,12 @@ var init_dist = __esm({
3065
3151
  };
3066
3152
  FS_OP_CACHE_MAX = 5e3;
3067
3153
  fsOpCache = /* @__PURE__ */ new Map();
3154
+ stripDotSlash = (p) => p.replace(/^\.\//, "");
3155
+ REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
3156
+ REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
3157
+ deriveRedirOp("cat <<X\nX"),
3158
+ deriveRedirOp("cat <<-X\nX")
3159
+ ]);
3068
3160
  DEFAULT_EGRESS_ALLOWLIST = [
3069
3161
  "*.github.com",
3070
3162
  "*.githubusercontent.com",
@@ -49212,9 +49304,10 @@ function loadGeminiCost(start, end, geminiTmpDir2) {
49212
49304
  function dimensionOfBlock(checkedBy, ruleName) {
49213
49305
  if (checkedBy.includes("egress")) return "network";
49214
49306
  if (checkedBy.includes("pii") || checkedBy.includes("dlp")) return "data";
49215
- if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "mcp";
49307
+ if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection"))
49308
+ return "detection";
49309
+ if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "apps";
49216
49310
  if (ruleName.startsWith("shield:project-jail")) return "files";
49217
- if (checkedBy === "loop-detected") return "toolRules";
49218
49311
  if (checkedBy === "timeout" || checkedBy === "local-decision") return "approvals";
49219
49312
  return "toolRules";
49220
49313
  }
@@ -49317,7 +49410,9 @@ function aggregateReportFromAudit(period, opts = {}) {
49317
49410
  let dimDataObserved = 0;
49318
49411
  let dimFilesBlocked = 0;
49319
49412
  let dimToolRulesBlocked = 0;
49320
- let dimMcpBlocked = 0;
49413
+ let dimAppsBlocked = 0;
49414
+ let dimPinMismatch = 0;
49415
+ let dimInjectionBlocked = 0;
49321
49416
  for (const e of entries) {
49322
49417
  if (superseded.has(supersedeKey(e))) continue;
49323
49418
  const allow = isAllow(e.decision);
@@ -49335,6 +49430,8 @@ function aggregateReportFromAudit(period, opts = {}) {
49335
49430
  }
49336
49431
  if (e.checkedBy === "loop-detected") loopHits++;
49337
49432
  const cb = e.checkedBy ?? "";
49433
+ if (cb === "mcp-pin-mismatch") dimPinMismatch++;
49434
+ if (cb.startsWith("injection")) dimInjectionBlocked++;
49338
49435
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
49339
49436
  dimDataObserved++;
49340
49437
  }
@@ -49346,8 +49443,8 @@ function aggregateReportFromAudit(period, opts = {}) {
49346
49443
  case "files":
49347
49444
  dimFilesBlocked++;
49348
49445
  break;
49349
- case "mcp":
49350
- dimMcpBlocked++;
49446
+ case "apps":
49447
+ dimAppsBlocked++;
49351
49448
  break;
49352
49449
  case "toolRules":
49353
49450
  dimToolRulesBlocked++;
@@ -49427,9 +49524,15 @@ function aggregateReportFromAudit(period, opts = {}) {
49427
49524
  dimensions: {
49428
49525
  network: { blocked: dimNetworkBlocked },
49429
49526
  data: { blocked: dlpBlocked, observed: dimDataObserved },
49527
+ detection: {
49528
+ loops: loopHits,
49529
+ pinMismatches: dimPinMismatch,
49530
+ injections: dimInjectionBlocked
49531
+ },
49430
49532
  approvals: { approved: userApproved, denied: userDenied, timedOut },
49431
49533
  files: { blocked: dimFilesBlocked },
49432
- toolRules: { blocked: dimToolRulesBlocked, mcp: dimMcpBlocked, loops: loopHits },
49534
+ toolRules: { blocked: dimToolRulesBlocked },
49535
+ apps: { blocked: dimAppsBlocked },
49433
49536
  cost: { totalUSD: claudeCost.total + codexCost.total + geminiCost.total }
49434
49537
  },
49435
49538
  generatedAt: now.toISOString()
@@ -49729,6 +49832,12 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
49729
49832
  d.data.blocked + d.data.observed > 0,
49730
49833
  `${num2(d.data.blocked)} blocked \xB7 ${num2(d.data.observed)} observed (DLP/PII)`
49731
49834
  );
49835
+ dimRow(
49836
+ "\u{1F501}",
49837
+ "Detection",
49838
+ d.detection.loops + d.detection.pinMismatches + d.detection.injections > 0,
49839
+ `${num2(d.detection.loops)} loops \xB7 ${num2(d.detection.pinMismatches)} pin \xB7 ${num2(d.detection.injections)} injection`
49840
+ );
49732
49841
  dimRow(
49733
49842
  "\u270B",
49734
49843
  "Approvals",
@@ -49739,9 +49848,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
49739
49848
  dimRow(
49740
49849
  "\u{1F6E0}",
49741
49850
  "Tool rules",
49742
- d.toolRules.blocked + d.toolRules.mcp + d.toolRules.loops > 0,
49743
- `${num2(d.toolRules.blocked)} shields/rules \xB7 ${num2(d.toolRules.mcp)} MCP \xB7 ${num2(d.toolRules.loops)} loops`
49851
+ d.toolRules.blocked > 0,
49852
+ `${num2(d.toolRules.blocked)} shields/rules`
49744
49853
  );
49854
+ dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
49745
49855
  dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
49746
49856
  }
49747
49857
  console.log("");
@@ -50817,7 +50927,7 @@ function normalizeClientName(name) {
50817
50927
  const sanitized = sanitize4(name).slice(0, 40);
50818
50928
  return sanitized.length > 0 ? sanitized : void 0;
50819
50929
  }
50820
- function reportPinMismatchToCloud(serverKey, agent) {
50930
+ function reportPinMismatchToCloud(serverKey, serverLabel, agent) {
50821
50931
  try {
50822
50932
  const creds = getCredentials();
50823
50933
  if (!creds) return;
@@ -50826,18 +50936,18 @@ function reportPinMismatchToCloud(serverKey, agent) {
50826
50936
  { serverKey, reason: "tool-pin-mismatch" },
50827
50937
  "mcp-pin-mismatch",
50828
50938
  creds,
50829
- { mcpServer: serverKey, agent },
50939
+ { mcpServer: serverLabel, agent },
50830
50940
  void 0,
50831
50941
  false,
50832
50942
  {
50833
50943
  ruleName: "MCP tool definitions changed (possible rug pull)",
50834
- ruleDescription: `The MCP server "${serverKey}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
50944
+ ruleDescription: `The MCP server "${serverLabel}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
50835
50945
  }
50836
50946
  );
50837
50947
  } catch {
50838
50948
  }
50839
50949
  }
50840
- function reportInventoryToCloud(serverKey, toolCount, agent) {
50950
+ function reportInventoryToCloud(serverKey, serverLabel, toolCount, agent) {
50841
50951
  try {
50842
50952
  const creds = getCredentials();
50843
50953
  if (!creds) return;
@@ -50846,7 +50956,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50846
50956
  { serverKey, toolCount },
50847
50957
  "mcp-discovered",
50848
50958
  creds,
50849
- { mcpServer: serverKey, agent },
50959
+ { mcpServer: serverLabel, agent },
50850
50960
  void 0,
50851
50961
  false,
50852
50962
  { mcpToolCount: toolCount }
@@ -50854,7 +50964,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50854
50964
  } catch {
50855
50965
  }
50856
50966
  }
50857
- function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50967
+ function reportLargeResponseToCloud(serverKey, serverLabel, responseBytes, agent) {
50858
50968
  try {
50859
50969
  const creds = getCredentials();
50860
50970
  if (!creds) return;
@@ -50863,7 +50973,7 @@ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50863
50973
  { serverKey, responseBytes },
50864
50974
  "mcp-large-response",
50865
50975
  creds,
50866
- { mcpServer: serverKey, agent },
50976
+ { mcpServer: serverLabel, agent },
50867
50977
  void 0,
50868
50978
  false,
50869
50979
  { mcpResponseBytes: responseBytes }
@@ -51100,7 +51210,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51100
51210
  const currentHash = hashToolDefinitions(tools);
51101
51211
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
51102
51212
  const token = getInternalToken();
51103
- reportInventoryToCloud(serverKey, tools.length, clientName);
51213
+ reportInventoryToCloud(
51214
+ serverKey,
51215
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51216
+ tools.length,
51217
+ clientName
51218
+ );
51104
51219
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51105
51220
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
51106
51221
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -51172,7 +51287,11 @@ async function runMcpGateway(upstreamCommand, configName) {
51172
51287
  console.error(import_chalk21.default.red(" Session quarantined \u2014 all tool calls blocked."));
51173
51288
  console.error(import_chalk21.default.yellow(` Run: node9 mcp pin update ${serverKey}
51174
51289
  `));
51175
- reportPinMismatchToCloud(serverKey, clientName);
51290
+ reportPinMismatchToCloud(
51291
+ serverKey,
51292
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51293
+ clientName
51294
+ );
51176
51295
  const errorResponse = {
51177
51296
  jsonrpc: "2.0",
51178
51297
  id: parsed.id,
@@ -51218,7 +51337,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51218
51337
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
51219
51338
  )
51220
51339
  );
51221
- reportLargeResponseToCloud(serverKey, line.length, clientName);
51340
+ reportLargeResponseToCloud(
51341
+ serverKey,
51342
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51343
+ line.length,
51344
+ clientName
51345
+ );
51222
51346
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51223
51347
  const token = getInternalToken();
51224
51348
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
package/dist/cli.mjs CHANGED
@@ -983,6 +983,91 @@ function analyzeFsOperation(command) {
983
983
  fsOpCache.set(normalized, computed);
984
984
  return computed;
985
985
  }
986
+ function isSensitiveCleanupName(p) {
987
+ const base = p.replace(/^.*[\\/]/, "");
988
+ return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
989
+ }
990
+ function isWaivableCleanupTarget(p) {
991
+ if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
992
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
993
+ if (/[*?[{]/.test(p)) return false;
994
+ if (isSensitiveCleanupName(p)) return false;
995
+ return true;
996
+ }
997
+ function deriveRedirOp(sample) {
998
+ try {
999
+ const f = sharedParser.Parse(sample, "cmd");
1000
+ let op = -1;
1001
+ syntax.Walk(f, (node) => {
1002
+ const n = node;
1003
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
1004
+ return true;
1005
+ });
1006
+ return op;
1007
+ } catch {
1008
+ return -1;
1009
+ }
1010
+ }
1011
+ function collectSameCommandCreations(f) {
1012
+ const created = /* @__PURE__ */ new Set();
1013
+ try {
1014
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
1015
+ for (const stmt of stmts) {
1016
+ if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
1017
+ const redirs = stmt.Redirs || [];
1018
+ if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
1019
+ for (const r of redirs) {
1020
+ if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
1021
+ const w = resolveWordLiteral(r.Word);
1022
+ if (w) created.add(stripDotSlash(w));
1023
+ }
1024
+ }
1025
+ }
1026
+ } catch {
1027
+ return created;
1028
+ }
1029
+ return created;
1030
+ }
1031
+ function isRmCreatedInCommandCleanup(command) {
1032
+ if (!/\brm\b/.test(command)) return false;
1033
+ const f = parseShared(command);
1034
+ if (f === PARSE_FAIL) return false;
1035
+ const created = collectSameCommandCreations(f);
1036
+ if (created.size === 0) return false;
1037
+ let sawRm = false;
1038
+ let ok2 = true;
1039
+ try {
1040
+ syntax.Walk(f, (node) => {
1041
+ if (!node || !ok2) return false;
1042
+ const n = node;
1043
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1044
+ const args = n.Args || [];
1045
+ const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
1046
+ if (name !== "rm") return true;
1047
+ sawRm = true;
1048
+ const { flags, paths } = extractLiteralArgs(n);
1049
+ if (args.length - 1 > flags.length + paths.length) {
1050
+ ok2 = false;
1051
+ return false;
1052
+ }
1053
+ if (paths.length === 0) {
1054
+ ok2 = false;
1055
+ return false;
1056
+ }
1057
+ for (const p of paths) {
1058
+ const np = stripDotSlash(p);
1059
+ if (!created.has(np) || !isWaivableCleanupTarget(np)) {
1060
+ ok2 = false;
1061
+ return false;
1062
+ }
1063
+ }
1064
+ return true;
1065
+ });
1066
+ } catch {
1067
+ return false;
1068
+ }
1069
+ return sawRm && ok2;
1070
+ }
986
1071
  function analyzeFsOperationImpl(command) {
987
1072
  const f = parseShared(command);
988
1073
  if (f === PARSE_FAIL) return null;
@@ -1553,8 +1638,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1553
1638
  }
1554
1639
  }
1555
1640
  if (config.policy.smartRules.length > 0) {
1641
+ const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
1556
1642
  const matches = config.policy.smartRules.filter(
1557
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
1643
+ (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)
1558
1644
  );
1559
1645
  const matchedRule = resolvePinned(matches);
1560
1646
  if (matchedRule) {
@@ -2295,7 +2381,7 @@ function* stringValues(obj, depth = 0) {
2295
2381
  }
2296
2382
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2297
2383
  }
2298
- var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2384
+ var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2299
2385
  var init_dist = __esm({
2300
2386
  "packages/policy-engine/dist/index.mjs"() {
2301
2387
  "use strict";
@@ -2966,7 +3052,7 @@ var init_dist = __esm({
2966
3052
  "mongosh"
2967
3053
  ]);
2968
3054
  SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
2969
- CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
3055
+ CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
2970
3056
  COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
2971
3057
  "sudo",
2972
3058
  "doas",
@@ -3069,6 +3155,12 @@ var init_dist = __esm({
3069
3155
  };
3070
3156
  FS_OP_CACHE_MAX = 5e3;
3071
3157
  fsOpCache = /* @__PURE__ */ new Map();
3158
+ stripDotSlash = (p) => p.replace(/^\.\//, "");
3159
+ REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
3160
+ REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
3161
+ deriveRedirOp("cat <<X\nX"),
3162
+ deriveRedirOp("cat <<-X\nX")
3163
+ ]);
3072
3164
  DEFAULT_EGRESS_ALLOWLIST = [
3073
3165
  "*.github.com",
3074
3166
  "*.githubusercontent.com",
@@ -49205,9 +49297,10 @@ function loadGeminiCost(start, end, geminiTmpDir2) {
49205
49297
  function dimensionOfBlock(checkedBy, ruleName) {
49206
49298
  if (checkedBy.includes("egress")) return "network";
49207
49299
  if (checkedBy.includes("pii") || checkedBy.includes("dlp")) return "data";
49208
- if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "mcp";
49300
+ if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection"))
49301
+ return "detection";
49302
+ if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "apps";
49209
49303
  if (ruleName.startsWith("shield:project-jail")) return "files";
49210
- if (checkedBy === "loop-detected") return "toolRules";
49211
49304
  if (checkedBy === "timeout" || checkedBy === "local-decision") return "approvals";
49212
49305
  return "toolRules";
49213
49306
  }
@@ -49310,7 +49403,9 @@ function aggregateReportFromAudit(period, opts = {}) {
49310
49403
  let dimDataObserved = 0;
49311
49404
  let dimFilesBlocked = 0;
49312
49405
  let dimToolRulesBlocked = 0;
49313
- let dimMcpBlocked = 0;
49406
+ let dimAppsBlocked = 0;
49407
+ let dimPinMismatch = 0;
49408
+ let dimInjectionBlocked = 0;
49314
49409
  for (const e of entries) {
49315
49410
  if (superseded.has(supersedeKey(e))) continue;
49316
49411
  const allow = isAllow(e.decision);
@@ -49328,6 +49423,8 @@ function aggregateReportFromAudit(period, opts = {}) {
49328
49423
  }
49329
49424
  if (e.checkedBy === "loop-detected") loopHits++;
49330
49425
  const cb = e.checkedBy ?? "";
49426
+ if (cb === "mcp-pin-mismatch") dimPinMismatch++;
49427
+ if (cb.startsWith("injection")) dimInjectionBlocked++;
49331
49428
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
49332
49429
  dimDataObserved++;
49333
49430
  }
@@ -49339,8 +49436,8 @@ function aggregateReportFromAudit(period, opts = {}) {
49339
49436
  case "files":
49340
49437
  dimFilesBlocked++;
49341
49438
  break;
49342
- case "mcp":
49343
- dimMcpBlocked++;
49439
+ case "apps":
49440
+ dimAppsBlocked++;
49344
49441
  break;
49345
49442
  case "toolRules":
49346
49443
  dimToolRulesBlocked++;
@@ -49420,9 +49517,15 @@ function aggregateReportFromAudit(period, opts = {}) {
49420
49517
  dimensions: {
49421
49518
  network: { blocked: dimNetworkBlocked },
49422
49519
  data: { blocked: dlpBlocked, observed: dimDataObserved },
49520
+ detection: {
49521
+ loops: loopHits,
49522
+ pinMismatches: dimPinMismatch,
49523
+ injections: dimInjectionBlocked
49524
+ },
49423
49525
  approvals: { approved: userApproved, denied: userDenied, timedOut },
49424
49526
  files: { blocked: dimFilesBlocked },
49425
- toolRules: { blocked: dimToolRulesBlocked, mcp: dimMcpBlocked, loops: loopHits },
49527
+ toolRules: { blocked: dimToolRulesBlocked },
49528
+ apps: { blocked: dimAppsBlocked },
49426
49529
  cost: { totalUSD: claudeCost.total + codexCost.total + geminiCost.total }
49427
49530
  },
49428
49531
  generatedAt: now.toISOString()
@@ -49722,6 +49825,12 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
49722
49825
  d.data.blocked + d.data.observed > 0,
49723
49826
  `${num2(d.data.blocked)} blocked \xB7 ${num2(d.data.observed)} observed (DLP/PII)`
49724
49827
  );
49828
+ dimRow(
49829
+ "\u{1F501}",
49830
+ "Detection",
49831
+ d.detection.loops + d.detection.pinMismatches + d.detection.injections > 0,
49832
+ `${num2(d.detection.loops)} loops \xB7 ${num2(d.detection.pinMismatches)} pin \xB7 ${num2(d.detection.injections)} injection`
49833
+ );
49725
49834
  dimRow(
49726
49835
  "\u270B",
49727
49836
  "Approvals",
@@ -49732,9 +49841,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
49732
49841
  dimRow(
49733
49842
  "\u{1F6E0}",
49734
49843
  "Tool rules",
49735
- d.toolRules.blocked + d.toolRules.mcp + d.toolRules.loops > 0,
49736
- `${num2(d.toolRules.blocked)} shields/rules \xB7 ${num2(d.toolRules.mcp)} MCP \xB7 ${num2(d.toolRules.loops)} loops`
49844
+ d.toolRules.blocked > 0,
49845
+ `${num2(d.toolRules.blocked)} shields/rules`
49737
49846
  );
49847
+ dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
49738
49848
  dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
49739
49849
  }
49740
49850
  console.log("");
@@ -50810,7 +50920,7 @@ function normalizeClientName(name) {
50810
50920
  const sanitized = sanitize4(name).slice(0, 40);
50811
50921
  return sanitized.length > 0 ? sanitized : void 0;
50812
50922
  }
50813
- function reportPinMismatchToCloud(serverKey, agent) {
50923
+ function reportPinMismatchToCloud(serverKey, serverLabel, agent) {
50814
50924
  try {
50815
50925
  const creds = getCredentials();
50816
50926
  if (!creds) return;
@@ -50819,18 +50929,18 @@ function reportPinMismatchToCloud(serverKey, agent) {
50819
50929
  { serverKey, reason: "tool-pin-mismatch" },
50820
50930
  "mcp-pin-mismatch",
50821
50931
  creds,
50822
- { mcpServer: serverKey, agent },
50932
+ { mcpServer: serverLabel, agent },
50823
50933
  void 0,
50824
50934
  false,
50825
50935
  {
50826
50936
  ruleName: "MCP tool definitions changed (possible rug pull)",
50827
- ruleDescription: `The MCP server "${serverKey}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
50937
+ ruleDescription: `The MCP server "${serverLabel}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
50828
50938
  }
50829
50939
  );
50830
50940
  } catch {
50831
50941
  }
50832
50942
  }
50833
- function reportInventoryToCloud(serverKey, toolCount, agent) {
50943
+ function reportInventoryToCloud(serverKey, serverLabel, toolCount, agent) {
50834
50944
  try {
50835
50945
  const creds = getCredentials();
50836
50946
  if (!creds) return;
@@ -50839,7 +50949,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50839
50949
  { serverKey, toolCount },
50840
50950
  "mcp-discovered",
50841
50951
  creds,
50842
- { mcpServer: serverKey, agent },
50952
+ { mcpServer: serverLabel, agent },
50843
50953
  void 0,
50844
50954
  false,
50845
50955
  { mcpToolCount: toolCount }
@@ -50847,7 +50957,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50847
50957
  } catch {
50848
50958
  }
50849
50959
  }
50850
- function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50960
+ function reportLargeResponseToCloud(serverKey, serverLabel, responseBytes, agent) {
50851
50961
  try {
50852
50962
  const creds = getCredentials();
50853
50963
  if (!creds) return;
@@ -50856,7 +50966,7 @@ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50856
50966
  { serverKey, responseBytes },
50857
50967
  "mcp-large-response",
50858
50968
  creds,
50859
- { mcpServer: serverKey, agent },
50969
+ { mcpServer: serverLabel, agent },
50860
50970
  void 0,
50861
50971
  false,
50862
50972
  { mcpResponseBytes: responseBytes }
@@ -51093,7 +51203,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51093
51203
  const currentHash = hashToolDefinitions(tools);
51094
51204
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
51095
51205
  const token = getInternalToken();
51096
- reportInventoryToCloud(serverKey, tools.length, clientName);
51206
+ reportInventoryToCloud(
51207
+ serverKey,
51208
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51209
+ tools.length,
51210
+ clientName
51211
+ );
51097
51212
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51098
51213
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
51099
51214
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -51165,7 +51280,11 @@ async function runMcpGateway(upstreamCommand, configName) {
51165
51280
  console.error(chalk21.red(" Session quarantined \u2014 all tool calls blocked."));
51166
51281
  console.error(chalk21.yellow(` Run: node9 mcp pin update ${serverKey}
51167
51282
  `));
51168
- reportPinMismatchToCloud(serverKey, clientName);
51283
+ reportPinMismatchToCloud(
51284
+ serverKey,
51285
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51286
+ clientName
51287
+ );
51169
51288
  const errorResponse = {
51170
51289
  jsonrpc: "2.0",
51171
51290
  id: parsed.id,
@@ -51211,7 +51330,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51211
51330
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
51212
51331
  )
51213
51332
  );
51214
- reportLargeResponseToCloud(serverKey, line.length, clientName);
51333
+ reportLargeResponseToCloud(
51334
+ serverKey,
51335
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51336
+ line.length,
51337
+ clientName
51338
+ );
51215
51339
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51216
51340
  const token = getInternalToken();
51217
51341
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
@@ -443,6 +443,20 @@ function analyzeFsOperation(command) {
443
443
  fsOpCache.set(normalized, computed);
444
444
  return computed;
445
445
  }
446
+ function deriveRedirOp(sample) {
447
+ try {
448
+ const f = sharedParser.Parse(sample, "cmd");
449
+ let op = -1;
450
+ syntax.Walk(f, (node) => {
451
+ const n = node;
452
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
453
+ return true;
454
+ });
455
+ return op;
456
+ } catch {
457
+ return -1;
458
+ }
459
+ }
446
460
  function analyzeFsOperationImpl(command) {
447
461
  const f = parseShared(command);
448
462
  if (f === PARSE_FAIL) return null;
@@ -655,7 +669,7 @@ function assertBuiltinShieldRegexesAreSafe() {
655
669
  }
656
670
  }
657
671
  }
658
- var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, COST_PER_LOOP_ITER_USD, LONG_OUTPUT_THRESHOLD_BYTES;
672
+ var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, COST_PER_LOOP_ITER_USD, LONG_OUTPUT_THRESHOLD_BYTES;
659
673
  var init_dist = __esm({
660
674
  "packages/policy-engine/dist/index.mjs"() {
661
675
  "use strict";
@@ -1273,6 +1287,11 @@ var init_dist = __esm({
1273
1287
  ]);
1274
1288
  FS_OP_CACHE_MAX = 5e3;
1275
1289
  fsOpCache = /* @__PURE__ */ new Map();
1290
+ REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
1291
+ REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
1292
+ deriveRedirOp("cat <<X\nX"),
1293
+ deriveRedirOp("cat <<-X\nX")
1294
+ ]);
1276
1295
  MAX_REGEX_LENGTH = 100;
1277
1296
  REGEX_CACHE_MAX = 500;
1278
1297
  regexCache = /* @__PURE__ */ new Map();
@@ -3438,9 +3457,10 @@ async function loadGeminiCostAsync(start, end, geminiTmpDir) {
3438
3457
  function dimensionOfBlock(checkedBy, ruleName) {
3439
3458
  if (checkedBy.includes("egress")) return "network";
3440
3459
  if (checkedBy.includes("pii") || checkedBy.includes("dlp")) return "data";
3441
- if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "mcp";
3460
+ if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection"))
3461
+ return "detection";
3462
+ if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "apps";
3442
3463
  if (ruleName.startsWith("shield:project-jail")) return "files";
3443
- if (checkedBy === "loop-detected") return "toolRules";
3444
3464
  if (checkedBy === "timeout" || checkedBy === "local-decision") return "approvals";
3445
3465
  return "toolRules";
3446
3466
  }
@@ -3543,7 +3563,9 @@ function aggregateReportFromAudit(period, opts = {}) {
3543
3563
  let dimDataObserved = 0;
3544
3564
  let dimFilesBlocked = 0;
3545
3565
  let dimToolRulesBlocked = 0;
3546
- let dimMcpBlocked = 0;
3566
+ let dimAppsBlocked = 0;
3567
+ let dimPinMismatch = 0;
3568
+ let dimInjectionBlocked = 0;
3547
3569
  for (const e of entries) {
3548
3570
  if (superseded.has(supersedeKey(e))) continue;
3549
3571
  const allow = isAllow(e.decision);
@@ -3561,6 +3583,8 @@ function aggregateReportFromAudit(period, opts = {}) {
3561
3583
  }
3562
3584
  if (e.checkedBy === "loop-detected") loopHits++;
3563
3585
  const cb = e.checkedBy ?? "";
3586
+ if (cb === "mcp-pin-mismatch") dimPinMismatch++;
3587
+ if (cb.startsWith("injection")) dimInjectionBlocked++;
3564
3588
  if (cb.includes("would-block") && (cb.includes("pii") || cb.includes("dlp"))) {
3565
3589
  dimDataObserved++;
3566
3590
  }
@@ -3572,8 +3596,8 @@ function aggregateReportFromAudit(period, opts = {}) {
3572
3596
  case "files":
3573
3597
  dimFilesBlocked++;
3574
3598
  break;
3575
- case "mcp":
3576
- dimMcpBlocked++;
3599
+ case "apps":
3600
+ dimAppsBlocked++;
3577
3601
  break;
3578
3602
  case "toolRules":
3579
3603
  dimToolRulesBlocked++;
@@ -3653,9 +3677,15 @@ function aggregateReportFromAudit(period, opts = {}) {
3653
3677
  dimensions: {
3654
3678
  network: { blocked: dimNetworkBlocked },
3655
3679
  data: { blocked: dlpBlocked, observed: dimDataObserved },
3680
+ detection: {
3681
+ loops: loopHits,
3682
+ pinMismatches: dimPinMismatch,
3683
+ injections: dimInjectionBlocked
3684
+ },
3656
3685
  approvals: { approved: userApproved, denied: userDenied, timedOut },
3657
3686
  files: { blocked: dimFilesBlocked },
3658
- toolRules: { blocked: dimToolRulesBlocked, mcp: dimMcpBlocked, loops: loopHits },
3687
+ toolRules: { blocked: dimToolRulesBlocked },
3688
+ apps: { blocked: dimAppsBlocked },
3659
3689
  cost: { totalUSD: claudeCost.total + codexCost.total + geminiCost.total }
3660
3690
  },
3661
3691
  generatedAt: now.toISOString()
package/dist/index.js CHANGED
@@ -1351,7 +1351,7 @@ function analyzeSqlDestructive(command) {
1351
1351
  description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
1352
1352
  };
1353
1353
  }
1354
- var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
1354
+ var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
1355
1355
  var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
1356
1356
  "sudo",
1357
1357
  "doas",
@@ -1671,6 +1671,97 @@ function analyzeFsOperation(command) {
1671
1671
  fsOpCache.set(normalized, computed);
1672
1672
  return computed;
1673
1673
  }
1674
+ var stripDotSlash = (p) => p.replace(/^\.\//, "");
1675
+ function isSensitiveCleanupName(p) {
1676
+ const base = p.replace(/^.*[\\/]/, "");
1677
+ return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
1678
+ }
1679
+ function isWaivableCleanupTarget(p) {
1680
+ if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
1681
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
1682
+ if (/[*?[{]/.test(p)) return false;
1683
+ if (isSensitiveCleanupName(p)) return false;
1684
+ return true;
1685
+ }
1686
+ function deriveRedirOp(sample) {
1687
+ try {
1688
+ const f = sharedParser.Parse(sample, "cmd");
1689
+ let op = -1;
1690
+ syntax.Walk(f, (node) => {
1691
+ const n = node;
1692
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
1693
+ return true;
1694
+ });
1695
+ return op;
1696
+ } catch {
1697
+ return -1;
1698
+ }
1699
+ }
1700
+ var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
1701
+ var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
1702
+ deriveRedirOp("cat <<X\nX"),
1703
+ deriveRedirOp("cat <<-X\nX")
1704
+ ]);
1705
+ function collectSameCommandCreations(f) {
1706
+ const created = /* @__PURE__ */ new Set();
1707
+ try {
1708
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
1709
+ for (const stmt of stmts) {
1710
+ if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
1711
+ const redirs = stmt.Redirs || [];
1712
+ if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
1713
+ for (const r of redirs) {
1714
+ if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
1715
+ const w = resolveWordLiteral(r.Word);
1716
+ if (w) created.add(stripDotSlash(w));
1717
+ }
1718
+ }
1719
+ }
1720
+ } catch {
1721
+ return created;
1722
+ }
1723
+ return created;
1724
+ }
1725
+ function isRmCreatedInCommandCleanup(command) {
1726
+ if (!/\brm\b/.test(command)) return false;
1727
+ const f = parseShared(command);
1728
+ if (f === PARSE_FAIL) return false;
1729
+ const created = collectSameCommandCreations(f);
1730
+ if (created.size === 0) return false;
1731
+ let sawRm = false;
1732
+ let ok = true;
1733
+ try {
1734
+ syntax.Walk(f, (node) => {
1735
+ if (!node || !ok) return false;
1736
+ const n = node;
1737
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1738
+ const args = n.Args || [];
1739
+ const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
1740
+ if (name !== "rm") return true;
1741
+ sawRm = true;
1742
+ const { flags, paths } = extractLiteralArgs(n);
1743
+ if (args.length - 1 > flags.length + paths.length) {
1744
+ ok = false;
1745
+ return false;
1746
+ }
1747
+ if (paths.length === 0) {
1748
+ ok = false;
1749
+ return false;
1750
+ }
1751
+ for (const p of paths) {
1752
+ const np = stripDotSlash(p);
1753
+ if (!created.has(np) || !isWaivableCleanupTarget(np)) {
1754
+ ok = false;
1755
+ return false;
1756
+ }
1757
+ }
1758
+ return true;
1759
+ });
1760
+ } catch {
1761
+ return false;
1762
+ }
1763
+ return sawRm && ok;
1764
+ }
1674
1765
  function analyzeFsOperationImpl(command) {
1675
1766
  const f = parseShared(command);
1676
1767
  if (f === PARSE_FAIL) return null;
@@ -2398,8 +2489,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2398
2489
  }
2399
2490
  }
2400
2491
  if (config.policy.smartRules.length > 0) {
2492
+ const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2401
2493
  const matches = config.policy.smartRules.filter(
2402
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
2494
+ (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)
2403
2495
  );
2404
2496
  const matchedRule = resolvePinned(matches);
2405
2497
  if (matchedRule) {
package/dist/index.mjs CHANGED
@@ -1321,7 +1321,7 @@ function analyzeSqlDestructive(command) {
1321
1321
  description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
1322
1322
  };
1323
1323
  }
1324
- var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
1324
+ var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
1325
1325
  var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
1326
1326
  "sudo",
1327
1327
  "doas",
@@ -1641,6 +1641,97 @@ function analyzeFsOperation(command) {
1641
1641
  fsOpCache.set(normalized, computed);
1642
1642
  return computed;
1643
1643
  }
1644
+ var stripDotSlash = (p) => p.replace(/^\.\//, "");
1645
+ function isSensitiveCleanupName(p) {
1646
+ const base = p.replace(/^.*[\\/]/, "");
1647
+ return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
1648
+ }
1649
+ function isWaivableCleanupTarget(p) {
1650
+ if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
1651
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
1652
+ if (/[*?[{]/.test(p)) return false;
1653
+ if (isSensitiveCleanupName(p)) return false;
1654
+ return true;
1655
+ }
1656
+ function deriveRedirOp(sample) {
1657
+ try {
1658
+ const f = sharedParser.Parse(sample, "cmd");
1659
+ let op = -1;
1660
+ syntax.Walk(f, (node) => {
1661
+ const n = node;
1662
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
1663
+ return true;
1664
+ });
1665
+ return op;
1666
+ } catch {
1667
+ return -1;
1668
+ }
1669
+ }
1670
+ var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
1671
+ var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
1672
+ deriveRedirOp("cat <<X\nX"),
1673
+ deriveRedirOp("cat <<-X\nX")
1674
+ ]);
1675
+ function collectSameCommandCreations(f) {
1676
+ const created = /* @__PURE__ */ new Set();
1677
+ try {
1678
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
1679
+ for (const stmt of stmts) {
1680
+ if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
1681
+ const redirs = stmt.Redirs || [];
1682
+ if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
1683
+ for (const r of redirs) {
1684
+ if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
1685
+ const w = resolveWordLiteral(r.Word);
1686
+ if (w) created.add(stripDotSlash(w));
1687
+ }
1688
+ }
1689
+ }
1690
+ } catch {
1691
+ return created;
1692
+ }
1693
+ return created;
1694
+ }
1695
+ function isRmCreatedInCommandCleanup(command) {
1696
+ if (!/\brm\b/.test(command)) return false;
1697
+ const f = parseShared(command);
1698
+ if (f === PARSE_FAIL) return false;
1699
+ const created = collectSameCommandCreations(f);
1700
+ if (created.size === 0) return false;
1701
+ let sawRm = false;
1702
+ let ok = true;
1703
+ try {
1704
+ syntax.Walk(f, (node) => {
1705
+ if (!node || !ok) return false;
1706
+ const n = node;
1707
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1708
+ const args = n.Args || [];
1709
+ const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
1710
+ if (name !== "rm") return true;
1711
+ sawRm = true;
1712
+ const { flags, paths } = extractLiteralArgs(n);
1713
+ if (args.length - 1 > flags.length + paths.length) {
1714
+ ok = false;
1715
+ return false;
1716
+ }
1717
+ if (paths.length === 0) {
1718
+ ok = false;
1719
+ return false;
1720
+ }
1721
+ for (const p of paths) {
1722
+ const np = stripDotSlash(p);
1723
+ if (!created.has(np) || !isWaivableCleanupTarget(np)) {
1724
+ ok = false;
1725
+ return false;
1726
+ }
1727
+ }
1728
+ return true;
1729
+ });
1730
+ } catch {
1731
+ return false;
1732
+ }
1733
+ return sawRm && ok;
1734
+ }
1644
1735
  function analyzeFsOperationImpl(command) {
1645
1736
  const f = parseShared(command);
1646
1737
  if (f === PARSE_FAIL) return null;
@@ -2368,8 +2459,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2368
2459
  }
2369
2460
  }
2370
2461
  if (config.policy.smartRules.length > 0) {
2462
+ const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2371
2463
  const matches = config.policy.smartRules.filter(
2372
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
2464
+ (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)
2373
2465
  );
2374
2466
  const matchedRule = resolvePinned(matches);
2375
2467
  if (matchedRule) {
package/dist/scan-ink.mjs CHANGED
@@ -818,6 +818,25 @@ function assertBuiltinPatternsAreSafe() {
818
818
  assertBuiltinPatternsAreSafe();
819
819
  var { syntax } = mvdanSh;
820
820
  var sharedParser = syntax.NewParser();
821
+ function deriveRedirOp(sample) {
822
+ try {
823
+ const f = sharedParser.Parse(sample, "cmd");
824
+ let op = -1;
825
+ syntax.Walk(f, (node) => {
826
+ const n = node;
827
+ if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
828
+ return true;
829
+ });
830
+ return op;
831
+ } catch {
832
+ return -1;
833
+ }
834
+ }
835
+ var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
836
+ var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
837
+ deriveRedirOp("cat <<X\nX"),
838
+ deriveRedirOp("cat <<-X\nX")
839
+ ]);
821
840
  var aws_default = {
822
841
  name: "aws",
823
842
  description: "Protects AWS infrastructure from destructive AI operations",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.60.0",
3
+ "version": "1.61.1",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",