@node9/proxy 2.37.1 → 2.39.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
  }
@@ -320,6 +415,28 @@ function classifySsrf(host2) {
320
415
  return null;
321
416
  }
322
417
  }
418
+ function isModeValue(v) {
419
+ return typeof v === "string" && MODE_VALUES.includes(v);
420
+ }
421
+ function levelOf(mode) {
422
+ if (mode === "observe") return "audit";
423
+ return LEVELS.includes(mode ?? "") ? mode : "standard";
424
+ }
425
+ function isRecordOnly(mode) {
426
+ return levelOf(mode) === "audit";
427
+ }
428
+ function isBlockAll(mode) {
429
+ return levelOf(mode) === "block-all";
430
+ }
431
+ function levelCheckValue(mode, id) {
432
+ return LEVEL_CHECKS[levelOf(mode)][id];
433
+ }
434
+ function levelChecks(mode) {
435
+ return LEVEL_CHECKS[levelOf(mode)];
436
+ }
437
+ function levelPackageSignals(mode) {
438
+ return levelOf(mode) === "soft" ? { newPackage: "off", installScript: "off" } : void 0;
439
+ }
323
440
  function isVerdict(v) {
324
441
  return v === "off" || v === "log" || v === "review" || v === "block";
325
442
  }
@@ -393,19 +510,21 @@ function resolveCheck(settings, id) {
393
510
  if (!def) return void 0;
394
511
  if (isLockedCheck(def)) return { id, value: "block", source: "locked" };
395
512
  const floor = def.floor ? CHECK_VERDICT_RANK[def.floor] : 0;
513
+ const levelDefault = levelCheckValue(settings.mode, id) ?? def.defaultValue;
514
+ const sourceOf = (value2) => value2 === levelDefault ? "default" : "configured";
396
515
  const explicit = settings.checks?.[id];
397
516
  if (isVerdict(explicit)) {
398
517
  const value2 = CHECK_VERDICT_RANK[explicit] < floor ? def.floor : explicit;
399
- return { id, value: value2, source: value2 === def.defaultValue ? "default" : "configured" };
518
+ return { id, value: value2, source: sourceOf(value2) };
400
519
  }
401
520
  if (def.pack) {
402
521
  const on = settings.appliedShields?.includes(def.pack) ?? false;
403
522
  return { id, value: on ? def.defaultValue : "off", source: "default" };
404
523
  }
405
524
  const read = LEGACY_READERS[id]?.(settings);
406
- if (read === void 0) return { id, value: def.defaultValue, source: "default" };
525
+ if (read === void 0) return { id, value: levelDefault, source: "default" };
407
526
  const value = CHECK_VERDICT_RANK[read] < floor ? def.floor : read;
408
- return { id, value, source: value === def.defaultValue ? "default" : "configured" };
527
+ return { id, value, source: sourceOf(value) };
409
528
  }
