@node9/proxy 2.37.0 → 2.38.0

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.
@@ -104,13 +104,99 @@ import picomatch from "picomatch";
104
104
  import safeRegex from "safe-regex2";
105
105
  import pm from "picomatch";
106
106
  import mvdanSh from "mvdan-sh";
107
- import safeRegex2 from "safe-regex2";
108
107
  import mvdanSh2 from "mvdan-sh";
109
- import safeRegex3 from "safe-regex2";
108
+ import safeRegex2 from "safe-regex2";
110
109
  import { createHash as createHash2 } from "crypto";
111
110
  import mvdanSh3 from "mvdan-sh";
112
111
  import { createHash as createHash3 } from "crypto";
113
112
  import mvdanSh4 from "mvdan-sh";
113
+ function validateRegex(pattern) {
114
+ if (!pattern) return "Pattern is required";
115
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
116
+ try {
117
+ new RegExp(pattern);
118
+ } catch (e) {
119
+ return `Invalid regex syntax: ${e.message}`;
120
+ }
121
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
122
+ if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
123
+ return null;
124
+ }
125
+ function getCompiledRegex(pattern, flags = "") {
126
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
127
+ const key = `${pattern}\0${flags}`;
128
+ if (regexCache.has(key)) {
129
+ const cached = regexCache.get(key);
130
+ regexCache.delete(key);
131
+ regexCache.set(key, cached);
132
+ return cached;
133
+ }
134
+ if (validateRegex(pattern) !== null) return null;
135
+ try {
136
+ const re = new RegExp(pattern, flags);
137
+ if (regexCache.size >= REGEX_CACHE_MAX) {
138
+ const oldest = regexCache.keys().next().value;
139
+ if (oldest) regexCache.delete(oldest);
140
+ }
141
+ regexCache.set(key, re);
142
+ return re;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ function isToolRule(rule) {
148
+ return rule.tool.startsWith("{,mcp__*__}");
149
+ }
150
+ function actionPart(ruleName) {
151
+ const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
152
+ return m ? m[1] : ruleName ?? "rule";
153
+ }
154
+ function ownReason(reason, owner) {
155
+ if (!reason) return reason;
156
+ return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
157
+ }
158
+ function buildAppShield(spec) {
159
+ const owner = spec.label;
160
+ const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
161
+ ...rule,
162
+ name: `shield:${spec.name}:${actionPart(rule.name)}`,
163
+ server: spec.server,
164
+ reason: ownReason(rule.reason, owner)
165
+ }));
166
+ const ownTools = (spec.mcpTools ?? []).map((rule) => ({
167
+ ...rule,
168
+ server: spec.server
169
+ }));
170
+ return {
171
+ name: spec.name,
172
+ description: spec.description,
173
+ aliases: [],
174
+ smartRules: [...toolRules, ...ownTools, ...spec.doors],
175
+ dangerousWords: []
176
+ };
177
+ }
178
+ function buildGeneralRules() {
179
+ const out = [];
180
+ for (const name of Object.keys(ACTION_TEMPLATES)) {
181
+ const { slug: slug2, label } = ACTION_CHECKS[name];
182
+ for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
183
+ const part = actionPart(rule.name).replace(/^review-/, "");
184
+ out.push({
185
+ ...rule,
186
+ name: `review-action-${slug2}-${part}`,
187
+ notServer: APP_SERVER_GLOB,
188
+ reason: ownReason(rule.reason, label)
189
+ });
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+ function generalActionRuleNames(template) {
195
+ const { slug: slug2 } = ACTION_CHECKS[template];
196
+ return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug2}-`)).map(
197
+ (r) => r.name
198
+ );
199
+ }
114
200
  function isShieldVerdict(v) {
115
201
  return v === "allow" || v === "review" || v === "block";
116
202
  }
@@ -148,16 +234,25 @@ function validateOverrides(raw) {
148
234
  return { overrides: result, warnings };
149
235
  }
150
236
  function assertBuiltinShieldRegexesAreSafe() {
151
- for (const shield of Object.values(BUILTIN_SHIELDS)) {
237
+ const sets = [
238
+ ...Object.values(BUILTIN_SHIELDS).map((sh) => ({
239
+ name: sh.name,
240
+ smartRules: sh.smartRules
241
+ })),
242
+ ...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
243
+ { name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
244
+ ];
245
+ for (const shield of sets) {
152
246
  for (const rule of shield.smartRules) {
153
247
  const conditions = rule.conditions ?? [];
154
248
  for (const cond of conditions) {
155
249
  if (cond.op !== "matches" && cond.op !== "notMatches") continue;
156
250
  const pattern = cond.value;
157
251
  if (!pattern) continue;
158
- if (!safeRegex(pattern)) {
252
+ const problem = validateRegex(pattern);
253
+ if (problem) {
159
254
  throw new Error(
160
- `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
255
+ `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
161
256
  );
162
257
  }
163
258
  }
@@ -486,6 +581,55 @@ function cachedNormalize(command, compute) {
486
581
  function normalizeCommandForPolicy(command) {
487
582
  return commandReadingsImpl(command).posix;
488
583
  }
584
+ function fileAccessView(command) {
585
+ const hit = accessViewCache.get(command);
586
+ if (hit !== void 0) return hit;
587
+ const view = fileAccessViewImpl(command);
588
+ if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
589
+ const oldest = accessViewCache.keys().next().value;
590
+ if (oldest !== void 0) accessViewCache.delete(oldest);
591
+ }
592
+ accessViewCache.set(command, view);
593
+ return view;
594
+ }
595
+ function fileAccessViewImpl(command) {
596
+ const text = command.replace(INTERPRETER_LINE, "");
597
+ const f = parseShared(text);
598
+ if (f === PARSE_FAIL) return text;
599
+ const toCharIndex = byteOffsetToCharIndex(text);
600
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
601
+ const edits = [];
602
+ try {
603
+ syntax.Walk(f, (node) => {
604
+ if (!node) return false;
605
+ const n = node;
606
+ if (syntax.NodeType(n) !== "CallExpr") return true;
607
+ const args = n.Args || [];
608
+ const words = args.map((a) => resolveWordLiteral(a));
609
+ const head = unwrapCommandHead(words);
610
+ for (const i of /* @__PURE__ */ new Set([0, head])) {
611
+ const word = words[i];
612
+ if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
613
+ continue;
614
+ }
615
+ if (!PROGRAM_DIR.test(word)) continue;
616
+ const name = word.split(/[\\/]/).pop() ?? "";
617
+ if (!name) continue;
618
+ const s = at(args[i].Pos().Offset());
619
+ const e = at(args[i].End().Offset());
620
+ if (s < 0 || e < 0) continue;
621
+ edits.push([s, e, name]);
622
+ }
623
+ return true;
624
+ });
625
+ } catch {
626
+ return text;
627
+ }
628
+ edits.sort((a, b) => b[0] - a[0]);
629
+ let out = text;
630
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
631
+ return out;
632
+ }
489
633
  function commandReadings(command) {
490
634
  const r = commandReadingsImpl(command);
491
635
  const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
@@ -1728,40 +1872,6 @@ function jailedRedirectRead(stmt) {
1728
1872
  }
1729
1873
  return null;
1730
1874
  }
1731
- function validateRegex(pattern) {
1732
- if (!pattern) return "Pattern is required";
1733
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
1734
- try {
1735
- new RegExp(pattern);
1736
- } catch (e) {
1737
- return `Invalid regex syntax: ${e.message}`;
1738
- }
1739
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
1740
- if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
1741
- return null;
1742
- }
1743
- function getCompiledRegex(pattern, flags = "") {
1744
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
1745
- const key = `${pattern}\0${flags}`;
1746
- if (regexCache.has(key)) {
1747
- const cached = regexCache.get(key);
1748
- regexCache.delete(key);
1749
- regexCache.set(key, cached);
1750
- return cached;
1751
- }
1752
- if (validateRegex(pattern) !== null) return null;
1753
- try {
1754
- const re = new RegExp(pattern, flags);
1755
- if (regexCache.size >= REGEX_CACHE_MAX) {
1756
- const oldest = regexCache.keys().next().value;
1757
- if (oldest) regexCache.delete(oldest);
1758
- }
1759
- regexCache.set(key, re);
1760
- return re;
1761
- } catch {
1762
- return null;
1763
- }
1764
- }
1765
1875
  function matchesPattern(text, patterns) {
1766
1876
  const p = Array.isArray(patterns) ? patterns : [patterns];
1767
1877
  if (p.length === 0) return false;
@@ -1794,23 +1904,26 @@ function evaluateSmartConditions(args, rule) {
1794
1904
  return val;
1795
1905
  };
1796
1906
  const readingsCache = /* @__PURE__ */ new Map();
1797
- const resolveFieldReadings = (field) => {
1798
- const cached = readingsCache.get(field);
1907
+ const resolveFieldReadings = (field, reading) => {
1908
+ const access = reading === "access" && field === "command";
1909
+ const key = access ? `${field}\0access` : field;
1910
+ const cached = readingsCache.get(key);
1799
1911
  if (cached) return cached;
1800
1912
  const primary = resolveField(field);
1801
1913
  if (primary === null) {
1802
- readingsCache.set(field, []);
1914
+ readingsCache.set(key, []);
1803
1915
  return [];
1804
1916
  }
1805
1917
  let out = [primary];
1806
1918
  if (field === "command") {
1807
1919
  const raw = getNestedValue(args, field);
1808
1920
  if (typeof raw === "string") {
1809
- const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
1810
- out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
1921
+ const text = access ? fileAccessView(raw) : raw;
1922
+ const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
1923
+ out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
1811
1924
  }
1812
1925
  }
1813
- readingsCache.set(field, out);
1926
+ readingsCache.set(key, out);
1814
1927
  return out;
1815
1928
  };
1816
1929
  const results = rule.conditions.map((cond) => {
@@ -1828,14 +1941,14 @@ function evaluateSmartConditions(args, rule) {
1828
1941
  if (val === null || !cond.value) return false;
1829
1942
  const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1830
1943
  if (!reM) return false;
1831
- return resolveFieldReadings(cond.field).some((v) => reM.test(v));
1944
+ return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
1832
1945
  }
1833
1946
  case "notMatches": {
1834
1947
  if (!cond.value) return false;
1835
1948
  if (val === null) return true;
1836
1949
  const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1837
1950
  if (!reN) return false;
1838
- return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
1951
+ return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
1839
1952
  }
1840
1953
  case "matchesGlob":
1841
1954
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
@@ -2218,14 +2331,14 @@ function validateOtpSecret(raw) {
2218
2331
  }
2219
2332
  function assertBuiltinPatternsAreSafe() {
2220
2333
  for (const p of DLP_PATTERNS) {
2221
- if (!safeRegex3(p.regex.source)) {
2334
+ if (!safeRegex2(p.regex.source)) {
2222
2335
  throw new Error(
2223
2336
  `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
2224
2337
  );
2225
2338
  }
