@node9/proxy 1.61.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",
@@ -50835,7 +50927,7 @@ function normalizeClientName(name) {
50835
50927
  const sanitized = sanitize4(name).slice(0, 40);
50836
50928
  return sanitized.length > 0 ? sanitized : void 0;
50837
50929
  }
50838
- function reportPinMismatchToCloud(serverKey, agent) {
50930
+ function reportPinMismatchToCloud(serverKey, serverLabel, agent) {
50839
50931
  try {
50840
50932
  const creds = getCredentials();
50841
50933
  if (!creds) return;
@@ -50844,18 +50936,18 @@ function reportPinMismatchToCloud(serverKey, agent) {
50844
50936
  { serverKey, reason: "tool-pin-mismatch" },
50845
50937
  "mcp-pin-mismatch",
50846
50938
  creds,
50847
- { mcpServer: serverKey, agent },
50939
+ { mcpServer: serverLabel, agent },
50848
50940
  void 0,
50849
50941
  false,
50850
50942
  {
50851
50943
  ruleName: "MCP tool definitions changed (possible rug pull)",
50852
- 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}`
50853
50945
  }
50854
50946
  );
50855
50947
  } catch {
50856
50948
  }
50857
50949
  }
50858
- function reportInventoryToCloud(serverKey, toolCount, agent) {
50950
+ function reportInventoryToCloud(serverKey, serverLabel, toolCount, agent) {
50859
50951
  try {
50860
50952
  const creds = getCredentials();
50861
50953
  if (!creds) return;
@@ -50864,7 +50956,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50864
50956
  { serverKey, toolCount },
50865
50957
  "mcp-discovered",
50866
50958
  creds,
50867
- { mcpServer: serverKey, agent },
50959
+ { mcpServer: serverLabel, agent },
50868
50960
  void 0,
50869
50961
  false,
50870
50962
  { mcpToolCount: toolCount }
@@ -50872,7 +50964,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50872
50964
  } catch {
50873
50965
  }
50874
50966
  }
50875
- function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50967
+ function reportLargeResponseToCloud(serverKey, serverLabel, responseBytes, agent) {
50876
50968
  try {
50877
50969
  const creds = getCredentials();
50878
50970
  if (!creds) return;
@@ -50881,7 +50973,7 @@ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50881
50973
  { serverKey, responseBytes },
50882
50974
  "mcp-large-response",
50883
50975
  creds,
50884
- { mcpServer: serverKey, agent },
50976
+ { mcpServer: serverLabel, agent },
50885
50977
  void 0,
50886
50978
  false,
50887
50979
  { mcpResponseBytes: responseBytes }
@@ -51118,7 +51210,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51118
51210
  const currentHash = hashToolDefinitions(tools);
51119
51211
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
51120
51212
  const token = getInternalToken();
51121
- reportInventoryToCloud(serverKey, tools.length, clientName);
51213
+ reportInventoryToCloud(
51214
+ serverKey,
51215
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51216
+ tools.length,
51217
+ clientName
51218
+ );
51122
51219
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51123
51220
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
51124
51221
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -51190,7 +51287,11 @@ async function runMcpGateway(upstreamCommand, configName) {
51190
51287
  console.error(import_chalk21.default.red(" Session quarantined \u2014 all tool calls blocked."));
51191
51288
  console.error(import_chalk21.default.yellow(` Run: node9 mcp pin update ${serverKey}
51192
51289
  `));
51193
- reportPinMismatchToCloud(serverKey, clientName);
51290
+ reportPinMismatchToCloud(
51291
+ serverKey,
51292
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51293
+ clientName
51294
+ );
51194
51295
  const errorResponse = {
51195
51296
  jsonrpc: "2.0",
51196
51297
  id: parsed.id,
@@ -51236,7 +51337,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51236
51337
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
51237
51338
  )
51238
51339
  );
51239
- reportLargeResponseToCloud(serverKey, line.length, clientName);
51340
+ reportLargeResponseToCloud(
51341
+ serverKey,
51342
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51343
+ line.length,
51344
+ clientName
51345
+ );
51240
51346
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51241
51347
  const token = getInternalToken();
51242
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",
@@ -50828,7 +50920,7 @@ function normalizeClientName(name) {
50828
50920
  const sanitized = sanitize4(name).slice(0, 40);
50829
50921
  return sanitized.length > 0 ? sanitized : void 0;
50830
50922
  }
50831
- function reportPinMismatchToCloud(serverKey, agent) {
50923
+ function reportPinMismatchToCloud(serverKey, serverLabel, agent) {
50832
50924
  try {
50833
50925
  const creds = getCredentials();
50834
50926
  if (!creds) return;
@@ -50837,18 +50929,18 @@ function reportPinMismatchToCloud(serverKey, agent) {
50837
50929
  { serverKey, reason: "tool-pin-mismatch" },
50838
50930
  "mcp-pin-mismatch",
50839
50931
  creds,
50840
- { mcpServer: serverKey, agent },
50932
+ { mcpServer: serverLabel, agent },
50841
50933
  void 0,
50842
50934
  false,
50843
50935
  {
50844
50936
  ruleName: "MCP tool definitions changed (possible rug pull)",
50845
- 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}`
50846
50938
  }
50847
50939
  );
50848
50940
  } catch {
50849
50941
  }
50850
50942
  }