410
529
  function checkValue(settings, id) {
411
530
  return resolveCheck(settings, id)?.value ?? "off";
@@ -486,6 +605,55 @@ function cachedNormalize(command, compute) {
486
605
  function normalizeCommandForPolicy(command) {
487
606
  return commandReadingsImpl(command).posix;
488
607
  }
608
+ function fileAccessView(command) {
609
+ const hit = accessViewCache.get(command);
610
+ if (hit !== void 0) return hit;
611
+ const view = fileAccessViewImpl(command);
612
+ if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
613
+ const oldest = accessViewCache.keys().next().value;
614
+ if (oldest !== void 0) accessViewCache.delete(oldest);
615
+ }
616
+ accessViewCache.set(command, view);
617
+ return view;
618
+ }
619
+ function fileAccessViewImpl(command) {
620
+ const text = command.replace(INTERPRETER_LINE, "");
621
+ const f = parseShared(text);
622
+ if (f === PARSE_FAIL) return text;
623
+ const toCharIndex = byteOffsetToCharIndex(text);
624
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
625
+ const edits = [];
626
+ try {
627
+ syntax.Walk(f, (node) => {
628
+ if (!node) return false;
629
+ const n = node;
630
+ if (syntax.NodeType(n) !== "CallExpr") return true;
631
+ const args = n.Args || [];
632
+ const words = args.map((a) => resolveWordLiteral(a));
633
+ const head = unwrapCommandHead(words);
634
+ for (const i of /* @__PURE__ */ new Set([0, head])) {
635
+ const word = words[i];
636
+ if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
637
+ continue;
638
+ }
639
+ if (!PROGRAM_DIR.test(word)) continue;
640
+ const name = word.split(/[\\/]/).pop() ?? "";
641
+ if (!name) continue;
642
+ const s = at(args[i].Pos().Offset());
643
+ const e = at(args[i].End().Offset());
644
+ if (s < 0 || e < 0) continue;
645
+ edits.push([s, e, name]);
646
+ }
647
+ return true;
648
+ });
649
+ } catch {
650
+ return text;
651
+ }
652
+ edits.sort((a, b) => b[0] - a[0]);
653
+ let out = text;
654
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
655
+ return out;
656
+ }
489
657
  function commandReadings(command) {
490
658
  const r = commandReadingsImpl(command);
491
659
  const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
@@ -1728,40 +1896,6 @@ function jailedRedirectRead(stmt) {
1728
1896
  }
1729
1897
  return null;
1730
1898
  }
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
1899
  function matchesPattern(text, patterns) {
1766
1900
  const p = Array.isArray(patterns) ? patterns : [patterns];
1767
1901
  if (p.length === 0) return false;
@@ -1794,23 +1928,26 @@ function evaluateSmartConditions(args, rule) {
1794
1928
  return val;
1795
1929
  };
1796
1930
  const readingsCache = /* @__PURE__ */ new Map();
1797
- const resolveFieldReadings = (field) => {
1798
- const cached = readingsCache.get(field);
1931
+ const resolveFieldReadings = (field, reading) => {
1932
+ const access = reading === "access" && field === "command";
1933
+ const key = access ? `${field}\0access` : field;
1934
+ const cached = readingsCache.get(key);
1799
1935
  if (cached) return cached;
1800
1936
  const primary = resolveField(field);
1801
1937
  if (primary === null) {
1802
- readingsCache.set(field, []);
1938
+ readingsCache.set(key, []);
1803
1939
  return [];
1804
1940
  }
1805
1941
  let out = [primary];
1806
1942
  if (field === "command") {
1807
1943
  const raw = getNestedValue(args, field);
1808
1944
  if (typeof raw === "string") {
1809
- const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
1810
- out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
1945
+ const text = access ? fileAccessView(raw) : raw;
1946
+ const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
1947
+ out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
1811
1948
  }
1812
1949
  }
1813
- readingsCache.set(field, out);
1950
+ readingsCache.set(key, out);
1814
1951
  return out;
1815
1952
  };
1816
1953
  const results = rule.conditions.map((cond) => {
@@ -1828,14 +1965,14 @@ function evaluateSmartConditions(args, rule) {
1828
1965
  if (val === null || !cond.value) return false;
1829
1966
  const reM = getCompiledRegex(cond.value, cond.flags ?? "");
1830
1967
  if (!reM) return false;
1831
- return resolveFieldReadings(cond.field).some((v) => reM.test(v));
1968
+ return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
1832
1969
  }
1833
1970
  case "notMatches": {
1834
1971
  if (!cond.value) return false;
1835
1972
  if (val === null) return true;
1836
1973
  const reN = getCompiledRegex(cond.value, cond.flags ?? "");
1837
1974
  if (!reN) return false;
1838
- return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
1975
+ return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
1839
1976
  }
1840
1977
  case "matchesGlob":
1841
1978
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
@@ -2218,14 +2355,14 @@ function validateOtpSecret(raw) {
2218
2355
  }
2219
2356
  function assertBuiltinPatternsAreSafe() {
2220
2357
  for (const p of DLP_PATTERNS) {
2221
- if (!safeRegex3(p.regex.source)) {
2358
+ if (!safeRegex2(p.regex.source)) {
2222
2359
  throw new Error(
2223
2360
  `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
2224
2361
  );
2225
2362
  }
2226
2363
  }
2227
2364
  for (const re of SENSITIVE_PATH_PATTERNS) {
2228
- if (!safeRegex3(re.source)) {
2365
+ if (!safeRegex2(re.source)) {
2229
2366
  throw new Error(
2230
2367
  `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
2231
2368
  );
@@ -2319,6 +2456,20 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
2319
2456
  }
2320
2457
  return null;
2321
2458
  }
2459
+ function tuningFromLegacyPolicy(policy) {
2460
+ const out = {};
2461
+ const p = policy;
2462
+ if (policy.packageCheck?.registrySignals === false) {
2463
+ out["loading.malicious-package"] = { newPackage: "off", installScript: "off" };
2464
+ }
2465
+ for (const f of TUNING_FIELDS) {
2466
+ const [block, key] = f.legacy;
2467
+ const value = p[block]?.[key];
2468
+ if (value === void 0) continue;
2469
+ (out[f.checkId] ??= {})[f.name] = value;
2470
+ }
2471
+ return out;
2472
+ }
2322
2473
  function tuningToLegacyPolicy(policy, tuning) {
2323
2474
  const unknown = [];
2324
2475
  for (const [id, fields] of Object.entries(tuning)) {
@@ -2433,6 +2584,25 @@ function checkToLegacyPolicy(policy, id, v) {
2433
2584
  return false;
2434
2585
  }
2435
2586
  }
2587
+ function knobStatedChecks(policy) {
2588
+ const out = /* @__PURE__ */ new Set();
2589
+ const has2 = (o, key) => !!o && typeof o === "object" && o[key] !== void 0;
2590
+ const { dlp, egress, loopDetection, injectionScan, skillPinning, packageCheck } = policy;
2591
+ const uc = policy.untrustedContent;
2592
+ if (has2(dlp, "enabled")) out.add("data.secrets");
2593
+ if (has2(dlp, "reviewAction") || dlp?.enabled === false) out.add("data.secrets-weak");
2594
+ if (has2(dlp, "pii")) out.add("data.pii");
2595
+ if (has2(egress, "enabled") || has2(egress, "mode")) out.add("network.unknown-host");
2596
+ if (has2(egress, "ssrfStrict")) out.add("network.internal-addresses");
2597
+ if (has2(loopDetection, "enabled")) out.add("behavior.loops");
2598
+ if (has2(injectionScan, "enabled")) out.add("behavior.prompt-injection");
2599
+ if (has2(skillPinning, "enabled") || has2(skillPinning, "mode")) out.add("loading.skill-tamper");
2600
+ if (has2(packageCheck, "enabled") || has2(packageCheck, "onMalicious"))
2601
+ out.add("loading.malicious-package");
2602
+ if (uc && typeof uc === "object" && (uc.enabled === false || has2(uc, "afterExposure")))
2603
+ out.add("behavior.untrusted-content");
2604
+ return out;
2605
+ }
2436
2606
  function isKnobGoverned(id) {
2437
2607
  return id in KNOB_GOVERNED;
2438
2608
  }
@@ -2691,10 +2861,13 @@ function matchCanaryArgs(args, values) {
2691
2861
  return null;
2692
2862
  }
2693
2863
  }
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;
2864
+ 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, LEVELS, MODE_VALUES, SOFT, STRICT, BLOCK_ALL, LEVEL_CHECKS, 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
2865
  var init_dist = __esm({
2696
2866
  "packages/policy-engine/dist/index.mjs"() {
2697
2867
  "use strict";
2868
+ MAX_REGEX_LENGTH = 256;
2869
+ REGEX_CACHE_MAX = 500;
2870
+ regexCache = /* @__PURE__ */ new Map();
2698
2871
  aws_default = {
2699
2872
  name: "aws",
2700
2873
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -2996,77 +3169,6 @@ var init_dist = __esm({
2996
3169
  ],
2997
3170
  dangerousWords: ["wipefs"]
2998
3171
  };
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
3172
  github_default = {
3071
3173
  name: "github",
3072
3174
  description: "Protects GitHub repositories from destructive AI operations",
@@ -3093,56 +3195,6 @@ var init_dist = __esm({
3093
3195
  ],
3094
3196
  dangerousWords: []
3095
3197
  };
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
3198
  k8s_default = {
3147
3199
  name: "k8s",
3148
3200
  description: "Protects Kubernetes clusters from destructive AI operations",
@@ -3395,7 +3447,7 @@ var init_dist = __esm({
3395
3447
  {
3396
3448
  field: "command",
3397
3449
  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|$|[;&|>)<])",
3450
+ 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
3451
  flags: "i"
3400
3452
  }
3401
3453
  ],
@@ -3409,7 +3461,7 @@ var init_dist = __esm({
3409
3461
  {
3410
3462
  field: "command",
3411
3463
  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)",
3464
+ 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
3465
  flags: "i"
3414
3466
  }
3415
3467
  ],
@@ -3475,15 +3527,215 @@ var init_dist = __esm({
3475
3527
  ],
3476
3528
  dangerousWords: []
3477
3529
  };
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"],
3530
+ redis_default = {
3531
+ name: "redis",
3532
+ description: "Protects Redis instances from destructive AI operations",
3533
+ aliases: [],
3534
+ _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
3535
  smartRules: [
3483
3536
  {
3484
- name: "shield:publishing:review-release",
3485
- tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
3486
- conditions: [],
3537
+ name: "shield:redis:block-flushall",
3538
+ tool: "*",
3539
+ conditions: [
3540
+ {
3541
+ field: "command",
3542
+ op: "matches",
3543
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
3544
+ flags: "i"
3545
+ }
3546
+ ],
3547
+ verdict: "block",
3548
+ reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
3549
+ },
3550
+ {
3551
+ name: "shield:redis:block-flushdb",
3552
+ tool: "*",
3553
+ conditions: [
3554
+ {
3555
+ field: "command",
3556
+ op: "matches",
3557
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
3558
+ flags: "i"
3559
+ }
3560
+ ],
3561
+ verdict: "block",
3562
+ reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
3563
+ },
3564
+ {
3565
+ name: "shield:redis:block-config-resetstat",
3566
+ tool: "*",
3567
+ conditions: [
3568
+ {
3569
+ field: "command",
3570
+ op: "matches",
3571
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
3572
+ flags: "i"
3573
+ }
3574
+ ],
3575
+ verdict: "block",
3576
+ reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
3577
+ },
3578
+ {
3579
+ name: "shield:redis:review-config-set",
3580
+ tool: "*",
3581
+ conditions: [
3582
+ {
3583
+ field: "command",
3584
+ op: "matches",
3585
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
3586
+ flags: "i"
3587
+ }
3588
+ ],
3589
+ verdict: "review",
3590
+ reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
3591
+ },
3592
+ {
3593
+ name: "shield:redis:review-del-wildcard",
3594
+ tool: "*",
3595
+ conditions: [
3596
+ {
3597
+ field: "command",
3598
+ op: "matches",
3599
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
3600
+ flags: "i"
3601
+ }
3602
+ ],
3603
+ verdict: "review",
3604
+ reason: "Wildcard key deletion requires human approval (Redis shield)"
3605
+ }
3606
+ ],
3607
+ dangerousWords: ["FLUSHALL", "FLUSHDB"]
3608
+ };
3609
+ finance_default = {
3610
+ name: "finance",
3611
+ 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.",
3612
+ aliases: ["payments", "money"],
3613
+ smartRules: [
3614
+ {
3615
+ name: "shield:finance:review-refund",
3616
+ tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
3617
+ conditions: [],
3618
+ verdict: "review",
3619
+ reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
3620
+ },
3621
+ {
3622
+ name: "shield:finance:review-payout",
3623
+ tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
3624
+ conditions: [],
3625
+ verdict: "review",
3626
+ reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
3627
+ },
3628
+ {
3629
+ name: "shield:finance:review-transfer",
3630
+ tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
3631
+ conditions: [],
3632
+ verdict: "review",
3633
+ reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
3634
+ },
3635
+ {
3636
+ name: "shield:finance:review-charge",
3637
+ 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}",
3638
+ conditions: [],
3639
+ verdict: "review",
3640
+ reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
3641
+ },
3642
+ {
3643
+ name: "shield:finance:review-payment-link",
3644
+ tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
3645
+ conditions: [],
3646
+ verdict: "review",
3647
+ reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
3648
+ },
3649
+ {
3650
+ name: "shield:finance:review-subscription",
3651
+ tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
3652
+ conditions: [],
3653
+ verdict: "review",
3654
+ reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
3655
+ },
3656
+ {
3657
+ name: "shield:finance:review-dispute",
3658
+ tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
3659
+ conditions: [],
3660
+ verdict: "review",
3661
+ reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
3662
+ },
3663
+ {
3664
+ name: "shield:finance:review-card",
3665
+ tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
3666
+ conditions: [],
3667
+ verdict: "review",
3668
+ reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
3669
+ },
3670
+ {
3671
+ name: "shield:finance:review-withdraw",
3672
+ tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
3673
+ conditions: [],
3674
+ verdict: "review",
3675
+ reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
3676
+ }
3677
+ ],
3678
+ dangerousWords: []
3679
+ };
3680
+ identity_default = {
3681
+ name: "identity",
3682
+ 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.",
3683
+ aliases: ["access", "iam"],
3684
+ smartRules: [
3685
+ {
3686
+ name: "shield:identity:review-password",
3687
+ tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
3688
+ conditions: [],
3689
+ verdict: "review",
3690
+ reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
3691
+ },
3692
+ {
3693
+ name: "shield:identity:review-key",
3694
+ 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}",
3695
+ conditions: [],
3696
+ verdict: "review",
3697
+ reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
3698
+ },
3699
+ {
3700
+ name: "shield:identity:review-access",
3701
+ 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}",
3702
+ conditions: [],
3703
+ verdict: "review",
3704
+ reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
3705
+ },
3706
+ {
3707
+ name: "shield:identity:review-membership",
3708
+ 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}",
3709
+ conditions: [],
3710
+ verdict: "review",
3711
+ reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
3712
+ },
3713
+ {
3714
+ name: "shield:identity:review-user",
3715
+ 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}",
3716
+ conditions: [],
3717
+ verdict: "review",
3718
+ reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
3719
+ },
3720
+ {
3721
+ name: "shield:identity:review-mfa",
3722
+ tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
3723
+ conditions: [],
3724
+ verdict: "review",
3725
+ reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
3726
+ }
3727
+ ],
3728
+ dangerousWords: []
3729
+ };
3730
+ publishing_default = {
3731
+ name: "publishing",
3732
+ 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.",
3733
+ aliases: ["release", "deploy"],
3734
+ smartRules: [
3735
+ {
3736
+ name: "shield:publishing:review-release",
3737
+ tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
3738
+ conditions: [],
3487
3739
  verdict: "review",
3488
3740
  reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
3489
3741
  },
@@ -3644,109 +3896,511 @@ var init_dist = __esm({
3644
3896
  ],
3645
3897
  dangerousWords: []
3646
3898
  };
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"]
3899
+ ACTION_TEMPLATES = {
3900
+ finance: finance_default.smartRules,
3901
+ identity: identity_default.smartRules,
3902
+ publishing: publishing_default.smartRules
3903
+ };
3904
+ ACTION_CHECKS = {
3905
+ finance: { checkId: "actions.money", slug: "money", label: "Money check" },
3906
+ identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
3907
+ publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
3908
+ };
3909
+ GAP = "[^;&|]{0,200}?";
3910
+ prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
3911
+ httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
3912
+ httpWrite = (methods) => ({
3913
+ field: "command",
3914
+ op: "matches",
3915
+ 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)`
3916
+ });
3917
+ HTTP_WRITE = httpWrite("POST|DELETE");
3918
+ HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
3919
+ NOT_STRIPE_TEST = {
3920
+ field: "command",
3921
+ op: "notMatches",
3922
+ value: "\\b[sr]k_test_"
3725
3923
  };
3924
+ postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
3925
+ STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
3926
+ APP_SHIELD_SPECS = [
3927
+ {
3928
+ name: "stripe",
3929
+ label: "Stripe",
3930
+ 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.",
3931
+ template: "finance",
3932
+ server: "*stripe*",
3933
+ doors: [
3934
+ {
3935
+ name: "shield:stripe:review-cli",
3936
+ tool: "bash",
3937
+ conditions: [
3938
+ {
3939
+ field: "command",
3940
+ op: "matches",
3941
+ value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
3942
+ flags: "i"
3943
+ },
3944
+ {
3945
+ field: "command",
3946
+ op: "matches",
3947
+ // The stripe CLI works in test mode unless told otherwise.
3948
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
3949
+ }
3950
+ ],
3951
+ conditionMode: "all",
3952
+ verdict: "review",
3953
+ reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
3954
+ },
3955
+ {
3956
+ name: "shield:stripe:review-cli-request",
3957
+ tool: "bash",
3958
+ conditions: [
3959
+ {
3960
+ field: "command",
3961
+ op: "matches",
3962
+ value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
3963
+ flags: "i"
3964
+ },
3965
+ {
3966
+ field: "command",
3967
+ op: "matches",
3968
+ // The stripe CLI works in test mode unless told otherwise.
3969
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
3970
+ }
3971
+ ],
3972
+ conditionMode: "all",
3973
+ verdict: "review",
3974
+ reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
3975
+ },
3976
+ {
3977
+ name: "shield:stripe:review-api",
3978
+ tool: "bash",
3979
+ conditions: [
3980
+ {
3981
+ field: "command",
3982
+ op: "matches",
3983
+ value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
3984
+ flags: "i"
3985
+ },
3986
+ HTTP_WRITE,
3987
+ NOT_STRIPE_TEST
3988
+ ],
3989
+ conditionMode: "all",
3990
+ verdict: "review",
3991
+ reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
3992
+ },
3993
+ {
3994
+ name: "shield:stripe:review-code",
3995
+ tool: "bash",
3996
+ conditions: [
3997
+ {
3998
+ field: "command",
3999
+ op: "matches",
4000
+ // Python: stripe.Refund.create(...)
4001
+ value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
4002
+ },
4003
+ NOT_STRIPE_TEST
4004
+ ],
4005
+ conditionMode: "all",
4006
+ verdict: "review",
4007
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
4008
+ },
4009
+ {
4010
+ name: "shield:stripe:review-code-node",
4011
+ tool: "bash",
4012
+ conditions: [
4013
+ {
4014
+ field: "command",
4015
+ op: "matches",
4016
+ // Node: stripe.refunds.create(...)
4017
+ value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
4018
+ },
4019
+ NOT_STRIPE_TEST
4020
+ ],
4021
+ conditionMode: "all",
4022
+ verdict: "review",
4023
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
4024
+ },
4025
+ {
4026
+ name: "shield:stripe:review-code-http",
4027
+ tool: "bash",
4028
+ conditions: [
4029
+ {
4030
+ field: "command",
4031
+ op: "matches",
4032
+ // requests.post("https://api.stripe.com/v1/refunds", ...)
4033
+ value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
4034
+ },
4035
+ NOT_STRIPE_TEST
4036
+ ],
4037
+ conditionMode: "all",
4038
+ verdict: "review",
4039
+ reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
4040
+ }
4041
+ ]
4042
+ },
4043
+ {
4044
+ name: "paypal",
4045
+ label: "PayPal",
4046
+ 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.",
4047
+ template: "finance",
4048
+ server: "*paypal*",
4049
+ doors: [
4050
+ {
4051
+ name: "shield:paypal:review-api",
4052
+ tool: "bash",
4053
+ conditions: [
4054
+ {
4055
+ field: "command",
4056
+ op: "matches",
4057
+ // The live hosts only: api.paypal.com and api-m.paypal.com. The
4058
+ // sandbox (api-m.sandbox.paypal.com) moves no money.
4059
+ value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
4060
+ flags: "i"
4061
+ },
4062
+ {
4063
+ field: "command",
4064
+ op: "matches",
4065
+ // The endpoints that move money.
4066
+ value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
4067
+ flags: "i"
4068
+ },
4069
+ HTTP_WRITE
4070
+ ],
4071
+ conditionMode: "all",
4072
+ verdict: "review",
4073
+ reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
4074
+ },
4075
+ {
4076
+ name: "shield:paypal:review-code",
4077
+ tool: "bash",
4078
+ conditions: [
4079
+ {
4080
+ field: "command",
4081
+ op: "matches",
4082
+ // The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
4083
+ // Python paypalrestsdk.
4084
+ value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
4085
+ }
4086
+ ],
4087
+ verdict: "review",
4088
+ reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
4089
+ },
4090
+ {
4091
+ name: "shield:paypal:review-code-http",
4092
+ tool: "bash",
4093
+ conditions: [
4094
+ {
4095
+ field: "command",
4096
+ op: "matches",
4097
+ // The endpoints that move money; the token call every script
4098
+ // starts with (/v1/oauth2/token) is not one.
4099
+ value: postTo(
4100
+ "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)"
4101
+ )
4102
+ }
4103
+ ],
4104
+ verdict: "review",
4105
+ reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
4106
+ }
4107
+ ]
4108
+ },
4109
+ {
4110
+ name: "square",
4111
+ label: "Square",
4112
+ 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.",
4113
+ template: "finance",
4114
+ // Square's servers, never Squarespace (a website builder).
4115
+ server: "{*square,*square[-_]*,*squareup*}",
4116
+ mcpTools: [
4117
+ {
4118
+ // Square's official MCP server has one tool for every API call; the
4119
+ // service and method name what it does.
4120
+ name: "shield:square:review-mcp-request",
4121
+ tool: "{,mcp__*__}make_api_request",
4122
+ conditions: [
4123
+ {
4124
+ field: "service",
4125
+ op: "matches",
4126
+ value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
4127
+ flags: "i"
4128
+ },
4129
+ {
4130
+ field: "method",
4131
+ op: "notMatches",
4132
+ value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
4133
+ flags: "i"
4134
+ }
4135
+ ],
4136
+ conditionMode: "all",
4137
+ verdict: "review",
4138
+ reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
4139
+ }
4140
+ ],
4141
+ doors: [
4142
+ {
4143
+ name: "shield:square:review-api",
4144
+ tool: "bash",
4145
+ conditions: [
4146
+ {
4147
+ field: "command",
4148
+ op: "matches",
4149
+ // The live host only; the sandbox is connect.squareupsandbox.com.
4150
+ value: httpTo(
4151
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
4152
+ ),
4153
+ flags: "i"
4154
+ },
4155
+ HTTP_WRITE_ANY
4156
+ ],
4157
+ conditionMode: "all",
4158
+ verdict: "review",
4159
+ reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
4160
+ },
4161
+ {
4162
+ name: "shield:square:review-code",
4163
+ tool: "bash",
4164
+ conditions: [
4165
+ {
4166
+ field: "command",
4167
+ op: "matches",
4168
+ // The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
4169
+ value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
4170
+ },
4171
+ {
4172
+ field: "command",
4173
+ op: "matches",
4174
+ 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*\\("
4175
+ }
4176
+ ],
4177
+ conditionMode: "all",
4178
+ verdict: "review",
4179
+ reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
4180
+ },
4181
+ {
4182
+ name: "shield:square:review-code-http",
4183
+ tool: "bash",
4184
+ conditions: [
4185
+ {
4186
+ field: "command",
4187
+ op: "matches",
4188
+ value: postTo(
4189
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
4190
+ )
4191
+ }
4192
+ ],
4193
+ verdict: "review",
4194
+ reason: "This code posts to the Square API. node9 asks before money moves (Square)."
4195
+ }
4196
+ ]
4197
+ },
4198
+ {
4199
+ name: "plaid",
4200
+ label: "Plaid",
4201
+ 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.",
4202
+ template: "finance",
4203
+ server: "*plaid*",
4204
+ doors: [
4205
+ {
4206
+ name: "shield:plaid:review-api",
4207
+ tool: "bash",
4208
+ conditions: [
4209
+ {
4210
+ field: "command",
4211
+ op: "matches",
4212
+ // Every Plaid endpoint is a POST, so the path is what tells a
4213
+ // write; the live host only, never sandbox.plaid.com. A script
4214
+ // that posts to the URL meets this door too.
4215
+ value: httpTo(
4216
+ "production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
4217
+ ),
4218
+ flags: "i"
4219
+ }
4220
+ ],
4221
+ verdict: "review",
4222
+ reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
4223
+ },
4224
+ {
4225
+ name: "shield:plaid:review-code",
4226
+ tool: "bash",
4227
+ conditions: [
4228
+ { field: "command", op: "matches", value: "\\bplaid", flags: "i" },
4229
+ {
4230
+ field: "command",
4231
+ op: "matches",
4232
+ value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
4233
+ }
4234
+ ],
4235
+ conditionMode: "all",
4236
+ verdict: "review",
4237
+ reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
4238
+ }
4239
+ ]
4240
+ },
4241
+ {
4242
+ name: "okta",
4243
+ label: "Okta",
4244
+ 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.",
4245
+ template: "identity",
4246
+ server: "*okta*",
4247
+ doors: [
4248
+ {
4249
+ name: "shield:okta:review-api",
4250
+ tool: "bash",
4251
+ conditions: [
4252
+ {
4253
+ field: "command",
4254
+ op: "matches",
4255
+ // The management API only: sign-in (/api/v1/authn, /sessions,
4256
+ // /oauth2) is not under these paths.
4257
+ value: httpTo(
4258
+ "[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
4259
+ ),
4260
+ flags: "i"
4261
+ },
4262
+ HTTP_WRITE_ANY
4263
+ ],
4264
+ conditionMode: "all",
4265
+ verdict: "review",
4266
+ reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
4267
+ },
4268
+ {
4269
+ name: "shield:okta:review-code",
4270
+ tool: "bash",
4271
+ conditions: [
4272
+ {
4273
+ field: "command",
4274
+ op: "matches",
4275
+ // The Okta SDK in the script: a method named createUser alone is
4276
+ // in half the applications ever written.
4277
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
4278
+ },
4279
+ {
4280
+ field: "command",
4281
+ op: "matches",
4282
+ value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
4283
+ }
4284
+ ],
4285
+ conditionMode: "all",
4286
+ verdict: "review",
4287
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
4288
+ },
4289
+ {
4290
+ name: "shield:okta:review-code-py",
4291
+ tool: "bash",
4292
+ conditions: [
4293
+ {
4294
+ field: "command",
4295
+ op: "matches",
4296
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
4297
+ },
4298
+ {
4299
+ field: "command",
4300
+ op: "matches",
4301
+ 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*\\("
4302
+ }
4303
+ ],
4304
+ conditionMode: "all",
4305
+ verdict: "review",
4306
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
4307
+ }
4308
+ ]
4309
+ },
4310
+ {
4311
+ name: "auth0",
4312
+ label: "Auth0",
4313
+ 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.",
4314
+ template: "identity",
4315
+ server: "*auth0*",
4316
+ mcpTools: [
4317
+ {
4318
+ // Auth0's official MCP server: applications (their secrets and
4319
+ // callbacks), APIs and their scopes, and Actions (code that runs at
4320
+ // every sign-in).
4321
+ name: "shield:auth0:review-tenant-config",
4322
+ 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}",
4323
+ conditions: [],
4324
+ verdict: "review",
4325
+ reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
4326
+ }
4327
+ ],
4328
+ doors: [
4329
+ {
4330
+ name: "shield:auth0:review-cli",
4331
+ tool: "bash",
4332
+ conditions: [
4333
+ {
4334
+ field: "command",
4335
+ op: "matches",
4336
+ 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-])",
4337
+ flags: "i"
4338
+ }
4339
+ ],
4340
+ verdict: "review",
4341
+ reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
4342
+ },
4343
+ {
4344
+ name: "shield:auth0:review-api",
4345
+ tool: "bash",
4346
+ conditions: [
4347
+ {
4348
+ field: "command",
4349
+ op: "matches",
4350
+ // The Management API only; /oauth/token is signing in.
4351
+ value: httpTo(
4352
+ "\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
4353
+ ),
4354
+ flags: "i"
4355
+ },
4356
+ HTTP_WRITE_ANY
4357
+ ],
4358
+ conditionMode: "all",
4359
+ verdict: "review",
4360
+ reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
4361
+ },
4362
+ {
4363
+ name: "shield:auth0:review-code",
4364
+ tool: "bash",
4365
+ conditions: [
4366
+ {
4367
+ field: "command",
4368
+ op: "matches",
4369
+ // The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
4370
+ value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
4371
+ },
4372
+ {
4373
+ field: "command",
4374
+ op: "matches",
4375
+ 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*\\("
4376
+ }
4377
+ ],
4378
+ conditionMode: "all",
4379
+ verdict: "review",
4380
+ reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
4381
+ }
4382
+ ]
4383
+ }
4384
+ ];
4385
+ APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
4386
+ APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
4387
+ APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
4388
+ GENERAL_ACTION_RULES = buildGeneralRules();
3726
4389
  BUILTIN_SHIELDS = {
3727
4390
  [aws_default.name]: aws_default,
3728
4391
  [bash_safe_default.name]: bash_safe_default,
3729
4392
  [docker_default.name]: docker_default,
3730
4393
  [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
4394
  [github_default.name]: github_default,
3737
- [identity_default.name]: identity_default,
3738
4395
  [k8s_default.name]: k8s_default,
3739
4396
  [mongodb_default.name]: mongodb_default,
3740
4397
  [postgres_default.name]: postgres_default,
3741
4398
  [project_jail_default.name]: project_jail_default,
3742
- [publishing_default.name]: publishing_default,
3743
- [redis_default.name]: redis_default
4399
+ [redis_default.name]: redis_default,
4400
+ // App shields generated from the action templates (node9Firewall#390): one
4401
+ // per app, every door (MCP server, CLI, API, library), off by default.
4402
+ ...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
3744
4403
  };
3745
- ACTION_SHIELD_NAMES = /* @__PURE__ */ new Set([
3746
- "finance",
3747
- "identity",
3748
- "publishing"
3749
- ]);
3750
4404
  assertBuiltinShieldRegexesAreSafe();
3751
4405
  SSRF_MAX_HOST = 253;
3752
4406
  METADATA_ADDRESSES = /* @__PURE__ */ new Set([
@@ -3792,9 +4446,9 @@ var init_dist = __esm({
3792
4446
  },
3793
4447
  // ── Commands ──────────────────────────────────────────────────────────────
3794
4448
  "commands.inline-exec": {
3795
- plain: "The agent runs a small program written inside the command itself.",
4449
+ 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
4450
  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."
4451
+ 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
4452
  },
3799
4453
  "commands.eval-dynamic": {
3800
4454
  plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
@@ -3826,6 +4480,21 @@ var init_dist = __esm({
3826
4480
  example: "chmod 777 deploy.sh",
3827
4481
  advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
3828
4482
  },
4483
+ "actions.money": {
4484
+ 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.",
4485
+ example: "refund_order on a server called acme-billing",
4486
+ 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."
4487
+ },
4488
+ "actions.access": {
4489
+ plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
4490
+ example: "invite_user or grant_role on a company admin server",
4491
+ advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
4492
+ },
4493
+ "actions.publish": {
4494
+ 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.",
4495
+ example: "create_release or deploy_to_production on a company MCP server",
4496
+ advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
4497
+ },
3829
4498
  "commands.sudo": {
3830
4499
  plain: "The agent runs a command as the administrator (root).",
3831
4500
  example: "sudo apt-get install -y nginx",
@@ -3986,11 +4655,57 @@ var init_dist = __esm({
3986
4655
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
3987
4656
  },
3988
4657
  "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.",
4658
+ 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
4659
  example: "npm install of a package flagged as malware",
3991
4660
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
3992
4661
  }
3993
4662
  };
4663
+ LEVELS = ["audit", "soft", "standard", "strict", "block-all"];
4664
+ MODE_VALUES = ["observe", ...LEVELS];
4665
+ SOFT = {
4666
+ "commands.inline-exec": "log",
4667
+ "commands.eval-dynamic": "log",
4668
+ "commands.rm": "log",
4669
+ "commands.chmod": "log",
4670
+ "commands.sudo": "log",
4671
+ "commands.git-destructive": "log",
4672
+ "commands.sql-ddl": "log",
4673
+ "commands.sql-no-where": "log",
4674
+ "commands.temp-binary": "log",
4675
+ "commands.disk-destroy": "log",
4676
+ "commands.dangerous-word": "log",
4677
+ "files.system-dirs": "log",
4678
+ "files.shell-startup": "log",
4679
+ "files.password-hashes": "log",
4680
+ "data.pipe-chain": "log",
4681
+ "behavior.untrusted-content": "log"
4682
+ };
4683
+ STRICT = {
4684
+ "commands.sql-ddl": "block",
4685
+ "commands.disk-destroy": "block",
4686
+ "commands.node9-disarm": "block",
4687
+ "files.system-dirs": "block",
4688
+ "files.shell-startup": "block",
4689
+ "files.password-hashes": "block",
4690
+ "data.secrets-weak": "block",
4691
+ "data.pipe-chain": "block",
4692
+ "network.unknown-host": "review",
4693
+ "network.internal-addresses": "block",
4694
+ "behavior.prompt-injection": "log",
4695
+ "loading.skill-tamper": "block"
4696
+ };
4697
+ BLOCK_ALL = {
4698
+ ...STRICT,
4699
+ "commands.unknown": "review",
4700
+ "commands.temp-binary": "block"
4701
+ };
4702
+ LEVEL_CHECKS = {
4703
+ audit: {},
4704
+ soft: SOFT,
4705
+ standard: {},
4706
+ strict: STRICT,
4707
+ "block-all": BLOCK_ALL
4708
+ };
3994
4709
  VERDICTS = ["off", "log", "review", "block"];
3995
4710
  CHECK_VERDICT_RANK = { off: 0, log: 1, review: 2, block: 3 };
3996
4711
  ALL = VERDICTS;
@@ -4038,7 +4753,7 @@ var init_dist = __esm({
4038
4753
  id: "commands.inline-exec",
4039
4754
  group: "commands",
4040
4755
  title: "Inline code execution",
4041
- catches: "node -e, python -c, bash -c, a heredoc into an interpreter",
4756
+ 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
4757
  values: ALL,
4043
4758
  defaultValue: "review",
4044
4759
  legacy: ["Node9 Standard (Inline Execution)"]
@@ -4395,6 +5110,38 @@ var init_dist = __esm({
4395
5110
  values: ALL,
4396
5111
  defaultValue: "block",
4397
5112
  legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
5113
+ },
5114
+ // ── Actions in apps (node9Firewall#390) ──────────────────────────────────
5115
+ // The safety net under the app shields: an action that moves money, changes
5116
+ // access or publishes, on an MCP server no app shield claims (an internal
5117
+ // billing server, a vendor with no shield, Zapier). Review by default; the
5118
+ // rules come from the action templates (shields/apps.ts).
5119
+ {
5120
+ id: "actions.money",
5121
+ group: "actions",
5122
+ title: "Money moves",
5123
+ catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
5124
+ values: ALL,
5125
+ defaultValue: "review",
5126
+ legacy: generalActionRuleNames("finance")
5127
+ },
5128
+ {
5129
+ id: "actions.access",
5130
+ group: "actions",
5131
+ title: "Accounts and access change",
5132
+ catches: "a password, key, role, permission or user changed through an app with no protection of its own",
5133
+ values: ALL,
5134
+ defaultValue: "review",
5135
+ legacy: generalActionRuleNames("identity")
5136
+ },
5137
+ {
5138
+ id: "actions.publish",
5139
+ group: "actions",
5140
+ title: "Something is published",
5141
+ catches: "a release, package, deploy or public post through an app with no protection of its own",
5142
+ values: ALL,
5143
+ defaultValue: "review",
5144
+ legacy: generalActionRuleNames("publishing")
4398
5145
  }
4399
5146
  ];
4400
5147
  SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
@@ -4412,7 +5159,7 @@ var init_dist = __esm({
4412
5159
  "commands.rm": (s) => knob(s.commandChecks?.rmAdvisory),
4413
5160
  "commands.chmod": (s) => knob(s.commandChecks?.chmod),
4414
5161
  "commands.sql-ddl": (s) => knob(s.commandChecks?.sqlDdl),
4415
- "commands.unknown": (s) => s.mode === void 0 ? void 0 : s.mode === "strict" ? "review" : "off",
5162
+ "commands.unknown": (s) => s.mode === void 0 ? void 0 : isBlockAll(s.mode) ? "review" : "off",
4416
5163
  "data.secrets": (s) => enabledOr(s.dlp?.enabled, "block"),
4417
5164
  "data.secrets-weak": (s) => {
4418
5165
  if (s.dlp?.enabled === false) return "off";
@@ -4515,6 +5262,170 @@ var init_dist = __esm({
4515
5262
  \u03A7: "X"
4516
5263
  };
4517
5264
  HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
5265
+ SYSTEM_DIRS = [
5266
+ "/usr",
5267
+ "/etc",
5268
+ "/boot",
5269
+ "/bin",
5270
+ "/sbin",
5271
+ "/lib",
5272
+ "/lib64",
5273
+ "/var/log"
5274
+ ];
5275
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
5276
+ LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
5277
+ MAX_INLINE_CODE = 64 * 1024;
5278
+ OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
5279
+ pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
5280
+ either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
5281
+ PYTHON = [
5282
+ [
5283
+ "deletes",
5284
+ /\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/
5285
+ ],
5286
+ [
5287
+ "writes-outside",
5288
+ new RegExp(
5289
+ 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}`
5290
+ )
5291
+ ],
5292
+ [
5293
+ "sends",
5294
+ either(
5295
+ pyImport(
5296
+ String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
5297
+ ),
5298
+ /\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
5299
+ )
5300
+ ],
5301
+ [
5302
+ "listens",
5303
+ either(
5304
+ pyImport(String.raw`pty|socketserver|http\.server`),
5305
+ /\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
5306
+ )
5307
+ ],
5308
+ [
5309
+ "runs-commands",
5310
+ either(
5311
+ pyImport("subprocess|pexpect|commands|psutil"),
5312
+ /\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+\*/
5313
+ )
5314
+ ],
5315
+ [
5316
+ "builds-code",
5317
+ /(?<![\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*\[/
5318
+ ],
5319
+ [
5320
+ // Spyware shapes: a screenshot, the clipboard, a key logger.
5321
+ "monitors",
5322
+ either(
5323
+ pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
5324
+ /\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
5325
+ )
5326
+ ],
5327
+ [
5328
+ "permissions",
5329
+ /\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
5330
+ ]
5331
+ ];
5332
+ NODE = [
5333
+ [
5334
+ "deletes",
5335
+ /\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
5336
+ ],
5337
+ [
5338
+ "writes-outside",
5339
+ new RegExp(
5340
+ 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}`
5341
+ )
5342
+ ],
5343
+ [
5344
+ "sends",
5345
+ /\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*\(/
5346
+ ],
5347
+ ["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
5348
+ [
5349
+ "runs-commands",
5350
+ /\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
5351
+ ],
5352
+ [
5353
+ "builds-code",
5354
+ /(?<![\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)]/
5355
+ ],
5356
+ [
5357
+ "permissions",
5358
+ /\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
5359
+ ]
5360
+ ];
5361
+ PERL = [
5362
+ ["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
5363
+ [
5364
+ "writes-outside",
5365
+ new RegExp(
5366
+ String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
5367
+ )
5368
+ ],
5369
+ [
5370
+ "sends",
5371
+ /\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
5372
+ ],
5373
+ [
5374
+ "runs-commands",
5375
+ /\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
5376
+ ],
5377
+ ["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
5378
+ ["permissions", /\b(?:chmod|chown)\b/]
5379
+ ];
5380
+ RUBY = [
5381
+ [
5382
+ "deletes",
5383
+ /\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
5384
+ ],
5385
+ [
5386
+ "writes-outside",
5387
+ new RegExp(
5388
+ 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}`
5389
+ )
5390
+ ],
5391
+ [
5392
+ "sends",
5393
+ /\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)['"]/
5394
+ ],
5395
+ ["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
5396
+ [
5397
+ "runs-commands",
5398
+ /(?<![\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/
5399
+ ],
5400
+ [
5401
+ "builds-code",
5402
+ /(?<![\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/
5403
+ ],
5404
+ ["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
5405
+ ];
5406
+ PHP = [
5407
+ ["deletes", /\b(?:unlink|rmdir)\s*\(/],
5408
+ [
5409
+ "writes-outside",
5410
+ new RegExp(
5411
+ String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
5412
+ )
5413
+ ],
5414
+ [
5415
+ "sends",
5416
+ /\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):/
5417
+ ],
5418
+ ["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
5419
+ [
5420
+ "runs-commands",
5421
+ /\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
5422
+ ],
5423
+ [
5424
+ "builds-code",
5425
+ /\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
5426
+ ],
5427
+ ["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
5428
+ ];
4518
5429
  ({ syntax } = mvdanSh);
4519
5430
  sharedParser = syntax.NewParser();
4520
5431
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -4552,6 +5463,10 @@ var init_dist = __esm({
4552
5463
  AST_CACHE_MAX = 5e3;
4553
5464
  astCache = /* @__PURE__ */ new Map();
4554
5465
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
5466
+ INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
5467
+ PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
5468
+ ACCESS_VIEW_CACHE_MAX = 1e3;
5469
+ accessViewCache = /* @__PURE__ */ new Map();
4555
5470
  MAX_READING_PAYLOADS = 8;
4556
5471
  defaultExpansionOps = null;
4557
5472
  VARIABLE_READERS = /* @__PURE__ */ new Set([
@@ -5285,9 +6200,6 @@ var init_dist = __esm({
5285
6200
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
5286
6201
  NONE = { kind: "none" };
5287
6202
  UNKNOWN = { kind: "unknown" };
5288
- MAX_REGEX_LENGTH = 256;
5289
- REGEX_CACHE_MAX = 500;
5290
- regexCache = /* @__PURE__ */ new Map();
5291
6203
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
5292
6204
  BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
5293
6205
  "bash",
@@ -5325,18 +6237,6 @@ var init_dist = __esm({
5325
6237
  (s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
5326
6238
  )
5327
6239
  );
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
6240
  BIP39_ENGLISH = `abandon
5341
6241
  ability
5342
6242
  able
@@ -8377,7 +9277,11 @@ zoo`.split("\n");
8377
9277
  "commands.disk-destroy",
8378
9278
  "commands.dangerous-word",
8379
9279
  "commands.node9-disarm",
8380
- "data.pipe-chain"
9280
+ "data.pipe-chain",
9281
+ // The general action checks (node9Firewall#390): smart rules the map governs.
9282
+ "actions.money",
9283
+ "actions.access",
9284
+ "actions.publish"
8381
9285
  ]);
8382
9286
  KNOB_GOVERNED = {
8383
9287
  "data.secrets": ["off", "block"],
@@ -9230,7 +10134,8 @@ var init_config_schema = __esm({
9230
10134
  }
9231
10135
  ),
9232
10136
  value: z.string().optional(),
9233
- flags: z.string().optional()
10137
+ flags: z.string().optional(),
10138
+ reading: z.enum(["access"]).optional()
9234
10139
  }).refine(
9235
10140
  (c) => {
9236
10141
  if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
@@ -9241,6 +10146,9 @@ var init_config_schema = __esm({
9241
10146
  SmartRuleSchema = z.object({
9242
10147
  name: z.string().optional(),
9243
10148
  tool: z.string().min(1, "Smart rule tool must not be empty"),
10149
+ // Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
10150
+ server: z.string().min(1).optional(),
10151
+ notServer: z.string().min(1).optional(),
9244
10152
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
9245
10153
  conditionMode: z.enum(["all", "any"]).optional(),
9246
10154
  verdict: z.enum(["allow", "review", "block"], {
@@ -9261,7 +10169,8 @@ var init_config_schema = __esm({
9261
10169
  ConfigFileSchema = z.object({
9262
10170
  version: z.string().optional(),
9263
10171
  settings: z.object({
9264
- mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
10172
+ // The protection level (levels.ts in the engine); `observe` is Audit's older name.
10173
+ mode: z.enum(["observe", "audit", "soft", "standard", "strict", "block-all"]).optional(),
9265
10174
  autoStartDaemon: z.boolean().optional(),
9266
10175
  // enableUndo / policy.snapshot: removed with the undo feature. The
9267
10176
  // schema strips undeclared keys silently (plain zod object — no
@@ -9391,6 +10300,82 @@ var init_config_schema = __esm({
9391
10300
  }
9392
10301
  });
9393
10302
 
10303
+ // src/shields/build.ts
10304
+ function escapeRegex(s) {
10305
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10306
+ }
10307
+ function slug(s) {
10308
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
10309
+ }
10310
+ function pathToRegexFragment(rawPath) {
10311
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
10312
+ const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
10313
+ if (segments2.length === 0) return "";
10314
+ return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
10315
+ }
10316
+ function pathRules(rawPath, verdict, reason) {
10317
+ const value = pathToRegexFragment(rawPath);
10318
+ if (!value) return [];
10319
+ const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
10320
+ const s = slug(rawPath);
10321
+ return [
10322
+ // `reading: 'access'`: a script's interpreter line and the program a
10323
+ // command runs are not reads of the jailed path (node9Firewall#385).
10324
+ {
10325
+ name: `${verdict}-path-${s}-bash`,
10326
+ tool: "bash",
10327
+ conditions: [{ field: "command", op: "matches", value, reading: "access" }],
10328
+ verdict,
10329
+ reason: why
10330
+ },
10331
+ // Keep the historical `-anytool` name for the file_path rule: the
10332
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
10333
+ {
10334
+ name: `${verdict}-path-${s}-anytool`,
10335
+ tool: "*",
10336
+ conditions: [{ field: "file_path", op: "matches", value }],
10337
+ verdict,
10338
+ reason: why
10339
+ },
10340
+ {
10341
+ name: `${verdict}-path-${s}-anytool-path`,
10342
+ tool: "*",
10343
+ conditions: [{ field: "path", op: "matches", value }],
10344
+ verdict,
10345
+ reason: why
10346
+ },
10347
+ {
10348
+ name: `${verdict}-path-${s}-anytool-pattern`,
10349
+ tool: "*",
10350
+ conditions: [{ field: "pattern", op: "matches", value }],
10351
+ verdict,
10352
+ reason: why
10353
+ }
10354
+ ];
10355
+ }
10356
+ function withAccessReading(def) {
10357
+ let changed = false;
10358
+ const smartRules = def.smartRules.map((r) => {
10359
+ const [c, ...rest] = r.conditions ?? [];
10360
+ 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) {
10361
+ return r;
10362
+ }
10363
+ changed = true;
10364
+ return { ...r, conditions: [{ ...c, reading: "access" }] };
10365
+ });
10366
+ return changed ? { ...def, smartRules } : def;
10367
+ }
10368
+ var B, SEP, LEGACY_BASH_PATH_RULE;
10369
+ var init_build = __esm({
10370
+ "src/shields/build.ts"() {
10371
+ "use strict";
10372
+ init_dist();
10373
+ B = "[\\s/\\\\]";
10374
+ SEP = "[/\\\\]";
10375
+ LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
10376
+ }
10377
+ });
10378
+
9394
10379
  // src/shields.ts
9395
10380
  import fs5 from "fs";
9396
10381
  import path6 from "path";
@@ -9419,7 +10404,7 @@ function loadUserShields() {
9419
10404
  `);
9420
10405
  continue;
9421
10406
  }
9422
- result[v.ok.name] = v.ok;
10407
+ result[v.ok.name] = withAccessReading(v.ok);
9423
10408
  } catch (err) {
9424
10409
  process.stderr.write(`[node9] Failed to load user shield ${file}: ${String(err)}
9425
10410
  `);
@@ -9475,6 +10460,7 @@ var init_shields = __esm({
9475
10460
  "src/shields.ts"() {
9476
10461
  "use strict";
9477
10462
  init_dist();
10463
+ init_build();
9478
10464
  init_dist();
9479
10465
  USER_SHIELDS_DIR = path6.join(os6.homedir(), ".node9", "shields");
9480
10466
  SHIELDS = buildSHIELDS();
@@ -9590,7 +10576,7 @@ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMA
9590
10576
  var init_managed = __esm({
9591
10577
  "src/config/managed.ts"() {
9592
10578
  "use strict";
9593
- MODE_ORDER = ["observe", "audit", "standard", "strict"];
10579
+ MODE_ORDER = ["observe", "audit", "soft", "standard", "strict", "block-all"];
9594
10580
  EGRESS_MODE_ORDER = ["off", "review", "block"];
9595
10581
  DLP_PII_ORDER = ["off", "block"];
9596
10582
  DLP_REVIEW_ACTION_ORDER = ["review", "block"];
@@ -9610,67 +10596,6 @@ var init_managed = __esm({
9610
10596
  }
9611
10597
  });
9612
10598
 
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
10599
  // src/auth/trusted-hosts.ts
9675
10600
  function normalizeHost2(raw) {
9676
10601
  return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
@@ -9741,7 +10666,7 @@ function v2ToLegacy(raw, options = {}) {
9741
10666
  continue;
9742
10667
  }
9743
10668
  if (id === "commands.unknown") {
9744
- warnings.push(`checks.${id}: set "mode": "strict" instead`);
10669
+ warnings.push(`checks.${id}: set "mode": "block-all" instead`);
9745
10670
  continue;
9746
10671
  }
9747
10672
  const allowed = configurableValues(id);
@@ -9974,6 +10899,11 @@ function logCacheReadIssue(cacheFile, kind) {
9974
10899
  } catch {
9975
10900
  }
9976
10901
  }
10902
+ function statedSignalFields(policy) {
10903
+ return Object.keys(tuningFromLegacyPolicy(policy)["loading.malicious-package"] ?? {}).filter(
10904
+ (k) => k === "newPackage" || k === "installScript"
10905
+ );
10906
+ }
9977
10907
  function catalogSettingsFrom(settings, policy) {
9978
10908
  return catalogSettingsFromConfig(settings, policy);
9979
10909
  }
@@ -10029,6 +10959,7 @@ function getConfig(cwd) {
10029
10959
  trustedHosts: [],
10030
10960
  trustedHostsManaged: false,
10031
10961
  appPermissions: {},
10962
+ appLinks: {},
10032
10963
  managedJailPaths: []
10033
10964
  };
10034
10965
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
@@ -10040,6 +10971,8 @@ function getConfig(cwd) {
10040
10971
  const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
10041
10972
  let ssrfStrictSource = "default";
10042
10973
  const workspaceChecks = {};
10974
+ const managedStatedChecks = /* @__PURE__ */ new Set();
10975
+ const managedStatedSignals = /* @__PURE__ */ new Set();
10043
10976
  const applyLayer = (source, isProject = false, isCloud = false) => {
10044
10977
  if (!source) return;
10045
10978
  const s = source.settings || {};
@@ -10247,9 +11180,11 @@ function getConfig(cwd) {
10247
11180
  if (raw.managedConfig && typeof raw.managedConfig === "object") {
10248
11181
  const mc = raw.managedConfig;
10249
11182
  const locked = Array.isArray(mc.locked) ? mc.locked.filter((f) => typeof f === "string") : [];
11183
+ for (const id of knobStatedChecks(mc)) managedStatedChecks.add(id);
11184
+ for (const k of statedSignalFields(mc)) managedStatedSignals.add(k);
10250
11185
  if (typeof mc.mode === "string") {
10251
11186
  if (keyed) {
10252
- if (["observe", "audit", "standard", "strict"].includes(mc.mode)) {
11187
+ if (isModeValue(mc.mode)) {
10253
11188
  mergedSettings.mode = mc.mode;
10254
11189
  }
10255
11190
  } else {
@@ -10426,6 +11361,13 @@ function getConfig(cwd) {
10426
11361
  mergedPolicy.trustedHostsManaged = true;
10427
11362
  mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
10428
11363
  }
11364
+ if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
11365
+ const links = {};
11366
+ for (const [key, slug2] of Object.entries(mc.appLinks)) {
11367
+ if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
11368
+ }
11369
+ mergedPolicy.appLinks = links;
11370
+ }
10429
11371
  if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
10430
11372
  const coerced = {};
10431
11373
  for (const [srv, tools] of Object.entries(mc.appPermissions)) {
@@ -10524,14 +11466,15 @@ function getConfig(cwd) {
10524
11466
  const envMode = process.env.NODE9_MODE;
10525
11467
  if (envMode && !modeCloudControlled && // PR-2: one listening point includes the env var — a keyed machine's
10526
11468
  // mode comes from the workspace (or the shipped default), never the shell.
10527
- !keyed && ["observe", "audit", "standard", "strict"].includes(envMode)) {
11469
+ !keyed && isModeValue(envMode)) {
10528
11470
  mergedSettings.mode = envMode;
10529
11471
  }
10530
- if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
11472
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && isRecordOnly(mergedSettings.mode)) {
10531
11473
  mergedSettings.mode = "standard";
10532
11474
  }
10533
- const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
10534
- if (modeCloudControlled && mergedSettings.mode === "strict") {
11475
+ const cloudStrictLevel = levelOf(mergedSettings.mode) === "strict" || levelOf(mergedSettings.mode) === "block-all";
11476
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && cloudStrictLevel;
11477
+ if (modeCloudControlled && cloudStrictLevel) {
10535
11478
  for (const name of Object.keys(mergedEnvironments)) {
10536
11479
  if (mergedEnvironments[name]?.requireApproval === false) {
10537
11480
  const cleaned = { ...mergedEnvironments[name] };
@@ -10549,6 +11492,35 @@ function getConfig(cwd) {
10549
11492
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
10550
11493
  mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
10551
11494
  const resolvedSsrfStrictSource = keyed && ssrfStrictSource === "local" ? "default" : ssrfStrictSource;
11495
+ const level = levelOf(mergedSettings.mode);
11496
+ const levelKnobChecks = Object.entries(levelChecks(level)).filter(([id]) => isKnobGoverned(id));
11497
+ const signals = levelPackageSignals(level);
11498
+ if (levelKnobChecks.length > 0 || signals) {
11499
+ const policyOf = (layer) => layer?.policy ?? {};
11500
+ const trusted = /* @__PURE__ */ new Set([...managedStatedChecks, ...Object.keys(workspaceChecks)]);
11501
+ const trustedSignals = new Set(managedStatedSignals);
11502
+ const project = /* @__PURE__ */ new Set();
11503
+ const projectSignals = /* @__PURE__ */ new Set();
11504
+ if (!keyed) {
11505
+ for (const id of knobStatedChecks(policyOf(globalConfig))) trusted.add(id);
11506
+ for (const k of statedSignalFields(policyOf(globalConfig))) trustedSignals.add(k);
11507
+ for (const id of knobStatedChecks(policyOf(projectConfig))) project.add(id);
11508
+ for (const k of statedSignalFields(policyOf(projectConfig))) projectSignals.add(k);
11509
+ }
11510
+ const before = resolveCheckMap(catalogSettingsFromConfig({}, mergedPolicy));
11511
+ for (const [id, v] of levelKnobChecks) {
11512
+ if (trusted.has(id)) continue;
11513
+ if (project.has(id) && CHECK_VERDICT_RANK[v] <= CHECK_VERDICT_RANK[before[id] ?? "off"])
11514
+ continue;
11515
+ checkToLegacyPolicy(mergedPolicy, id, v);
11516
+ }
11517
+ if (signals) {
11518
+ for (const key of ["newPackage", "installScript"]) {
11519
+ if (trustedSignals.has(key) || projectSignals.has(key)) continue;
11520
+ mergedPolicy.packageCheck[key] = signals[key];
11521
+ }
11522
+ }
11523
+ }
10552
11524
  const explicit = {};
10553
11525
  const checkSources = {};
10554
11526
  if (keyed) {
@@ -10726,6 +11698,7 @@ var init_config = __esm({
10726
11698
  init_build();
10727
11699
  init_trusted_hosts();
10728
11700
  init_dist();
11701
+ init_dist();
10729
11702
  init_v2();
10730
11703
  init_dist();
10731
11704
  init_dist();
@@ -10859,7 +11832,9 @@ var init_config = __esm({
10859
11832
  op: "matches",
10860
11833
  // Single spaces on purpose: a nested `\\s+` inside the alternation
10861
11834
  // 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",
11835
+ // `apps`: turning an app off, allowing one of its actions, or
11836
+ // linking a server to an app (the general checks leave it).
11837
+ 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
11838
  flags: "i"
10864
11839
  }
10865
11840
  ],
@@ -10945,7 +11920,12 @@ var init_config = __esm({
10945
11920
  verdict: "block",
10946
11921
  reason: "Piping remote script into a shell is a supply-chain attack vector",
10947
11922
  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
- }
11923
+ },
11924
+ // ── Actions in apps (node9Firewall#390) ───────────────────────────────
11925
+ // actions.money / actions.access / actions.publish: the action templates
11926
+ // on every MCP server no app shield claims, plus the shell publishing
11927
+ // rules. Review by default; the check value governs them.
11928
+ ...GENERAL_ACTION_RULES
10949
11929
  ],
10950
11930
  dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
10951
11931
  egress: {
@@ -10982,6 +11962,7 @@ var init_config = __esm({
10982
11962
  trustedHosts: [],
10983
11963
  trustedHostsManaged: false,
10984
11964
  appPermissions: {},
11965
+ appLinks: {},
10985
11966
  managedJailPaths: []
10986
11967
  },
10987
11968
  environments: {}
@@ -12021,6 +13002,7 @@ var init_provenance = __esm({
12021
13002
  var init_policy = __esm({
12022
13003
  "src/policy/index.ts"() {
12023
13004
  "use strict";
13005
+ init_dist();
12024
13006
  init_dlp();
12025
13007
  init_file_access();
12026
13008
  init_config();
@@ -13532,7 +14514,7 @@ function loadShieldStatus() {
13532
14514
  cfg.policySource === "workspace" ? cfg.policy.appliedShields ?? [] : readActiveShields()
13533
14515
  );
13534
14516
  const active = all.filter((n) => activeSet.has(n));
13535
- const inactive = all.filter((n) => !activeSet.has(n) && !ACTION_SHIELD_NAMES.has(n));
14517
+ const inactive = all.filter((n) => !activeSet.has(n) && !APP_SHIELD_NAMES.has(n));
13536
14518
  return { active, inactive };
13537
14519
  } catch {
13538
14520
  return { active: [], inactive: [] };