2226
2339
  }
2227
2340
  for (const re of SENSITIVE_PATH_PATTERNS) {
2228
- if (!safeRegex3(re.source)) {
2341
+ if (!safeRegex2(re.source)) {
2229
2342
  throw new Error(
2230
2343
  `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
2231
2344
  );
@@ -2691,10 +2804,13 @@ function matchCanaryArgs(args, values) {
2691
2804
  return null;
2692
2805
  }
2693
2806
  }
2694
- var aws_default, bash_safe_default, docker_default, filesystem_default, finance_default, github_default, identity_default, k8s_default, mongodb_default, postgres_default, project_jail_default, publishing_default, redis_default, BUILTIN_SHIELDS, ACTION_SHIELD_NAMES, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, MAX_READING_PAYLOADS, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, CURL_SOURCE_FLAGS, CURL_VALUE_LETTERS, WGET_VALUE_LETTERS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_DIRS, UPLOAD_SYSTEM_DIRS, LEGACY_SYSTEM_PATH_RE, BIP39_ENGLISH, indices, IBAN_LENGTH, IBAN_BBAN_SHAPE, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, CF_ODD, CF_EVEN, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, CIPHER_KEY_NAME_RE, CIPHER_NAME_PARTS, CIPHER_KEY_VALUE_RE, LARAVEL_KEY_VALUE_RE, SQL_PASSWORD_VALUE_RE, GRAFANA_KEY_VALUE_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DB_URL_PASSWORD_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax3, EDITOR_HEADS, MANAGER_HEADS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, SOURCE_COMMANDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, MASKED_KINDS, ENTRY_RE, syntax4, PUBLISH, OWN_HEADS, TERMINAL_ESCAPE_RE, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
2807
+ var MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, finance_default, identity_default, publishing_default, ACTION_TEMPLATES, ACTION_CHECKS, GAP, prog, httpTo, httpWrite, HTTP_WRITE, HTTP_WRITE_ANY, NOT_STRIPE_TEST, postTo, STRIPE_OBJECTS, APP_SHIELD_SPECS, APP_SHIELDS, APP_SHIELD_NAMES, APP_SERVER_GLOB, GENERAL_ACTION_RULES, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, SYSTEM_DIRS, UPLOAD_SYSTEM_DIRS, LEGACY_SYSTEM_PATH_RE, MAX_INLINE_CODE, OUTSIDE, pyImport, either, PYTHON, NODE, PERL, RUBY, PHP, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, INTERPRETER_LINE, PROGRAM_DIR, ACCESS_VIEW_CACHE_MAX, accessViewCache, MAX_READING_PAYLOADS, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, CURL_SOURCE_FLAGS, CURL_VALUE_LETTERS, WGET_VALUE_LETTERS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, BIP39_ENGLISH, indices, IBAN_LENGTH, IBAN_BBAN_SHAPE, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, CF_ODD, CF_EVEN, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, CIPHER_KEY_NAME_RE, CIPHER_NAME_PARTS, CIPHER_KEY_VALUE_RE, LARAVEL_KEY_VALUE_RE, SQL_PASSWORD_VALUE_RE, GRAFANA_KEY_VALUE_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DB_URL_PASSWORD_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax3, EDITOR_HEADS, MANAGER_HEADS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, SOURCE_COMMANDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, MASKED_KINDS, ENTRY_RE, syntax4, PUBLISH, OWN_HEADS, TERMINAL_ESCAPE_RE, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
2695
2808
  var init_dist = __esm({
2696
2809
  "packages/policy-engine/dist/index.mjs"() {
2697
2810
  "use strict";
2811
+ MAX_REGEX_LENGTH = 256;
2812
+ REGEX_CACHE_MAX = 500;
2813
+ regexCache = /* @__PURE__ */ new Map();
2698
2814
  aws_default = {
2699
2815
  name: "aws",
2700
2816
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -2996,77 +3112,6 @@ var init_dist = __esm({
2996
3112
  ],
2997
3113
  dangerousWords: ["wipefs"]
2998
3114
  };
2999
- finance_default = {
3000
- name: "finance",
3001
- description: "Asks before money moves through a payment or banking app (refunds, payouts, transfers, charges, invoices), whatever the request said. Reads are never asked about.",
3002
- aliases: ["payments", "money"],
3003
- smartRules: [
3004
- {
3005
- name: "shield:finance:review-refund",
3006
- tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
3007
- conditions: [],
3008
- verdict: "review",
3009
- reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
3010
- },
3011
- {
3012
- name: "shield:finance:review-payout",
3013
- tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
3014
- conditions: [],
3015
- verdict: "review",
3016
- reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
3017
- },
3018
- {
3019
- name: "shield:finance:review-transfer",
3020
- tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction,create_transaction}",
3021
- conditions: [],
3022
- verdict: "review",
3023
- reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
3024
- },
3025
- {
3026
- name: "shield:finance:review-charge",
3027
- tool: "{,mcp__*__}{create_payment_intent,confirm_payment_intent,capture_payment_intent,create_charge,create_payment,capture_payment,capture_order,create_order,authorize_order,create_checkout_session}",
3028
- conditions: [],
3029
- verdict: "review",
3030
- reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
3031
- },
3032
- {
3033
- name: "shield:finance:review-payment-link",
3034
- tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
3035
- conditions: [],
3036
- verdict: "review",
3037
- reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
3038
- },
3039
- {
3040
- name: "shield:finance:review-subscription",
3041
- tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
3042
- conditions: [],
3043
- verdict: "review",
3044
- reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
3045
- },
3046
- {
3047
- name: "shield:finance:review-dispute",
3048
- tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
3049
- conditions: [],
3050
- verdict: "review",
3051
- reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
3052
- },
3053
- {
3054
- name: "shield:finance:review-card",
3055
- tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
3056
- conditions: [],
3057
- verdict: "review",
3058
- reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
3059
- },
3060
- {
3061
- name: "shield:finance:review-withdraw",
3062
- tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
3063
- conditions: [],
3064
- verdict: "review",
3065
- reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
3066
- }
3067
- ],
3068
- dangerousWords: []
3069
- };
3070
3115
  github_default = {
3071
3116
  name: "github",
3072
3117
  description: "Protects GitHub repositories from destructive AI operations",
@@ -3093,56 +3138,6 @@ var init_dist = __esm({
3093
3138
  ],
3094
3139
  dangerousWords: []
3095
3140
  };
3096
- identity_default = {
3097
- name: "identity",
3098
- description: "Asks before a password, a key, a token, a role or a user changes in an identity or admin app, whatever the request said. Reads are never asked about.",
3099
- aliases: ["access", "iam"],
3100
- smartRules: [
3101
- {
3102
- name: "shield:identity:review-password",
3103
- tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
3104
- conditions: [],
3105
- verdict: "review",
3106
- reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
3107
- },
3108
- {
3109
- name: "shield:identity:review-key",
3110
- tool: "{,mcp__*__}{rotate_key,rotate_api_key,create_api_key,create_token,create_access_token,create_personal_access_token,regenerate_secret,rotate_secret,create_service_account_key,create_credential}",
3111
- conditions: [],
3112
- verdict: "review",
3113
- reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
3114
- },
3115
- {
3116
- name: "shield:identity:review-access",
3117
- tool: "{,mcp__*__}{grant_access,grant_permission,grant_role,revoke_access,revoke_permission,revoke_role,revoke_token,set_permission,set_permissions,set_role,assign_role,update_role,add_permission,remove_permission,attach_policy,detach_policy}",
3118
- conditions: [],
3119
- verdict: "review",
3120
- reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
3121
- },
3122
- {
3123
- name: "shield:identity:review-membership",
3124
- tool: "{,mcp__*__}{add_member,remove_member,add_user_to_group,remove_user_from_group,add_collaborator,remove_collaborator,invite_user,invite_member,invite_user_to_slack,invite_users,add_org_member,remove_org_member,add_team_member,remove_team_member}",
3125
- conditions: [],
3126
- verdict: "review",
3127
- reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
3128
- },
3129
- {
3130
- name: "shield:identity:review-user",
3131
- tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
3132
- conditions: [],
3133
- verdict: "review",
3134
- reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
3135
- },
3136
- {
3137
- name: "shield:identity:review-mfa",
3138
- tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
3139
- conditions: [],
3140
- verdict: "review",
3141
- reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
3142
- }
3143
- ],
3144
- dangerousWords: []
3145
- };
3146
3141
  k8s_default = {
3147
3142
  name: "k8s",
3148
3143
  description: "Protects Kubernetes clusters from destructive AI operations",
@@ -3395,7 +3390,7 @@ var init_dist = __esm({
3395
3390
  {
3396
3391
  field: "command",
3397
3392
  op: "matches",
3398
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
3393
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim?|emacs|code|type|[ef]?grep|rg|ag|ack|g?awk|sed|cut|tr|[jy]q|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development)){0,2}(?=[\\s;&|>)<]|$)",
3399
3394
  flags: "i"
3400
3395
  }
3401
3396
  ],
@@ -3409,7 +3404,7 @@ var init_dist = __esm({
3409
3404
  {
3410
3405
  field: "command",
3411
3406
  op: "matches",
3412
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3407
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim?|emacs|code|type|[ef]?grep|rg|ag|ack|g?awk|sed|cut|tr|[jy]q|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3413
3408
  flags: "i"
3414
3409
  }
3415
3410
  ],
@@ -3475,20 +3470,220 @@ var init_dist = __esm({
3475
3470
  ],
3476
3471
  dangerousWords: []
3477
3472
  };
3478
- publishing_default = {
3479
- name: "publishing",
3480
- description: "Asks before something goes public or ships outside the organisation (a release, a package, a deploy, a post), whatever the request said. The same switch covers the shell commands that publish.",
3481
- aliases: ["release", "deploy"],
3473
+ redis_default = {
3474
+ name: "redis",
3475
+ description: "Protects Redis instances from destructive AI operations",
3476
+ aliases: [],
3477
+ _scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
3482
3478
  smartRules: [
3483
3479
  {
3484
- name: "shield:publishing:review-release",
3485
- tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
3486
- conditions: [],
3487
- verdict: "review",
3488
- reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
3480
+ name: "shield:redis:block-flushall",
3481
+ tool: "*",
3482
+ conditions: [
3483
+ {
3484
+ field: "command",
3485
+ op: "matches",
3486
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3487
+ flags: "i"
3488
+ }
3489
+ ],
3490
+ verdict: "block",
3491
+ reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
3489
3492
  },
3490
3493
  {
3491
- name: "shield:publishing:review-package",
3494
+ name: "shield:redis:block-flushdb",
3495
+ tool: "*",
3496
+ conditions: [
3497
+ {
3498
+ field: "command",
3499
+ op: "matches",
3500
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
3501
+ flags: "i"
3502
+ }
3503
+ ],
3504
+ verdict: "block",
3505
+ reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
3506
+ },
3507
+ {
3508
+ name: "shield:redis:block-config-resetstat",
3509
+ tool: "*",
3510
+ conditions: [
3511
+ {
3512
+ field: "command",
3513
+ op: "matches",
3514
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
3515
+ flags: "i"
3516
+ }
3517
+ ],
3518
+ verdict: "block",
3519
+ reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
3520
+ },
3521
+ {
3522
+ name: "shield:redis:review-config-set",
3523
+ tool: "*",
3524
+ conditions: [
3525
+ {
3526
+ field: "command",
3527
+ op: "matches",
3528
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
3529
+ flags: "i"
3530
+ }
3531
+ ],
3532
+ verdict: "review",
3533
+ reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
3534
+ },
3535
+ {
3536
+ name: "shield:redis:review-del-wildcard",
3537
+ tool: "*",
3538
+ conditions: [
3539
+ {
3540
+ field: "command",
3541
+ op: "matches",
3542
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
3543
+ flags: "i"
3544
+ }
3545
+ ],
3546
+ verdict: "review",
3547
+ reason: "Wildcard key deletion requires human approval (Redis shield)"
3548
+ }
3549
+ ],
3550
+ dangerousWords: ["FLUSHALL", "FLUSHDB"]
3551
+ };
3552
+ finance_default = {
3553
+ name: "finance",
3554
+ description: "Asks before money moves through a payment or banking app (refunds, payouts, transfers, charges, invoices), whatever the request said. Reads are never asked about.",
3555
+ aliases: ["payments", "money"],
3556
+ smartRules: [
3557
+ {
3558
+ name: "shield:finance:review-refund",
3559
+ tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
3560
+ conditions: [],
3561
+ verdict: "review",
3562
+ reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
3563
+ },
3564
+ {
3565
+ name: "shield:finance:review-payout",
3566
+ tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
3567
+ conditions: [],
3568
+ verdict: "review",
3569
+ reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
3570
+ },
3571
+ {
3572
+ name: "shield:finance:review-transfer",
3573
+ tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
3574
+ conditions: [],
3575
+ verdict: "review",
3576
+ reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
3577
+ },
3578
+ {
3579
+ name: "shield:finance:review-charge",
3580
+ tool: "{,mcp__*__}{create_payment_intent,confirm_payment_intent,capture_payment_intent,create_charge,create_payment,capture_payment,capture_order,create_order,authorize_order,create_checkout_session}",
3581
+ conditions: [],
3582
+ verdict: "review",
3583
+ reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
3584
+ },
3585
+ {
3586
+ name: "shield:finance:review-payment-link",
3587
+ tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
3588
+ conditions: [],
3589
+ verdict: "review",
3590
+ reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
3591
+ },
3592
+ {
3593
+ name: "shield:finance:review-subscription",
3594
+ tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
3595
+ conditions: [],
3596
+ verdict: "review",
3597
+ reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
3598
+ },
3599
+ {
3600
+ name: "shield:finance:review-dispute",
3601
+ tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
3602
+ conditions: [],
3603
+ verdict: "review",
3604
+ reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
3605
+ },
3606
+ {
3607
+ name: "shield:finance:review-card",
3608
+ tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
3609
+ conditions: [],
3610
+ verdict: "review",
3611
+ reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
3612
+ },
3613
+ {
3614
+ name: "shield:finance:review-withdraw",
3615
+ tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
3616
+ conditions: [],
3617
+ verdict: "review",
3618
+ reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
3619
+ }
3620
+ ],
3621
+ dangerousWords: []
3622
+ };
3623
+ identity_default = {
3624
+ name: "identity",
3625
+ description: "Asks before a password, a key, a token, a role or a user changes in an identity or admin app, whatever the request said. Reads are never asked about.",
3626
+ aliases: ["access", "iam"],
3627
+ smartRules: [
3628
+ {
3629
+ name: "shield:identity:review-password",
3630
+ tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
3631
+ conditions: [],
3632
+ verdict: "review",
3633
+ reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
3634
+ },
3635
+ {
3636
+ name: "shield:identity:review-key",
3637
+ tool: "{,mcp__*__}{rotate_key,rotate_api_key,create_api_key,create_token,create_access_token,create_personal_access_token,regenerate_secret,rotate_secret,create_service_account_key,create_credential}",
3638
+ conditions: [],
3639
+ verdict: "review",
3640
+ reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
3641
+ },
3642
+ {
3643
+ name: "shield:identity:review-access",
3644
+ tool: "{,mcp__*__}{grant_access,grant_permission,grant_role,revoke_access,revoke_permission,revoke_role,revoke_token,set_permission,set_permissions,set_role,assign_role,update_role,add_permission,remove_permission,attach_policy,detach_policy,share_file}",
3645
+ conditions: [],
3646
+ verdict: "review",
3647
+ reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
3648
+ },
3649
+ {
3650
+ name: "shield:identity:review-membership",
3651
+ tool: "{,mcp__*__}{add_member,remove_member,add_user_to_group,remove_user_from_group,add_collaborator,remove_collaborator,invite_user,invite_member,invite_user_to_slack,invite_users,add_org_member,remove_org_member,add_team_member,remove_team_member,add_user_to_channel,remove_user_from_slack}",
3652
+ conditions: [],
3653
+ verdict: "review",
3654
+ reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
3655
+ },
3656
+ {
3657
+ name: "shield:identity:review-user",
3658
+ tool: "{,mcp__*__}{create_user,delete_user,delete_deactivated_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
3659
+ conditions: [],
3660
+ verdict: "review",
3661
+ reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
3662
+ },
3663
+ {
3664
+ name: "shield:identity:review-mfa",
3665
+ tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
3666
+ conditions: [],
3667
+ verdict: "review",
3668
+ reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
3669
+ }
3670
+ ],
3671
+ dangerousWords: []
3672
+ };
3673
+ publishing_default = {
3674
+ name: "publishing",
3675
+ description: "Asks before something goes public or ships outside the organisation (a release, a package, a deploy, a post), whatever the request said. The same switch covers the shell commands that publish.",
3676
+ aliases: ["release", "deploy"],
3677
+ smartRules: [
3678
+ {
3679
+ name: "shield:publishing:review-release",
3680
+ tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
3681
+ conditions: [],
3682
+ verdict: "review",
3683
+ reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
3684
+ },
3685
+ {
3686
+ name: "shield:publishing:review-package",
3492
3687
  tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
3493
3688
  conditions: [],
3494
3689
  verdict: "review",
@@ -3644,109 +3839,511 @@ var init_dist = __esm({
3644
3839
  ],
3645
3840
  dangerousWords: []
3646
3841
  };
3647
- redis_default = {
3648
- name: "redis",
3649
- description: "Protects Redis instances from destructive AI operations",
3650
- aliases: [],
3651
- _scopeNote: "Every rule requires Redis to actually be involved: a client in the command (redis-cli / valkey-cli, in either order \u2014 `redis-cli FLUSHALL` and `echo FLUSHALL | redis-cli` are both real), OR the command field holding the bare keyword, which is what the MCP redis_command tool sends ({ command: 'FLUSHALL', args: [] }). Without the client requirement these rules matched the WORD anywhere \u2014 including prose in a heredoc writing a file \u2014 which produced two observed false positives: a test fixture describing the shield, and a commit message naming the rule. Every other builtin shield already scopes this way (aws s3, docker system prune, kubectl delete, gh repo delete). See redis-scope.spec.ts.",
3652
- smartRules: [
3653
- {
3654
- name: "shield:redis:block-flushall",
3655
- tool: "*",
3656
- conditions: [
3657
- {
3658
- field: "command",
3659
- op: "matches",
3660
- value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3661
- flags: "i"
3662
- }
3663
- ],
3664
- verdict: "block",
3665
- reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
3666
- },
3667
- {
3668
- name: "shield:redis:block-flushdb",
3669
- tool: "*",
3670
- conditions: [
3671
- {
3672
- field: "command",
3673
- op: "matches",
3674
- value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
3675
- flags: "i"
3676
- }
3677
- ],
3678
- verdict: "block",
3679
- reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
3680
- },
3681
- {
3682
- name: "shield:redis:block-config-resetstat",
3683
- tool: "*",
3684
- conditions: [
3685
- {
3686
- field: "command",
3687
- op: "matches",
3688
- value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
3689
- flags: "i"
3690
- }
3691
- ],
3692
- verdict: "block",
3693
- reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
3694
- },
3695
- {
3696
- name: "shield:redis:review-config-set",
3697
- tool: "*",
3698
- conditions: [
3699
- {
3700
- field: "command",
3701
- op: "matches",
3702
- value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
3703
- flags: "i"
3704
- }
3705
- ],
3706
- verdict: "review",
3707
- reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
3708
- },
3709
- {
3710
- name: "shield:redis:review-del-wildcard",
3711
- tool: "*",
3712
- conditions: [
3713
- {
3714
- field: "command",
3715
- op: "matches",
3716
- value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
3717
- flags: "i"
3718
- }
3719
- ],
3720
- verdict: "review",
3721
- reason: "Wildcard key deletion requires human approval (Redis shield)"
3722
- }
3723
- ],
3724
- dangerousWords: ["FLUSHALL", "FLUSHDB"]
3842
+ ACTION_TEMPLATES = {
3843
+ finance: finance_default.smartRules,
3844
+ identity: identity_default.smartRules,
3845
+ publishing: publishing_default.smartRules
3846
+ };
3847
+ ACTION_CHECKS = {
3848
+ finance: { checkId: "actions.money", slug: "money", label: "Money check" },
3849
+ identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
3850
+ publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
3851
+ };
3852
+ GAP = "[^;&|]{0,200}?";
3853
+ prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
3854
+ httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
3855
+ httpWrite = (methods) => ({
3856
+ field: "command",
3857
+ op: "matches",
3858
+ value: `-X\\s*(?:${methods})|--(?:request|method)[=\\s]+(?:${methods})|--post-(?:data|file)|\\s(?:${methods}|${methods.toLowerCase()})\\s|^(?!.*\\s(?:-[a-zA-Z]*G|--get)(?![\\w-])).*(?:\\s-d[\\s'"@]|\\s--data|\\s--json\\s|\\s-F\\s|\\s--form\\s)`
3859
+ });
3860
+ HTTP_WRITE = httpWrite("POST|DELETE");
3861
+ HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
3862
+ NOT_STRIPE_TEST = {
3863
+ field: "command",
3864
+ op: "notMatches",
3865
+ value: "\\b[sr]k_test_"
3725
3866
  };
3867
+ postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
3868
+ STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
3869
+ APP_SHIELD_SPECS = [
3870
+ {
3871
+ name: "stripe",
3872
+ label: "Stripe",
3873
+ description: "Asks before money moves in Stripe (refunds, payouts, charges, invoices, subscriptions), whether the agent uses the Stripe MCP server, the stripe CLI, the API or the Stripe library in a script. Reads are never asked about.",
3874
+ template: "finance",
3875
+ server: "*stripe*",
3876
+ doors: [
3877
+ {
3878
+ name: "shield:stripe:review-cli",
3879
+ tool: "bash",
3880
+ conditions: [
3881
+ {
3882
+ field: "command",
3883
+ op: "matches",
3884
+ value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
3885
+ flags: "i"
3886
+ },
3887
+ {
3888
+ field: "command",
3889
+ op: "matches",
3890
+ // The stripe CLI works in test mode unless told otherwise.
3891
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
3892
+ }
3893
+ ],
3894
+ conditionMode: "all",
3895
+ verdict: "review",
3896
+ reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
3897
+ },
3898
+ {
3899
+ name: "shield:stripe:review-cli-request",
3900
+ tool: "bash",
3901
+ conditions: [
3902
+ {
3903
+ field: "command",
3904
+ op: "matches",
3905
+ value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
3906
+ flags: "i"
3907
+ },
3908
+ {
3909
+ field: "command",
3910
+ op: "matches",
3911
+ // The stripe CLI works in test mode unless told otherwise.
3912
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
3913
+ }
3914
+ ],
3915
+ conditionMode: "all",
3916
+ verdict: "review",
3917
+ reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
3918
+ },
3919
+ {
3920
+ name: "shield:stripe:review-api",
3921
+ tool: "bash",
3922
+ conditions: [
3923
+ {
3924
+ field: "command",
3925
+ op: "matches",
3926
+ value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
3927
+ flags: "i"
3928
+ },
3929
+ HTTP_WRITE,
3930
+ NOT_STRIPE_TEST
3931
+ ],
3932
+ conditionMode: "all",
3933
+ verdict: "review",
3934
+ reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
3935
+ },
3936
+ {
3937
+ name: "shield:stripe:review-code",
3938
+ tool: "bash",
3939
+ conditions: [
3940
+ {
3941
+ field: "command",
3942
+ op: "matches",
3943
+ // Python: stripe.Refund.create(...)
3944
+ value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
3945
+ },
3946
+ NOT_STRIPE_TEST
3947
+ ],
3948
+ conditionMode: "all",
3949
+ verdict: "review",
3950
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
3951
+ },
3952
+ {
3953
+ name: "shield:stripe:review-code-node",
3954
+ tool: "bash",
3955
+ conditions: [
3956
+ {
3957
+ field: "command",
3958
+ op: "matches",
3959
+ // Node: stripe.refunds.create(...)
3960
+ value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
3961
+ },
3962
+ NOT_STRIPE_TEST
3963
+ ],
3964
+ conditionMode: "all",
3965
+ verdict: "review",
3966
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
3967
+ },
3968
+ {
3969
+ name: "shield:stripe:review-code-http",
3970
+ tool: "bash",
3971
+ conditions: [
3972
+ {
3973
+ field: "command",
3974
+ op: "matches",
3975
+ // requests.post("https://api.stripe.com/v1/refunds", ...)
3976
+ value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
3977
+ },
3978
+ NOT_STRIPE_TEST
3979
+ ],
3980
+ conditionMode: "all",
3981
+ verdict: "review",
3982
+ reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
3983
+ }
3984
+ ]
3985
+ },
3986
+ {
3987
+ name: "paypal",
3988
+ label: "PayPal",
3989
+ description: "Asks before money moves in PayPal (refunds, payouts, captured orders, invoices, subscriptions), whether the agent uses a PayPal MCP server, the API or the PayPal SDK in a script. The sandbox is never asked about, and neither are reads.",
3990
+ template: "finance",
3991
+ server: "*paypal*",
3992
+ doors: [
3993
+ {
3994
+ name: "shield:paypal:review-api",
3995
+ tool: "bash",
3996
+ conditions: [
3997
+ {
3998
+ field: "command",
3999
+ op: "matches",
4000
+ // The live hosts only: api.paypal.com and api-m.paypal.com. The
4001
+ // sandbox (api-m.sandbox.paypal.com) moves no money.
4002
+ value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
4003
+ flags: "i"
4004
+ },
4005
+ {
4006
+ field: "command",
4007
+ op: "matches",
4008
+ // The endpoints that move money.
4009
+ value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
4010
+ flags: "i"
4011
+ },
4012
+ HTTP_WRITE
4013
+ ],
4014
+ conditionMode: "all",
4015
+ verdict: "review",
4016
+ reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
4017
+ },
4018
+ {
4019
+ name: "shield:paypal:review-code",
4020
+ tool: "bash",
4021
+ conditions: [
4022
+ {
4023
+ field: "command",
4024
+ op: "matches",
4025
+ // The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
4026
+ // Python paypalrestsdk.
4027
+ value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
4028
+ }
4029
+ ],
4030
+ verdict: "review",
4031
+ reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
4032
+ },
4033
+ {
4034
+ name: "shield:paypal:review-code-http",
4035
+ tool: "bash",
4036
+ conditions: [
4037
+ {
4038
+ field: "command",
4039
+ op: "matches",
4040
+ // The endpoints that move money; the token call every script
4041
+ // starts with (/v1/oauth2/token) is not one.
4042
+ value: postTo(
4043
+ "api(?:-m)?\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/\"'`]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)"
4044
+ )
4045
+ }
4046
+ ],
4047
+ verdict: "review",
4048
+ reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
4049
+ }
4050
+ ]
4051
+ },
4052
+ {
4053
+ name: "square",
4054
+ label: "Square",
4055
+ description: "Asks before money moves in Square (refunds, payments, paid orders, invoices, subscriptions), whether the agent uses a Square MCP server, the API or the Square SDK in a script. The sandbox host is never asked about, and neither are reads.",
4056
+ template: "finance",
4057
+ // Square's servers, never Squarespace (a website builder).
4058
+ server: "{*square,*square[-_]*,*squareup*}",
4059
+ mcpTools: [
4060
+ {
4061
+ // Square's official MCP server has one tool for every API call; the
4062
+ // service and method name what it does.
4063
+ name: "shield:square:review-mcp-request",
4064
+ tool: "{,mcp__*__}make_api_request",
4065
+ conditions: [
4066
+ {
4067
+ field: "service",
4068
+ op: "matches",
4069
+ value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
4070
+ flags: "i"
4071
+ },
4072
+ {
4073
+ field: "method",
4074
+ op: "notMatches",
4075
+ value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
4076
+ flags: "i"
4077
+ }
4078
+ ],
4079
+ conditionMode: "all",
4080
+ verdict: "review",
4081
+ reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
4082
+ }
4083
+ ],
4084
+ doors: [
4085
+ {
4086
+ name: "shield:square:review-api",
4087
+ tool: "bash",
4088
+ conditions: [
4089
+ {
4090
+ field: "command",
4091
+ op: "matches",
4092
+ // The live host only; the sandbox is connect.squareupsandbox.com.
4093
+ value: httpTo(
4094
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
4095
+ ),
4096
+ flags: "i"
4097
+ },
4098
+ HTTP_WRITE_ANY
4099
+ ],
4100
+ conditionMode: "all",
4101
+ verdict: "review",
4102
+ reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
4103
+ },
4104
+ {
4105
+ name: "shield:square:review-code",
4106
+ tool: "bash",
4107
+ conditions: [
4108
+ {
4109
+ field: "command",
4110
+ op: "matches",
4111
+ // The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
4112
+ value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
4113
+ },
4114
+ {
4115
+ field: "command",
4116
+ op: "matches",
4117
+ value: "\\.refunds(?:Api)?\\.refund(?:Payment|_payment)\\s*\\(|\\.payments(?:Api)?\\.(?:create|complete)(?:Payment|_payment)?\\s*\\(|\\.orders(?:Api)?\\.pay(?:Order|_order)?\\s*\\(|\\.invoices(?:Api)?\\.publish(?:Invoice|_invoice)?\\s*\\("
4118
+ }
4119
+ ],
4120
+ conditionMode: "all",
4121
+ verdict: "review",
4122
+ reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
4123
+ },
4124
+ {
4125
+ name: "shield:square:review-code-http",
4126
+ tool: "bash",
4127
+ conditions: [
4128
+ {
4129
+ field: "command",
4130
+ op: "matches",
4131
+ value: postTo(
4132
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
4133
+ )
4134
+ }
4135
+ ],
4136
+ verdict: "review",
4137
+ reason: "This code posts to the Square API. node9 asks before money moves (Square)."
4138
+ }
4139
+ ]
4140
+ },
4141
+ {
4142
+ name: "plaid",
4143
+ label: "Plaid",
4144
+ description: "Asks before money moves through Plaid (transfers, transfer authorizations, payment initiation), whether the agent uses a Plaid MCP server, the API or the Plaid SDK in a script. The sandbox host is never asked about, and neither are reads such as balances or transactions.",
4145
+ template: "finance",
4146
+ server: "*plaid*",
4147
+ doors: [
4148
+ {
4149
+ name: "shield:plaid:review-api",
4150
+ tool: "bash",
4151
+ conditions: [
4152
+ {
4153
+ field: "command",
4154
+ op: "matches",
4155
+ // Every Plaid endpoint is a POST, so the path is what tells a
4156
+ // write; the live host only, never sandbox.plaid.com. A script
4157
+ // that posts to the URL meets this door too.
4158
+ value: httpTo(
4159
+ "production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
4160
+ ),
4161
+ flags: "i"
4162
+ }
4163
+ ],
4164
+ verdict: "review",
4165
+ reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
4166
+ },
4167
+ {
4168
+ name: "shield:plaid:review-code",
4169
+ tool: "bash",
4170
+ conditions: [
4171
+ { field: "command", op: "matches", value: "\\bplaid", flags: "i" },
4172
+ {
4173
+ field: "command",
4174
+ op: "matches",
4175
+ value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
4176
+ }
4177
+ ],
4178
+ conditionMode: "all",
4179
+ verdict: "review",
4180
+ reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
4181
+ }
4182
+ ]
4183
+ },
4184
+ {
4185
+ name: "okta",
4186
+ label: "Okta",
4187
+ description: "Asks before a user, a group member, a role, a password or a key changes in Okta, whether the agent uses an Okta MCP server, the API or the Okta SDK in a script. Reads and signing in are never asked about.",
4188
+ template: "identity",
4189
+ server: "*okta*",
4190
+ doors: [
4191
+ {
4192
+ name: "shield:okta:review-api",
4193
+ tool: "bash",
4194
+ conditions: [
4195
+ {
4196
+ field: "command",
4197
+ op: "matches",
4198
+ // The management API only: sign-in (/api/v1/authn, /sessions,
4199
+ // /oauth2) is not under these paths.
4200
+ value: httpTo(
4201
+ "[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
4202
+ ),
4203
+ flags: "i"
4204
+ },
4205
+ HTTP_WRITE_ANY
4206
+ ],
4207
+ conditionMode: "all",
4208
+ verdict: "review",
4209
+ reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
4210
+ },
4211
+ {
4212
+ name: "shield:okta:review-code",
4213
+ tool: "bash",
4214
+ conditions: [
4215
+ {
4216
+ field: "command",
4217
+ op: "matches",
4218
+ // The Okta SDK in the script: a method named createUser alone is
4219
+ // in half the applications ever written.
4220
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
4221
+ },
4222
+ {
4223
+ field: "command",
4224
+ op: "matches",
4225
+ value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
4226
+ }
4227
+ ],
4228
+ conditionMode: "all",
4229
+ verdict: "review",
4230
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
4231
+ },
4232
+ {
4233
+ name: "shield:okta:review-code-py",
4234
+ tool: "bash",
4235
+ conditions: [
4236
+ {
4237
+ field: "command",
4238
+ op: "matches",
4239
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
4240
+ },
4241
+ {
4242
+ field: "command",
4243
+ op: "matches",
4244
+ value: "\\b(?:(?:create|delete|deactivate|deactivate_or_delete|suspend|unlock|activate)_user|(?:reset|expire)_password|reset_factors|(?:add|remove)_user_(?:to|from)_group|assign_role_to_(?:user|group))\\s*\\("
4245
+ }
4246
+ ],
4247
+ conditionMode: "all",
4248
+ verdict: "review",
4249
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
4250
+ }
4251
+ ]
4252
+ },
4253
+ {
4254
+ name: "auth0",
4255
+ label: "Auth0",
4256
+ description: "Asks before a user, a role, an application or its permissions change in Auth0, whether the agent uses an Auth0 MCP server, the auth0 CLI, the Management API or the Auth0 SDK in a script. Reads and signing in are never asked about.",
4257
+ template: "identity",
4258
+ server: "*auth0*",
4259
+ mcpTools: [
4260
+ {
4261
+ // Auth0's official MCP server: applications (their secrets and
4262
+ // callbacks), APIs and their scopes, and Actions (code that runs at
4263
+ // every sign-in).
4264
+ name: "shield:auth0:review-tenant-config",
4265
+ tool: "{,mcp__*__}{auth0_create_application,auth0_update_application,auth0_create_resource_server,auth0_update_resource_server,auth0_create_action,auth0_update_action,auth0_deploy_action}",
4266
+ conditions: [],
4267
+ verdict: "review",
4268
+ reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
4269
+ }
4270
+ ],
4271
+ doors: [
4272
+ {
4273
+ name: "shield:auth0:review-cli",
4274
+ tool: "bash",
4275
+ conditions: [
4276
+ {
4277
+ field: "command",
4278
+ op: "matches",
4279
+ value: prog("auth0") + GAP + "\\s(?:(?:users|roles|apps|orgs)\\s+(?:create|update|delete|rm)|users\\s+roles\\s+(?:assign|add|remove|rm)|roles\\s+permissions\\s+(?:add|remove|rm)|users\\s+blocks\\s+unblock|api\\s+(?:post|put|patch|delete))(?![\\w-])",
4280
+ flags: "i"
4281
+ }
4282
+ ],
4283
+ verdict: "review",
4284
+ reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
4285
+ },
4286
+ {
4287
+ name: "shield:auth0:review-api",
4288
+ tool: "bash",
4289
+ conditions: [
4290
+ {
4291
+ field: "command",
4292
+ op: "matches",
4293
+ // The Management API only; /oauth/token is signing in.
4294
+ value: httpTo(
4295
+ "\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
4296
+ ),
4297
+ flags: "i"
4298
+ },
4299
+ HTTP_WRITE_ANY
4300
+ ],
4301
+ conditionMode: "all",
4302
+ verdict: "review",
4303
+ reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
4304
+ },
4305
+ {
4306
+ name: "shield:auth0:review-code",
4307
+ tool: "bash",
4308
+ conditions: [
4309
+ {
4310
+ field: "command",
4311
+ op: "matches",
4312
+ // The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
4313
+ value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
4314
+ },
4315
+ {
4316
+ field: "command",
4317
+ op: "matches",
4318
+ value: "\\.(?:users|roles|organizations|clients)\\.(?:create|delete|update|assignRoles|deleteRoles|assignUsers|addPermissions|deletePermissions|addMembers|deleteMembers|add_roles|remove_roles|add_users|add_permissions|remove_permissions)\\s*\\("
4319
+ }
4320
+ ],
4321
+ conditionMode: "all",
4322
+ verdict: "review",
4323
+ reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
4324
+ }
4325
+ ]
4326
+ }
4327
+ ];
4328
+ APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
4329
+ APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
4330
+ APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
4331
+ GENERAL_ACTION_RULES = buildGeneralRules();
3726
4332
  BUILTIN_SHIELDS = {
3727
4333
  [aws_default.name]: aws_default,
3728
4334
  [bash_safe_default.name]: bash_safe_default,
3729
4335
  [docker_default.name]: docker_default,
3730
4336
  [filesystem_default.name]: filesystem_default,
3731
- // Dangerous actions as category shields (node9Firewall#385): an MCP app's
3732
- // money, credential and publishing tools, asked about whatever the request
3733
- // said. One glob per rule covers the bare name the gateway sends and the
3734
- // mcp__<server>__ name the agent hook sends.
3735
- [finance_default.name]: finance_default,
3736
4337
  [github_default.name]: github_default,
3737
- [identity_default.name]: identity_default,
3738
4338
  [k8s_default.name]: k8s_default,
3739
4339
  [mongodb_default.name]: mongodb_default,
3740
4340
  [postgres_default.name]: postgres_default,
3741
4341
  [project_jail_default.name]: project_jail_default,
3742
- [publishing_default.name]: publishing_default,
3743
- [redis_default.name]: redis_default
4342
+ [redis_default.name]: redis_default,
4343
+ // App shields generated from the action templates (node9Firewall#390): one
4344
+ // per app, every door (MCP server, CLI, API, library), off by default.
4345
+ ...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
3744
4346
  };
3745
- ACTION_SHIELD_NAMES = /* @__PURE__ */ new Set([
3746
- "finance",
3747
- "identity",
3748
- "publishing"
3749
- ]);
3750
4347
  assertBuiltinShieldRegexesAreSafe();
3751
4348
  SSRF_MAX_HOST = 253;
3752
4349
  METADATA_ADDRESSES = /* @__PURE__ */ new Set([
@@ -3792,9 +4389,9 @@ var init_dist = __esm({
3792
4389
  },
3793
4390
  // ── Commands ──────────────────────────────────────────────────────────────
3794
4391
  "commands.inline-exec": {
3795
- plain: "The agent runs a small program written inside the command itself.",
4392
+ plain: "The agent runs a small program written inside the command itself, and node9 reads it: ordinary code runs, code that deletes, sends data out, runs other commands or reads protected files is held, and so is code node9 cannot read.",
3796
4393
  example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
3797
- advice: "Agents do this often for harmless tasks. Set Log if your team finds the review prompts noisy; keep Review if agents work near production."
4394
+ advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
3798
4395
  },
3799
4396
  "commands.eval-dynamic": {
3800
4397
  plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
@@ -3826,6 +4423,21 @@ var init_dist = __esm({
3826
4423
  example: "chmod 777 deploy.sh",
3827
4424
  advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
3828
4425
  },
4426
+ "actions.money": {
4427
+ plain: "The agent moves money (a refund, a payout, a transfer, a charge) through an app node9 has no protection for, such as an internal billing server.",
4428
+ example: "refund_order on a server called acme-billing",
4429
+ advice: "Keep Review. When the app is Stripe or PayPal, turn on its own protection instead: it covers the CLI, the API and scripts too, and a call from it no longer comes here."
4430
+ },
4431
+ "actions.access": {
4432
+ plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
4433
+ example: "invite_user or grant_role on a company admin server",
4434
+ advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
4435
+ },
4436
+ "actions.publish": {
4437
+ plain: "The agent publishes something outside the organisation (a release, a package, a deploy, a public post) through an app node9 has no protection for.",
4438
+ example: "create_release or deploy_to_production on a company MCP server",
4439
+ advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
4440
+ },
3829
4441
  "commands.sudo": {
3830
4442
  plain: "The agent runs a command as the administrator (root).",
3831
4443
  example: "sudo apt-get install -y nginx",
@@ -3986,7 +4598,7 @@ var init_dist = __esm({
3986
4598
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
3987
4599
  },
3988
4600
  "loading.malicious-package": {
3989
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only.",
4601
+ plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only. A new release built from the same repository as the one before it (registry provenance) and an install script the package has always run do not stop; a release that lost the provenance its previous release had does.",
3990
4602
  example: "npm install of a package flagged as malware",
3991
4603
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
3992
4604
  }
@@ -4038,7 +4650,7 @@ var init_dist = __esm({
4038
4650
  id: "commands.inline-exec",
4039
4651
  group: "commands",
4040
4652
  title: "Inline code execution",
4041
- catches: "node -e, python -c, bash -c, a heredoc into an interpreter",
4653
+ catches: "inline code (python -c, node -e, a heredoc into an interpreter) that deletes, sends, opens a socket, runs commands, reads protected files or decodes and runs; unreadable inline code",
4042
4654
  values: ALL,
4043
4655
  defaultValue: "review",
4044
4656
  legacy: ["Node9 Standard (Inline Execution)"]
@@ -4395,6 +5007,38 @@ var init_dist = __esm({
4395
5007
  values: ALL,
4396
5008
  defaultValue: "block",
4397
5009
  legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
5010
+ },
5011
+ // ── Actions in apps (node9Firewall#390) ──────────────────────────────────
5012
+ // The safety net under the app shields: an action that moves money, changes
5013
+ // access or publishes, on an MCP server no app shield claims (an internal
5014
+ // billing server, a vendor with no shield, Zapier). Review by default; the
5015
+ // rules come from the action templates (shields/apps.ts).
5016
+ {
5017
+ id: "actions.money",
5018
+ group: "actions",
5019
+ title: "Money moves",
5020
+ catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
5021
+ values: ALL,
5022
+ defaultValue: "review",
5023
+ legacy: generalActionRuleNames("finance")
5024
+ },
5025
+ {
5026
+ id: "actions.access",
5027
+ group: "actions",
5028
+ title: "Accounts and access change",
5029
+ catches: "a password, key, role, permission or user changed through an app with no protection of its own",
5030
+ values: ALL,
5031
+ defaultValue: "review",
5032
+ legacy: generalActionRuleNames("identity")
5033
+ },
5034
+ {
5035
+ id: "actions.publish",
5036
+ group: "actions",
5037
+ title: "Something is published",
5038
+ catches: "a release, package, deploy or public post through an app with no protection of its own",
5039
+ values: ALL,
5040
+ defaultValue: "review",
5041
+ legacy: generalActionRuleNames("publishing")
4398
5042
  }
4399
5043
  ];
4400
5044
  SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
@@ -4515,6 +5159,170 @@ var init_dist = __esm({
4515
5159
  \u03A7: "X"
4516
5160
  };
4517
5161
  HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
5162
+ SYSTEM_DIRS = [
5163
+ "/usr",
5164
+ "/etc",
5165
+ "/boot",
5166
+ "/bin",
5167
+ "/sbin",
5168
+ "/lib",
5169
+ "/lib64",
5170
+ "/var/log"
5171
+ ];
5172
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
5173
+ LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
5174
+ MAX_INLINE_CODE = 64 * 1024;
5175
+ OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
5176
+ pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
5177
+ either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
5178
+ PYTHON = [
5179
+ [
5180
+ "deletes",
5181
+ /\bos\.(?:remove|unlink|rmdir|removedirs)\s*\(|\bshutil\.rmtree\s*\(|\.(?:unlink|rmdir)\s*\(|\bfrom\s+(?:os|shutil)\s+import\s+[^\n]*\b(?:remove|unlink|rmdir|removedirs|rmtree)\b/
5182
+ ],
5183
+ [
5184
+ "writes-outside",
5185
+ new RegExp(
5186
+ String.raw`\bopen\s*\(\s*[rRbBuU]?['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*(?:mode\s*=\s*)?[rRbB]?['"][^'"\n]*[wax+]|\bPath\s*\(\s*[rR]?['"]${OUTSIDE}[^'"\n]*['"]\s*\)\s*\.(?:write_text|write_bytes|touch|mkdir|symlink_to)\s*\(|\bshutil\.(?:copy\w*|move)\s*\([^)\n]*['"]${OUTSIDE}|\bos\.(?:rename|replace|symlink|link|truncate|makedirs|mkdir)\s*\([^)\n]*['"]${OUTSIDE}`
5187
+ )
5188
+ ],
5189
+ [
5190
+ "sends",
5191
+ either(
5192
+ pyImport(
5193
+ String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
5194
+ ),
5195
+ /\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
5196
+ )
5197
+ ],
5198
+ [
5199
+ "listens",
5200
+ either(
5201
+ pyImport(String.raw`pty|socketserver|http\.server`),
5202
+ /\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
5203
+ )
5204
+ ],
5205
+ [
5206
+ "runs-commands",
5207
+ either(
5208
+ pyImport("subprocess|pexpect|commands|psutil"),
5209
+ /\bos\.(?:system|popen|exec\w*|spawn\w*|posix_spawn\w*|fork|forkpty|kill|killpg)\s*\(|\bfrom\s+os\s+import\s+[^\n;]*\b(?:system|popen|exec\w*|spawn\w*|fork|kill)\b|\bfrom\s+(?:os|shutil|pty|ctypes)\s+import\s+\*/
5210
+ )
5211
+ ],
5212
+ [
5213
+ "builds-code",
5214
+ /(?<![\w.])(?:exec|eval|compile|execfile)\s*\(|\b__import__\s*\(|\bimportlib\b|\b(?:c?pickle|dill|marshal|shelve)\.loads?\s*\(|\byaml\.(?:unsafe_)?load\s*\((?![^)\n]*SafeLoader)|\bctypes\b|\b(?:getattr|vars)\s*\(\s*(?:os|subprocess|shutil|socket|sys|pty|builtins|__builtins__)\b|\b(?:os|subprocess|shutil|socket|sys|pty|builtins)\.__dict__\b|\b__builtins__\b|\bsys\.modules\b|\bglobals\s*\(\s*\)\s*\[/
5215
+ ],
5216
+ [
5217
+ // Spyware shapes: a screenshot, the clipboard, a key logger.
5218
+ "monitors",
5219
+ either(
5220
+ pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
5221
+ /\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
5222
+ )
5223
+ ],
5224
+ [
5225
+ "permissions",
5226
+ /\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
5227
+ ]
5228
+ ];
5229
+ NODE = [
5230
+ [
5231
+ "deletes",
5232
+ /\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
5233
+ ],
5234
+ [
5235
+ "writes-outside",
5236
+ new RegExp(
5237
+ String.raw`(?:\.(?:writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream|copyFile|copyFileSync|cp|cpSync|rename|renameSync|truncate|truncateSync|symlink|symlinkSync|mkdir|mkdirSync)|\bDeno\.(?:writeTextFile|writeFile|rename|mkdir)(?:Sync)?|\bBun\.write)\s*\(\s*['"\x60]${OUTSIDE}`
5238
+ )
5239
+ ],
5240
+ [
5241
+ "sends",
5242
+ /\bfetch\s*\(|(?:\brequire\s*\(\s*|\bfrom\s+|\bimport\s+)['"\x60](?:node:)?(?:http|https|http2|net|dgram|tls|axios|node-fetch|ws|request|got|undici|nodemailer)['"\x60]|\bnew\s+WebSocket\b|\bXMLHttpRequest\b|\b(?:Deno|Bun)\.connect\w*\s*\(/
5243
+ ],
5244
+ ["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
5245
+ [
5246
+ "runs-commands",
5247
+ /\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
5248
+ ],
5249
+ [
5250
+ "builds-code",
5251
+ /(?<![\w.])eval\s*\(|\bnew\s+Function\s*\(|(?<![\w.])Function\s*\(|(?:\brequire\s*\(\s*|\bfrom\s+)['"\x60](?:node:)?vm['"\x60]|\bvm\.\w+|\brequire\s*\(\s*[^'"\x60\s)]|\bimport\s*\(\s*[^'"\x60\s)]/
5252
+ ],
5253
+ [
5254
+ "permissions",
5255
+ /\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
5256
+ ]
5257
+ ];
5258
+ PERL = [
5259
+ ["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
5260
+ [
5261
+ "writes-outside",
5262
+ new RegExp(
5263
+ String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
5264
+ )
5265
+ ],
5266
+ [
5267
+ "sends",
5268
+ /\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
5269
+ ],
5270
+ [
5271
+ "runs-commands",
5272
+ /\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
5273
+ ],
5274
+ ["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
5275
+ ["permissions", /\b(?:chmod|chown)\b/]
5276
+ ];
5277
+ RUBY = [
5278
+ [
5279
+ "deletes",
5280
+ /\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
5281
+ ],
5282
+ [
5283
+ "writes-outside",
5284
+ new RegExp(
5285
+ String.raw`\bFile\.(?:write|binwrite|symlink|rename)\s*\(?\s*['"]${OUTSIDE}|\bFile\.open\s*\(?\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][wa+]|\bFileUtils\.(?:cp\w*|mv|install|ln\w*|mkdir_p|touch)\s*\(?[^\n]*['"]${OUTSIDE}`
5286
+ )
5287
+ ],
5288
+ [
5289
+ "sends",
5290
+ /\bNet::\w+|\b(?:TCPSocket|UDPSocket|UNIXSocket|Socket)\b|\bopen-uri\b|\bURI\.open\b|\b(?:HTTParty|Faraday|RestClient|Excon|Typhoeus)\b|\brequire\s+['"](?:socket|net\/\w+|open-uri)['"]/
5291
+ ],
5292
+ ["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
5293
+ [
5294
+ "runs-commands",
5295
+ /(?<![\w.])(?:system|exec|spawn|fork)\b|`[^`]*`|%x[{([|!]|(?<![\w.])open\s*\(?\s*['"]\||\bIO\.popen\b|\bOpen3\b|\bProcess\.(?:spawn|kill|exec)\b|\bKernel\.(?:system|exec|spawn|open)\b/
5296
+ ],
5297
+ [
5298
+ "builds-code",
5299
+ /(?<![\w.])eval\b|\b(?:instance|class|module)_eval\b|\b(?:send|public_send)\s*\(?\s*:?['"]?(?:system|eval|exec|spawn)\b|\bMarshal\.load\b|\bYAML\.(?:unsafe_)?load\b|\bconst_get\b/
5300
+ ],
5301
+ ["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
5302
+ ];
5303
+ PHP = [
5304
+ ["deletes", /\b(?:unlink|rmdir)\s*\(/],
5305
+ [
5306
+ "writes-outside",
5307
+ new RegExp(
5308
+ String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
5309
+ )
5310
+ ],
5311
+ [
5312
+ "sends",
5313
+ /\b(?:curl_exec|curl_init|fsockopen|pfsockopen|stream_socket_client|socket_create|socket_connect|mail|ftp_connect)\s*\(|\b(?:file_get_contents|fopen|file|readfile)\s*\(\s*['"](?:https?|ftp):/
5314
+ ],
5315
+ ["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
5316
+ [
5317
+ "runs-commands",
5318
+ /\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
5319
+ ],
5320
+ [
5321
+ "builds-code",
5322
+ /\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
5323
+ ],
5324
+ ["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
5325
+ ];
4518
5326
  ({ syntax } = mvdanSh);
4519
5327
  sharedParser = syntax.NewParser();
4520
5328
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -4552,6 +5360,10 @@ var init_dist = __esm({
4552
5360
  AST_CACHE_MAX = 5e3;
4553
5361
  astCache = /* @__PURE__ */ new Map();
4554
5362
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
5363
+ INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
5364
+ PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
5365
+ ACCESS_VIEW_CACHE_MAX = 1e3;
5366
+ accessViewCache = /* @__PURE__ */ new Map();
4555
5367
  MAX_READING_PAYLOADS = 8;
4556
5368
  defaultExpansionOps = null;
4557
5369
  VARIABLE_READERS = /* @__PURE__ */ new Set([
@@ -5285,9 +6097,6 @@ var init_dist = __esm({
5285
6097
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
5286
6098
  NONE = { kind: "none" };
5287
6099
  UNKNOWN = { kind: "unknown" };
5288
- MAX_REGEX_LENGTH = 256;
5289
- REGEX_CACHE_MAX = 500;
5290
- regexCache = /* @__PURE__ */ new Map();
5291
6100
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
5292
6101
  BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
5293
6102
  "bash",
@@ -5325,18 +6134,6 @@ var init_dist = __esm({
5325
6134
  (s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
5326
6135
  )
5327
6136
  );
5328
- SYSTEM_DIRS = [
5329
- "/usr",
5330
- "/etc",
5331
- "/boot",
5332
- "/bin",
5333
- "/sbin",
5334
- "/lib",
5335
- "/lib64",
5336
- "/var/log"
5337
- ];
5338
- UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
5339
- LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
5340
6137
  BIP39_ENGLISH = `abandon
5341
6138
  ability
5342
6139
  able
@@ -8377,7 +9174,11 @@ zoo`.split("\n");
8377
9174
  "commands.disk-destroy",
8378
9175
  "commands.dangerous-word",
8379
9176
  "commands.node9-disarm",
8380
- "data.pipe-chain"
9177
+ "data.pipe-chain",
9178
+ // The general action checks (node9Firewall#390): smart rules the map governs.
9179
+ "actions.money",
9180
+ "actions.access",
9181
+ "actions.publish"
8381
9182
  ]);
8382
9183
  KNOB_GOVERNED = {
8383
9184
  "data.secrets": ["off", "block"],
@@ -9230,7 +10031,8 @@ var init_config_schema = __esm({
9230
10031
  }
9231
10032
  ),
9232
10033
  value: z.string().optional(),
9233
- flags: z.string().optional()
10034
+ flags: z.string().optional(),
10035
+ reading: z.enum(["access"]).optional()
9234
10036
  }).refine(
9235
10037
  (c) => {
9236
10038
  if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
@@ -9241,6 +10043,9 @@ var init_config_schema = __esm({
9241
10043
  SmartRuleSchema = z.object({
9242
10044
  name: z.string().optional(),
9243
10045
  tool: z.string().min(1, "Smart rule tool must not be empty"),
10046
+ // Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
10047
+ server: z.string().min(1).optional(),
10048
+ notServer: z.string().min(1).optional(),
9244
10049
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
9245
10050
  conditionMode: z.enum(["all", "any"]).optional(),
9246
10051
  verdict: z.enum(["allow", "review", "block"], {
@@ -9391,6 +10196,82 @@ var init_config_schema = __esm({
9391
10196
  }
9392
10197
  });
9393
10198
 
10199
+ // src/shields/build.ts
10200
+ function escapeRegex(s) {
10201
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10202
+ }
10203
+ function slug(s) {
10204
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
10205
+ }
10206
+ function pathToRegexFragment(rawPath) {
10207
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
10208
+ const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
10209
+ if (segments2.length === 0) return "";
10210
+ return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
10211
+ }
10212
+ function pathRules(rawPath, verdict, reason) {
10213
+ const value = pathToRegexFragment(rawPath);
10214
+ if (!value) return [];
10215
+ const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
10216
+ const s = slug(rawPath);
10217
+ return [
10218
+ // `reading: 'access'`: a script's interpreter line and the program a
10219
+ // command runs are not reads of the jailed path (node9Firewall#385).
10220
+ {
10221
+ name: `${verdict}-path-${s}-bash`,
10222
+ tool: "bash",
10223
+ conditions: [{ field: "command", op: "matches", value, reading: "access" }],
10224
+ verdict,
10225
+ reason: why
10226
+ },
10227
+ // Keep the historical `-anytool` name for the file_path rule: the
10228
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
10229
+ {
10230
+ name: `${verdict}-path-${s}-anytool`,
10231
+ tool: "*",
10232
+ conditions: [{ field: "file_path", op: "matches", value }],
10233
+ verdict,
10234
+ reason: why
10235
+ },
10236
+ {
10237
+ name: `${verdict}-path-${s}-anytool-path`,
10238
+ tool: "*",
10239
+ conditions: [{ field: "path", op: "matches", value }],
10240
+ verdict,
10241
+ reason: why
10242
+ },
10243
+ {
10244
+ name: `${verdict}-path-${s}-anytool-pattern`,
10245
+ tool: "*",
10246
+ conditions: [{ field: "pattern", op: "matches", value }],
10247
+ verdict,
10248
+ reason: why
10249
+ }
10250
+ ];
10251
+ }
10252
+ function withAccessReading(def) {
10253
+ let changed = false;
10254
+ const smartRules = def.smartRules.map((r) => {
10255
+ const [c, ...rest] = r.conditions ?? [];
10256
+ if (rest.length > 0 || !c || r.tool !== "bash" || !LEGACY_BASH_PATH_RULE.test(r.name ?? "") || c.field !== "command" || c.op !== "matches" || c.reading !== void 0) {
10257
+ return r;
10258
+ }
10259
+ changed = true;
10260
+ return { ...r, conditions: [{ ...c, reading: "access" }] };
10261
+ });
10262
+ return changed ? { ...def, smartRules } : def;
10263
+ }
10264
+ var B, SEP, LEGACY_BASH_PATH_RULE;
10265
+ var init_build = __esm({
10266
+ "src/shields/build.ts"() {
10267
+ "use strict";
10268
+ init_dist();
10269
+ B = "[\\s/\\\\]";
10270
+ SEP = "[/\\\\]";
10271
+ LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
10272
+ }
10273
+ });
10274
+
9394
10275
  // src/shields.ts
9395
10276
  import fs5 from "fs";
9396
10277
  import path6 from "path";
@@ -9419,7 +10300,7 @@ function loadUserShields() {
9419
10300
  `);
9420
10301
  continue;
9421
10302
  }
9422
- result[v.ok.name] = v.ok;
10303
+ result[v.ok.name] = withAccessReading(v.ok);
9423
10304
  } catch (err) {
9424
10305
  process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
9425
10306
  `);
@@ -9475,6 +10356,7 @@ var init_shields = __esm({
9475
10356
  "src/shields.ts"() {
9476
10357
  "use strict";
9477
10358
  init_dist();
10359
+ init_build();
9478
10360
  init_dist();
9479
10361
  USER_SHIELDS_DIR = path6.join(os6.homedir(), ".node9", "shields");
9480
10362
  SHIELDS = buildSHIELDS();
@@ -9610,67 +10492,6 @@ var init_managed = __esm({
9610
10492
  }
9611
10493
  });
9612
10494
 
9613
- // src/shields/build.ts
9614
- function escapeRegex(s) {
9615
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9616
- }
9617
- function slug(s) {
9618
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
9619
- }
9620
- function pathToRegexFragment(rawPath) {
9621
- const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
9622
- const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
9623
- if (segments2.length === 0) return "";
9624
- return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
9625
- }
9626
- function pathRules(rawPath, verdict, reason) {
9627
- const value = pathToRegexFragment(rawPath);
9628
- if (!value) return [];
9629
- const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
9630
- const s = slug(rawPath);
9631
- return [
9632
- {
9633
- name: `${verdict}-path-${s}-bash`,
9634
- tool: "bash",
9635
- conditions: [{ field: "command", op: "matches", value }],
9636
- verdict,
9637
- reason: why
9638
- },
9639
- // Keep the historical `-anytool` name for the file_path rule: the
9640
- // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
9641
- {
9642
- name: `${verdict}-path-${s}-anytool`,
9643
- tool: "*",
9644
- conditions: [{ field: "file_path", op: "matches", value }],
9645
- verdict,
9646
- reason: why
9647
- },
9648
- {
9649
- name: `${verdict}-path-${s}-anytool-path`,
9650
- tool: "*",
9651
- conditions: [{ field: "path", op: "matches", value }],
9652
- verdict,
9653
- reason: why
9654
- },
9655
- {
9656
- name: `${verdict}-path-${s}-anytool-pattern`,
9657
- tool: "*",
9658
- conditions: [{ field: "pattern", op: "matches", value }],
9659
- verdict,
9660
- reason: why
9661
- }
9662
- ];
9663
- }
9664
- var B, SEP;
9665
- var init_build = __esm({
9666
- "src/shields/build.ts"() {
9667
- "use strict";
9668
- init_dist();
9669
- B = "[\\s/\\\\]";
9670
- SEP = "[/\\\\]";
9671
- }
9672
- });
9673
-
9674
10495
  // src/auth/trusted-hosts.ts
9675
10496
  function normalizeHost2(raw) {
9676
10497
  return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
@@ -10029,6 +10850,7 @@ function getConfig(cwd) {
10029
10850
  trustedHosts: [],
10030
10851
  trustedHostsManaged: false,
10031
10852
  appPermissions: {},
10853
+ appLinks: {},
10032
10854
  managedJailPaths: []
10033
10855
  };
10034
10856
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
@@ -10426,6 +11248,13 @@ function getConfig(cwd) {
10426
11248
  mergedPolicy.trustedHostsManaged = true;
10427
11249
  mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
10428
11250
  }
11251
+ if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
11252
+ const links = {};
11253
+ for (const [key, slug2] of Object.entries(mc.appLinks)) {
11254
+ if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
11255
+ }
11256
+ mergedPolicy.appLinks = links;
11257
+ }
10429
11258
  if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
10430
11259
  const coerced = {};
10431
11260
  for (const [srv, tools] of Object.entries(mc.appPermissions)) {
@@ -10726,6 +11555,7 @@ var init_config = __esm({
10726
11555
  init_build();
10727
11556
  init_trusted_hosts();
10728
11557
  init_dist();
11558
+ init_dist();
10729
11559
  init_v2();
10730
11560
  init_dist();
10731
11561
  init_dist();
@@ -10859,7 +11689,9 @@ var init_config = __esm({
10859
11689
  op: "matches",
10860
11690
  // Single spaces on purpose: a nested `\\s+` inside the alternation
10861
11691
  // fails the engine's ReDoS check and the rule would be dropped.
10862
- value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|egress allow|approver set)\\b",
11692
+ // `apps`: turning an app off, allowing one of its actions, or
11693
+ // linking a server to an app (the general checks leave it).
11694
+ value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|apps (link|[a-z0-9-]{1,32} (off|[a-z]{1,32} allow))|egress allow|approver set)\\b",
10863
11695
  flags: "i"
10864
11696
  }
10865
11697
  ],
@@ -10945,7 +11777,12 @@ var init_config = __esm({
10945
11777
  verdict: "block",
10946
11778
  reason: "Piping remote script into a shell is a supply-chain attack vector",
10947
11779
  description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
10948
- }
11780
+ },
11781
+ // ── Actions in apps (node9Firewall#390) ───────────────────────────────
11782
+ // actions.money / actions.access / actions.publish: the action templates
11783
+ // on every MCP server no app shield claims, plus the shell publishing
11784
+ // rules. Review by default; the check value governs them.
11785
+ ...GENERAL_ACTION_RULES
10949
11786
  ],
10950
11787
  dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
10951
11788
  egress: {
@@ -10982,6 +11819,7 @@ var init_config = __esm({
10982
11819
  trustedHosts: [],
10983
11820
  trustedHostsManaged: false,
10984
11821
  appPermissions: {},
11822
+ appLinks: {},
10985
11823
  managedJailPaths: []
10986
11824
  },
10987
11825
  environments: {}
@@ -13532,7 +14370,7 @@ function loadShieldStatus() {
13532
14370
  cfg.policySource === "workspace" ? cfg.policy.appliedShields ?? [] : readActiveShields()
13533
14371
  );
13534
14372
  const active = all.filter((n) => activeSet.has(n));
13535
- const inactive = all.filter((n) => !activeSet.has(n) && !ACTION_SHIELD_NAMES.has(n));
14373
+ const inactive = all.filter((n) => !activeSet.has(n) && !APP_SHIELD_NAMES.has(n));
13536
14374
  return { active, inactive };
13537
14375
  } catch {
13538
14376
  return { active: [], inactive: [] };