50851
- function reportInventoryToCloud(serverKey, toolCount, agent) {
50943
+ function reportInventoryToCloud(serverKey, serverLabel, toolCount, agent) {
50852
50944
  try {
50853
50945
  const creds = getCredentials();
50854
50946
  if (!creds) return;
@@ -50857,7 +50949,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50857
50949
  { serverKey, toolCount },
50858
50950
  "mcp-discovered",
50859
50951
  creds,
50860
- { mcpServer: serverKey, agent },
50952
+ { mcpServer: serverLabel, agent },
50861
50953
  void 0,
50862
50954
  false,
50863
50955
  { mcpToolCount: toolCount }
@@ -50865,7 +50957,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
50865
50957
  } catch {
50866
50958
  }
50867
50959
  }
50868
- function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50960
+ function reportLargeResponseToCloud(serverKey, serverLabel, responseBytes, agent) {
50869
50961
  try {
50870
50962
  const creds = getCredentials();
50871
50963
  if (!creds) return;
@@ -50874,7 +50966,7 @@ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
50874
50966
  { serverKey, responseBytes },
50875
50967
  "mcp-large-response",
50876
50968
  creds,
50877
- { mcpServer: serverKey, agent },
50969
+ { mcpServer: serverLabel, agent },
50878
50970
  void 0,
50879
50971
  false,
50880
50972
  { mcpResponseBytes: responseBytes }
@@ -51111,7 +51203,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51111
51203
  const currentHash = hashToolDefinitions(tools);
51112
51204
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
51113
51205
  const token = getInternalToken();
51114
- reportInventoryToCloud(serverKey, tools.length, clientName);
51206
+ reportInventoryToCloud(
51207
+ serverKey,
51208
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51209
+ tools.length,
51210
+ clientName
51211
+ );
51115
51212
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51116
51213
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
51117
51214
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -51183,7 +51280,11 @@ async function runMcpGateway(upstreamCommand, configName) {
51183
51280
  console.error(chalk21.red(" Session quarantined \u2014 all tool calls blocked."));
51184
51281
  console.error(chalk21.yellow(` Run: node9 mcp pin update ${serverKey}
51185
51282
  `));
51186
- reportPinMismatchToCloud(serverKey, clientName);
51283
+ reportPinMismatchToCloud(
51284
+ serverKey,
51285
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51286
+ clientName
51287
+ );
51187
51288
  const errorResponse = {
51188
51289
  jsonrpc: "2.0",
51189
51290
  id: parsed.id,
@@ -51229,7 +51330,12 @@ async function runMcpGateway(upstreamCommand, configName) {
51229
51330
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
51230
51331
  )
51231
51332
  );
51232
- reportLargeResponseToCloud(serverKey, line.length, clientName);
51333
+ reportLargeResponseToCloud(
51334
+ serverKey,
51335
+ resolveServerLabel("", serverKey, upstreamCommand, configName),
51336
+ line.length,
51337
+ clientName
51338
+ );
51233
51339
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
51234
51340
  const token = getInternalToken();
51235
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();
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.61.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",