@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.
package/dist/index.mjs CHANGED
@@ -52,15 +52,101 @@ import picomatch from "picomatch";
52
52
  import safeRegex from "safe-regex2";
53
53
  import pm from "picomatch";
54
54
  import mvdanSh from "mvdan-sh";
55
- import safeRegex2 from "safe-regex2";
56
55
  import mvdanSh2 from "mvdan-sh";
57
- import safeRegex3 from "safe-regex2";
56
+ import safeRegex2 from "safe-regex2";
58
57
  import { createHash as createHash2 } from "crypto";
59
58
  import { createHash as createHash22 } from "crypto";
60
59
  import mvdanSh3 from "mvdan-sh";
61
60
  import { createHash as createHash3 } from "crypto";
62
61
  import mvdanSh4 from "mvdan-sh";
63
62
  import crypto from "crypto";
63
+ function validateRegex(pattern) {
64
+ if (!pattern) return "Pattern is required";
65
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
66
+ try {
67
+ new RegExp(pattern);
68
+ } catch (e) {
69
+ return `Invalid regex syntax: ${e.message}`;
70
+ }
71
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
72
+ if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
73
+ return null;
74
+ }
75
+ function getCompiledRegex(pattern, flags = "") {
76
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
77
+ const key = `${pattern}\0${flags}`;
78
+ if (regexCache.has(key)) {
79
+ const cached = regexCache.get(key);
80
+ regexCache.delete(key);
81
+ regexCache.set(key, cached);
82
+ return cached;
83
+ }
84
+ if (validateRegex(pattern) !== null) return null;
85
+ try {
86
+ const re = new RegExp(pattern, flags);
87
+ if (regexCache.size >= REGEX_CACHE_MAX) {
88
+ const oldest = regexCache.keys().next().value;
89
+ if (oldest) regexCache.delete(oldest);
90
+ }
91
+ regexCache.set(key, re);
92
+ return re;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ function isToolRule(rule) {
98
+ return rule.tool.startsWith("{,mcp__*__}");
99
+ }
100
+ function actionPart(ruleName) {
101
+ const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
102
+ return m ? m[1] : ruleName ?? "rule";
103
+ }
104
+ function ownReason(reason, owner) {
105
+ if (!reason) return reason;
106
+ return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
107
+ }
108
+ function buildAppShield(spec) {
109
+ const owner = spec.label;
110
+ const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
111
+ ...rule,
112
+ name: `shield:${spec.name}:${actionPart(rule.name)}`,
113
+ server: spec.server,
114
+ reason: ownReason(rule.reason, owner)
115
+ }));
116
+ const ownTools = (spec.mcpTools ?? []).map((rule) => ({
117
+ ...rule,
118
+ server: spec.server
119
+ }));
120
+ return {
121
+ name: spec.name,
122
+ description: spec.description,
123
+ aliases: [],
124
+ smartRules: [...toolRules, ...ownTools, ...spec.doors],
125
+ dangerousWords: []
126
+ };
127
+ }
128
+ function buildGeneralRules() {
129
+ const out = [];
130
+ for (const name of Object.keys(ACTION_TEMPLATES)) {
131
+ const { slug: slug2, label: label2 } = ACTION_CHECKS[name];
132
+ for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
133
+ const part = actionPart(rule.name).replace(/^review-/, "");
134
+ out.push({
135
+ ...rule,
136
+ name: `review-action-${slug2}-${part}`,
137
+ notServer: APP_SERVER_GLOB,
138
+ reason: ownReason(rule.reason, label2)
139
+ });
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+ function generalActionRuleNames(template) {
145
+ const { slug: slug2 } = ACTION_CHECKS[template];
146
+ return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug2}-`)).map(
147
+ (r) => r.name
148
+ );
149
+ }
64
150
  function isShieldVerdict(v) {
65
151
  return v === "allow" || v === "review" || v === "block";
66
152
  }
@@ -98,16 +184,25 @@ function validateOverrides(raw) {
98
184
  return { overrides: result, warnings };
99
185
  }
100
186
  function assertBuiltinShieldRegexesAreSafe() {
101
- for (const shield of Object.values(BUILTIN_SHIELDS)) {
187
+ const sets = [
188
+ ...Object.values(BUILTIN_SHIELDS).map((sh) => ({
189
+ name: sh.name,
190
+ smartRules: sh.smartRules
191
+ })),
192
+ ...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
193
+ { name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
194
+ ];
195
+ for (const shield of sets) {
102
196
  for (const rule of shield.smartRules) {
103
197
  const conditions = rule.conditions ?? [];
104
198
  for (const cond of conditions) {
105
199
  if (cond.op !== "matches" && cond.op !== "notMatches") continue;
106
200
  const pattern = cond.value;
107
201
  if (!pattern) continue;
108
- if (!safeRegex(pattern)) {
202
+ const problem = validateRegex(pattern);
203
+ if (problem) {
109
204
  throw new Error(
110
- `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
205
+ `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
111
206
  );
112
207
  }
113
208
  }
@@ -343,6 +438,32 @@ function ssrfFloorDecision(m, asWritten, opts) {
343
438
  }
344
439
  return "block";
345
440
  }
441
+ function isModeValue(v) {
442
+ return typeof v === "string" && MODE_VALUES.includes(v);
443
+ }
444
+ function levelOf(mode) {
445
+ if (mode === "observe") return "audit";
446
+ return LEVELS.includes(mode ?? "") ? mode : "standard";
447
+ }
448
+ function isRecordOnly(mode) {
449
+ return levelOf(mode) === "audit";
450
+ }
451
+ function reviewsEveryInline(mode) {
452
+ const level = levelOf(mode);
453
+ return level === "strict" || level === "block-all";
454
+ }
455
+ function isBlockAll(mode) {
456
+ return levelOf(mode) === "block-all";
457
+ }
458
+ function levelCheckValue(mode, id) {
459
+ return LEVEL_CHECKS[levelOf(mode)][id];
460
+ }
461
+ function levelChecks(mode) {
462
+ return LEVEL_CHECKS[levelOf(mode)];
463
+ }
464
+ function levelPackageSignals(mode) {
465
+ return levelOf(mode) === "soft" ? { newPackage: "off", installScript: "off" } : void 0;
466
+ }
346
467
  function isVerdict(v) {
347
468
  return v === "off" || v === "log" || v === "review" || v === "block";
348
469
  }
@@ -393,7 +514,7 @@ function isLockedCheck(def) {
393
514
  function ssrfCheckId(tier) {
394
515
  return isStrictGatedTier(tier) ? "network.internal-addresses" : "network.metadata";
395
516
  }
396
- function unwrapRuleLabel(s) {
517
+ function ruleNameInLabel(s) {
397
518
  let rest = s;
398
519
  const override = rest.lastIndexOf(OVERRIDE_PREFIX);
399
520
  if (override >= 0) rest = rest.slice(override + OVERRIDE_PREFIX.length).trimStart();
@@ -409,7 +530,7 @@ function checkIdForRule(nameOrLabel) {
409
530
  const s = nameOrLabel.trim();
410
531
  const exact = LEGACY_TO_ID.get(s);
411
532
  if (exact) return exact;
412
- const unwrapped = unwrapRuleLabel(s);
533
+ const unwrapped = ruleNameInLabel(s);
413
534
  if (unwrapped !== void 0) return checkIdForRule(unwrapped);
414
535
  if (s.startsWith("shield:project-jail:")) {
415
536
  return PROJECT_JAIL_BLOCK_READ.test(s) ? "data.credential-files" : "data.credential-files-other";
@@ -488,19 +609,21 @@ function resolveCheck(settings, id) {
488
609
  if (!def) return void 0;
489
610
  if (isLockedCheck(def)) return { id, value: "block", source: "locked" };
490
611
  const floor = def.floor ? CHECK_VERDICT_RANK[def.floor] : 0;
612
+ const levelDefault = levelCheckValue(settings.mode, id) ?? def.defaultValue;
613
+ const sourceOf = (value2) => value2 === levelDefault ? "default" : "configured";
491
614
  const explicit = settings.checks?.[id];
492
615
  if (isVerdict(explicit)) {
493
616
  const value2 = CHECK_VERDICT_RANK[explicit] < floor ? def.floor : explicit;
494
- return { id, value: value2, source: value2 === def.defaultValue ? "default" : "configured" };
617
+ return { id, value: value2, source: sourceOf(value2) };
495
618
  }
496
619
  if (def.pack) {
497
620
  const on = settings.appliedShields?.includes(def.pack) ?? false;
498
621
  return { id, value: on ? def.defaultValue : "off", source: "default" };
499
622
  }
500
- const read = LEGACY_READERS[id]?.(settings);
501
- if (read === void 0) return { id, value: def.defaultValue, source: "default" };
502
- const value = CHECK_VERDICT_RANK[read] < floor ? def.floor : read;
503
- return { id, value, source: value === def.defaultValue ? "default" : "configured" };
623
+ const read2 = LEGACY_READERS[id]?.(settings);
624
+ if (read2 === void 0) return { id, value: levelDefault, source: "default" };
625
+ const value = CHECK_VERDICT_RANK[read2] < floor ? def.floor : read2;
626
+ return { id, value, source: sourceOf(value) };
504
627
  }
505
628
  function checkValue(settings, id) {
506
629
  return resolveCheck(settings, id)?.value ?? "off";
@@ -567,6 +690,102 @@ function scanViews(text) {
567
690
  }
568
691
  return views;
569
692
  }
693
+ function isAgentInstructionPath(absPath, root) {
694
+ const r = root.replace(/\/$/, "");
695
+ if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
696
+ const rel = absPath.slice(r.length + 1);
697
+ if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
698
+ return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
699
+ }
700
+ function isInside(root, p) {
701
+ const r = root.replace(/\/$/, "") || "/";
702
+ return p === r || p.startsWith(r === "/" ? "/" : r + "/");
703
+ }
704
+ function isPasswordHashPath(p) {
705
+ return PASSWORD_HASH_FILES.includes(p);
706
+ }
707
+ function isLegacySystemPath(p) {
708
+ return LEGACY_SYSTEM_PATH_RE.test(p);
709
+ }
710
+ function normalizeFilePath(p, cwd = "", home = "~") {
711
+ if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
712
+ if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
713
+ const rooted = p.startsWith("/");
714
+ const parts = [];
715
+ for (const part of p.split("/")) {
716
+ if (!part || part === ".") continue;
717
+ if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
718
+ else if (part !== ".." || !rooted) parts.push(part);
719
+ }
720
+ return (rooted ? "/" : "") + parts.join("/");
721
+ }
722
+ function inlineLanguageOf(interp) {
723
+ const i = interp.toLowerCase();
724
+ if (/^python[\d.]*$/.test(i)) return "python";
725
+ if (/^(?:node|tsx|ts-node|deno|bun)$/.test(i)) return "node";
726
+ if (i === "perl") return "perl";
727
+ if (i === "ruby" || i === "irb") return "ruby";
728
+ if (i === "php") return "php";
729
+ return null;
730
+ }
731
+ function resolvePythonAliases(code) {
732
+ const aliases = [...code.matchAll(/\bimport\s+([\w.]+)\s+as\s+(\w+)/g)].slice(0, 32);
733
+ let out = code;
734
+ for (const [, mod, alias] of aliases) {
735
+ if (alias === mod) continue;
736
+ out = out.replace(new RegExp(String.raw`(?<![\w.])${alias}\b`, "g"), mod);
737
+ }
738
+ return out;
739
+ }
740
+ function protectedLiteral(code, isProtected) {
741
+ let n = 0;
742
+ for (const m of code.matchAll(STRING_LITERAL)) {
743
+ if (++n > 2048) break;
744
+ const value = m[2].trim();
745
+ if (value && isProtected(value)) return value;
746
+ }
747
+ return null;
748
+ }
749
+ function amplifies(language, code) {
750
+ const loop = language === "python" ? /\bwhile\s+(?:True|1)\s*:/ : language === "node" ? /\bwhile\s*\(\s*(?:true|1)\s*\)|\bfor\s*\(\s*;\s*;\s*\)|\bsetInterval\s*\(/ : /\bwhile\s*\(?\s*(?:1|true)\s*\)?|\bloop\s+do\b|\bfor\s*\(\s*;\s*;\s*\)/;
751
+ const m = loop.exec(code);
752
+ if (!m) return null;
753
+ const body = code.slice(m.index);
754
+ return /\.write\s*\(|\.(?:append|write)File(?:Sync)?\s*\(|\bopen\s*\(|\bprint\s*\(\s*[^)\n]*\bfile\s*=|\.send(?:all|to)?\s*\(|\bfwrite\s*\(|\bfile_put_contents\s*\(|\bsyswrite\b/.test(
755
+ body
756
+ ) ? m[0] : null;
757
+ }
758
+ function judgeInlineCode(language, code, isProtected) {
759
+ if (code.length > MAX_INLINE_CODE) {
760
+ return { outcome: "unreadable", what: "is too long for node9 to read" };
761
+ }
762
+ const text = language === "python" ? resolvePythonAliases(code) : code;
763
+ for (const [category, re] of TABLES[language]) {
764
+ const m = re.exec(text);
765
+ if (!m) continue;
766
+ const evidence = m[0].trim().replace(/\s+/g, " ").slice(0, 60);
767
+ return {
768
+ outcome: category === "builds-code" ? "unreadable" : "dangerous",
769
+ what: WHAT[category],
770
+ evidence,
771
+ category
772
+ };
773
+ }
774
+ const path28 = protectedLiteral(text, isProtected);
775
+ if (path28) {
776
+ return {
777
+ outcome: "dangerous",
778
+ what: WHAT["reads-protected"],
779
+ evidence: path28.slice(0, 60),
780
+ category: "reads-protected"
781
+ };
782
+ }
783
+ const loop = amplifies(language, text);
784
+ if (loop) {
785
+ return { outcome: "dangerous", what: WHAT.amplifies, evidence: loop, category: "amplifies" };
786
+ }
787
+ return null;
788
+ }
570
789
  function isCatHeredocOrLit(part) {
571
790
  if (!part) return false;
572
791
  const t = syntax.NodeType(part);
@@ -635,6 +854,55 @@ function cachedNormalize(command, compute) {
635
854
  function normalizeCommandForPolicy(command) {
636
855
  return commandReadingsImpl(command).posix;
637
856
  }
857
+ function fileAccessView(command) {
858
+ const hit = accessViewCache.get(command);
859
+ if (hit !== void 0) return hit;
860
+ const view = fileAccessViewImpl(command);
861
+ if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
862
+ const oldest = accessViewCache.keys().next().value;
863
+ if (oldest !== void 0) accessViewCache.delete(oldest);
864
+ }
865
+ accessViewCache.set(command, view);
866
+ return view;
867
+ }
868
+ function fileAccessViewImpl(command) {
869
+ const text = command.replace(INTERPRETER_LINE, "");
870
+ const f = parseShared(text);
871
+ if (f === PARSE_FAIL) return text;
872
+ const toCharIndex = byteOffsetToCharIndex(text);
873
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
874
+ const edits = [];
875
+ try {
876
+ syntax.Walk(f, (node) => {
877
+ if (!node) return false;
878
+ const n = node;
879
+ if (syntax.NodeType(n) !== "CallExpr") return true;
880
+ const args = n.Args || [];
881
+ const words = args.map((a) => resolveWordLiteral(a));
882
+ const head = unwrapCommandHead(words);
883
+ for (const i of /* @__PURE__ */ new Set([0, head])) {
884
+ const word = words[i];
885
+ if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
886
+ continue;
887
+ }
888
+ if (!PROGRAM_DIR.test(word)) continue;
889
+ const name = word.split(/[\\/]/).pop() ?? "";
890
+ if (!name) continue;
891
+ const s = at(args[i].Pos().Offset());
892
+ const e = at(args[i].End().Offset());
893
+ if (s < 0 || e < 0) continue;
894
+ edits.push([s, e, name]);
895
+ }
896
+ return true;
897
+ });
898
+ } catch {
899
+ return text;
900
+ }
901
+ edits.sort((a, b) => b[0] - a[0]);
902
+ let out = text;
903
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
904
+ return out;
905
+ }
638
906
  function commandReadings(command) {
639
907
  const r = commandReadingsImpl(command);
640
908
  const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
@@ -1090,53 +1358,119 @@ function unwrapCommandHead(words, stopAtFind = false, stopAtDynamic = false) {
1090
1358
  }
1091
1359
  return i;
1092
1360
  }
1093
- function inlineExecStmt(stmt, pipeFed) {
1361
+ function heredocText(word) {
1362
+ const parts = word?.Parts ?? [];
1363
+ let out = "";
1364
+ for (const p of parts) {
1365
+ if (syntax.NodeType(p) !== "Lit") return null;
1366
+ out += p.Value ?? "";
1367
+ }
1368
+ return out;
1369
+ }
1370
+ function stdinCodeOf(redirs) {
1371
+ const heredoc = /* @__PURE__ */ new Set([deriveRedirOp("cat <<X\nX"), deriveRedirOp("cat <<-X\nX")]);
1372
+ const herestring = deriveRedirOp("cat <<< x");
1373
+ for (const r of redirs) {
1374
+ if (!r || !redirStdinOps().has(r.Op)) continue;
1375
+ if (heredoc.has(r.Op)) {
1376
+ const text = heredocText(r.Hdoc);
1377
+ return text === null ? { code: null, source: "variable" } : { code: text };
1378
+ }
1379
+ if (r.Op === herestring) {
1380
+ const text = resolveWordLiteral(r.Word);
1381
+ return text === null ? { code: null, source: "variable" } : { code: text };
1382
+ }
1383
+ return { code: null, source: "file" };
1384
+ }
1385
+ return { code: null, source: "file" };
1386
+ }
1387
+ function attachedCode(interp, flag) {
1388
+ const eq = flag.indexOf("=");
1389
+ if (flag.startsWith("--")) return eq >= 0 ? flag.slice(eq + 1) : null;
1390
+ if (/^(pwsh|powershell)/.test(interp)) return null;
1391
+ const letters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1392
+ const body = flag.slice(1);
1393
+ const at = [...body].findIndex((ch) => letters.includes(ch.toLowerCase()));
1394
+ if (at < 0) return null;
1395
+ let rest = body.slice(at + 1);
1396
+ if (letters === "ep" && /^[ep]*$/i.test(rest)) rest = "";
1397
+ return rest === "" ? null : rest;
1398
+ }
1399
+ function inlineExecCall(stmt, pipeFed) {
1094
1400
  const cmd = stmt?.Cmd;
1095
- if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1401
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return null;
1096
1402
  const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1097
- if (words.length === 0) return false;
1403
+ if (words.length === 0) return null;
1098
1404
  const headIdx = unwrapCommandHead(words);
1099
1405
  const rawHead = words[headIdx];
1100
- if (rawHead == null) return false;
1406
+ if (rawHead == null) return null;
1101
1407
  const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1102
- if (!INLINE_INTERPRETER.test(interp)) return false;
1408
+ if (!INLINE_INTERPRETER.test(interp)) return null;
1103
1409
  let args = words.slice(headIdx + 1);
1104
- if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1410
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") {
1411
+ const code = args[1] ?? null;
1412
+ return { interp, code: [code], source: code === null ? "variable" : void 0 };
1413
+ }
1105
1414
  if (INTERP_LEADING_TARGET.has(interp)) {
1106
1415
  const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1107
1416
  args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1108
1417
  }
1418
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1419
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1420
+ const fromStdin = () => {
1421
+ if (stdinFed) {
1422
+ const { code, source } = stdinCodeOf(redirs);
1423
+ return { interp, code: [code], source };
1424
+ }
1425
+ return { interp, code: [null], source: "pipe" };
1426
+ };
1109
1427
  let positionals2 = 0;
1110
1428
  let selectedProgram = false;
1429
+ const codes = [];
1111
1430
  for (let i = 0; i < args.length; i++) {
1112
1431
  const a = args[i];
1113
1432
  if (a == null) {
1433
+ if (codes.length > 0) break;
1114
1434
  positionals2++;
1115
1435
  selectedProgram = true;
1116
1436
  continue;
1117
1437
  }
1118
1438
  if (!selectedProgram && isInlineCodeFlag(interp, a)) {
1119
- if (!/^(bash|sh|zsh|dash|ksh)$/.test(interp)) return true;
1120
- const payload = args[i + 1];
1121
- if (payload == null) return true;
1122
- return shellPayloadNeedsReview(payload);
1439
+ if (/^(bash|sh|zsh|dash|ksh)$/.test(interp)) {
1440
+ const payload = args[i + 1];
1441
+ if (payload == null) return { interp, code: [null], source: "variable" };
1442
+ return shellPayloadNeedsReview(payload) ? { interp, code: [null], source: "payload" } : null;
1443
+ }
1444
+ const attached = attachedCode(interp, a);
1445
+ if (attached !== null) {
1446
+ codes.push(attached);
1447
+ } else {
1448
+ codes.push(args[i + 1] ?? null);
1449
+ i++;
1450
+ }
1451
+ continue;
1452
+ }
1453
+ if (codes.length > 0) {
1454
+ if (a.startsWith("-")) continue;
1455
+ break;
1123
1456
  }
1124
1457
  if (a === "-m") {
1125
1458
  selectedProgram = true;
1126
1459
  continue;
1127
1460
  }
1128
- if (a === "-" && !selectedProgram) return true;
1461
+ if (a === "-" && !selectedProgram) return fromStdin();
1129
1462
  if (!a.startsWith("-")) {
1130
1463
  positionals2++;
1131
1464
  selectedProgram = true;
1132
1465
  }
1133
1466
  }
1134
- const redirs = stmt.Redirs || cmd.Redirs || [];
1135
- const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1467
+ if (codes.length > 0) {
1468
+ return { interp, code: codes, source: codes.includes(null) ? "variable" : void 0 };
1469
+ }
1136
1470
  if (positionals2 === 0 && (stdinFed || pipeFed)) {
1137
- if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1471
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return fromStdin();
1138
1472
  }
1139
- return false;
1473
+ return null;
1140
1474
  }
1141
1475
  function shellPayloadNeedsReview(payload) {
1142
1476
  if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
@@ -1169,7 +1503,7 @@ function shellPayloadNeedsReview(payload) {
1169
1503
  payload
1170
1504
  ))
1171
1505
  review = true;
1172
- if (!review && detectInlineExec(payload)) review = true;
1506
+ if (!review && inlineExecReview(payload) !== null) review = true;
1173
1507
  } catch {
1174
1508
  review = true;
1175
1509
  } finally {
@@ -1177,35 +1511,77 @@ function shellPayloadNeedsReview(payload) {
1177
1511
  }
1178
1512
  return review;
1179
1513
  }
1180
- function detectInlineExec(command) {
1514
+ function inlineExecCalls(command) {
1181
1515
  const f = parseShared(command);
1182
- if (f === PARSE_FAIL) {
1183
- return /(^|[|;&]|&&)\s*(?:[\w./-]*\/)?(python[\d.]*|perl|ruby|node|php|lua|deno|bun|pwsh|osascript|rscript|bash|sh|zsh)\s+-{1,2}[a-z]*[ceEr]/i.test(
1184
- command
1185
- );
1186
- }
1187
- let found = false;
1516
+ if (f === PARSE_FAIL) return PARSE_FAIL;
1517
+ const calls = [];
1188
1518
  try {
1189
1519
  syntax.Walk(f, (node) => {
1190
- if (!node || found) return false;
1520
+ if (!node || calls.length >= MAX_INLINE_CALLS) return false;
1191
1521
  const n = node;
1192
1522
  const t = syntax.NodeType(n);
1193
1523
  if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1194
- if (inlineExecStmt(n.Y, true)) {
1195
- found = true;
1196
- return false;
1197
- }
1524
+ const call = inlineExecCall(n.Y, true);
1525
+ if (call) calls.push(call);
1198
1526
  }
1199
- if (t === "Stmt" && inlineExecStmt(n, false)) {
1200
- found = true;
1201
- return false;
1527
+ if (t === "Stmt") {
1528
+ const call = inlineExecCall(n, false);
1529
+ if (call) calls.push(call);
1202
1530
  }
1203
1531
  return true;
1204
1532
  });
1205
1533
  } catch {
1206
- return found;
1207
1534
  }
1208
- return found;
1535
+ return calls;
1536
+ }
1537
+ function detectInlineExec(command) {
1538
+ const calls = inlineExecCalls(command);
1539
+ if (calls === PARSE_FAIL) return INLINE_EXEC_FALLBACK.test(command);
1540
+ return calls.length > 0;
1541
+ }
1542
+ function isProtectedCodePath(p) {
1543
+ if (matchSensitivePath(p) !== null) return true;
1544
+ return SYSTEM_AUTH_FILES.includes(p) || SYSTEM_AUTH_DIRS.some((d) => p === d || p.startsWith(d + "/"));
1545
+ }
1546
+ function inlineExecDescription(r) {
1547
+ const lead = r ? `The AI is running code directly from the command line: ${inlineExecReason(r).replace(/^This/, "this")}.` : "The AI is running code directly from the command line.";
1548
+ return `${lead} Review the full script below before allowing it to execute.`;
1549
+ }
1550
+ function inlineExecReason(r) {
1551
+ const who = r.interp ? `This inline ${r.interp} code` : "This inline code";
1552
+ return r.evidence ? `${who} ${r.what} (${r.evidence})` : `${who} ${r.what}`;
1553
+ }
1554
+ function inlineExecReview(command) {
1555
+ const calls = inlineExecCalls(command);
1556
+ if (calls === PARSE_FAIL) {
1557
+ return INLINE_EXEC_FALLBACK.test(command) ? { outcome: "unreadable", interp: "", what: "could not be parsed" } : null;
1558
+ }
1559
+ let unreadable = calls.length >= MAX_INLINE_CALLS ? { outcome: "unreadable", interp: "", what: "runs more inline programs than node9 reads" } : null;
1560
+ for (const call of calls) {
1561
+ const language = inlineLanguageOf(call.interp);
1562
+ for (const code of call.code) {
1563
+ let finding = null;
1564
+ if (code === null) {
1565
+ finding = {
1566
+ outcome: "unreadable",
1567
+ interp: call.interp,
1568
+ what: UNREADABLE_SOURCE[call.source ?? "variable"]
1569
+ };
1570
+ } else if (language === null) {
1571
+ finding = {
1572
+ outcome: "unreadable",
1573
+ interp: call.interp,
1574
+ what: `is ${call.interp} code, which node9 does not read`
1575
+ };
1576
+ } else {
1577
+ const judged = judgeInlineCode(language, code, isProtectedCodePath);
1578
+ if (judged) finding = { ...judged, interp: call.interp };
1579
+ }
1580
+ if (finding?.outcome === "dangerous") return finding;
1581
+ if (finding && !unreadable) unreadable = finding;
1582
+ }
1583
+ }
1584
+ return unreadable;
1209
1585
  }
1210
1586
  function analyzeChmod777(command) {
1211
1587
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
@@ -2454,40 +2830,6 @@ function analyzeShellCommand(command) {
2454
2830
  }
2455
2831
  return { actions, paths, allTokens };
2456
2832
  }
2457
- function validateRegex(pattern) {
2458
- if (!pattern) return "Pattern is required";
2459
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
2460
- try {
2461
- new RegExp(pattern);
2462
- } catch (e) {
2463
- return `Invalid regex syntax: ${e.message}`;
2464
- }
2465
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
2466
- if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
2467
- return null;
2468
- }
2469
- function getCompiledRegex(pattern, flags = "") {
2470
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
2471
- const key = `${pattern}\0${flags}`;
2472
- if (regexCache.has(key)) {
2473
- const cached = regexCache.get(key);
2474
- regexCache.delete(key);
2475
- regexCache.set(key, cached);
2476
- return cached;
2477
- }
2478
- if (validateRegex(pattern) !== null) return null;
2479
- try {
2480
- const re = new RegExp(pattern, flags);
2481
- if (regexCache.size >= REGEX_CACHE_MAX) {
2482
- const oldest = regexCache.keys().next().value;
2483
- if (oldest) regexCache.delete(oldest);
2484
- }
2485
- regexCache.set(key, re);
2486
- return re;
2487
- } catch {
2488
- return null;
2489
- }
2490
- }
2491
2833
  function matchesPattern(text, patterns) {
2492
2834
  const p = Array.isArray(patterns) ? patterns : [patterns];
2493
2835
  if (p.length === 0) return false;
@@ -2498,9 +2840,21 @@ function matchesPattern(text, patterns) {
2498
2840
  const withoutDotSlash = text.replace(/^\.\//, "");
2499
2841
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2500
2842
  }
2501
- function getNestedValue(obj, path27) {
2843
+ function mcpServerFromToolName(toolName) {
2844
+ const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
2845
+ return match?.[1];
2846
+ }
2847
+ function ruleServerMatches(rule, serverLabel) {
2848
+ const label2 = typeof serverLabel === "string" ? serverLabel.trim().replace(/[\\/]/g, "-") || void 0 : void 0;
2849
+ if (rule.server) {
2850
+ if (!label2 || !matchesPattern(label2, rule.server)) return false;
2851
+ }
2852
+ if (rule.notServer && label2 && matchesPattern(label2, rule.notServer)) return false;
2853
+ return true;
2854
+ }
2855
+ function getNestedValue(obj, path28) {
2502
2856
  if (!obj || typeof obj !== "object") return null;
2503
- const segments2 = path27.split(".");
2857
+ const segments2 = path28.split(".");
2504
2858
  for (const seg of segments2) {
2505
2859
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2506
2860
  }
@@ -2520,23 +2874,26 @@ function evaluateSmartConditions(args, rule) {
2520
2874
  return val;
2521
2875
  };
2522
2876
  const readingsCache = /* @__PURE__ */ new Map();
2523
- const resolveFieldReadings = (field) => {
2524
- const cached = readingsCache.get(field);
2877
+ const resolveFieldReadings = (field, reading) => {
2878
+ const access = reading === "access" && field === "command";
2879
+ const key = access ? `${field}\0access` : field;
2880
+ const cached = readingsCache.get(key);
2525
2881
  if (cached) return cached;
2526
2882
  const primary = resolveField(field);
2527
2883
  if (primary === null) {
2528
- readingsCache.set(field, []);
2884
+ readingsCache.set(key, []);
2529
2885
  return [];
2530
2886
  }
2531
2887
  let out = [primary];
2532
2888
  if (field === "command") {
2533
2889
  const raw = getNestedValue(args, field);
2534
2890
  if (typeof raw === "string") {
2535
- const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
2536
- out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2891
+ const text = access ? fileAccessView(raw) : raw;
2892
+ const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
2893
+ out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2537
2894
  }
2538
2895
  }
2539
- readingsCache.set(field, out);
2896
+ readingsCache.set(key, out);
2540
2897
  return out;
2541
2898
  };
2542
2899
  const results = rule.conditions.map((cond) => {
@@ -2554,14 +2911,14 @@ function evaluateSmartConditions(args, rule) {
2554
2911
  if (val === null || !cond.value) return false;
2555
2912
  const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2556
2913
  if (!reM) return false;
2557
- return resolveFieldReadings(cond.field).some((v) => reM.test(v));
2914
+ return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
2558
2915
  }
2559
2916
  case "notMatches": {
2560
2917
  if (!cond.value) return false;
2561
2918
  if (val === null) return true;
2562
2919
  const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2563
2920
  if (!reN) return false;
2564
- return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
2921
+ return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
2565
2922
  }
2566
2923
  case "matchesGlob":
2567
2924
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
@@ -3403,35 +3760,6 @@ function collectShellFileAccesses(command, context = {}) {
3403
3760
  }
3404
3761
  return out;
3405
3762
  }
3406
- function isAgentInstructionPath(absPath, root) {
3407
- const r = root.replace(/\/$/, "");
3408
- if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
3409
- const rel = absPath.slice(r.length + 1);
3410
- if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
3411
- return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
3412
- }
3413
- function isInside(root, p) {
3414
- const r = root.replace(/\/$/, "") || "/";
3415
- return p === r || p.startsWith(r === "/" ? "/" : r + "/");
3416
- }
3417
- function isPasswordHashPath(p) {
3418
- return PASSWORD_HASH_FILES.includes(p);
3419
- }
3420
- function isLegacySystemPath(p) {
3421
- return LEGACY_SYSTEM_PATH_RE.test(p);
3422
- }
3423
- function normalizeFilePath(p, cwd = "", home = "~") {
3424
- if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
3425
- if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
3426
- const rooted = p.startsWith("/");
3427
- const parts = [];
3428
- for (const part of p.split("/")) {
3429
- if (!part || part === ".") continue;
3430
- if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
3431
- else if (part !== ".." || !rooted) parts.push(part);
3432
- }
3433
- return (rooted ? "/" : "") + parts.join("/");
3434
- }
3435
3763
  function incompleteFileVerdict(operation = "write") {
3436
3764
  return {
3437
3765
  decision: "review",
@@ -3483,8 +3811,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
3483
3811
  }
3484
3812
  return out;
3485
3813
  }
3486
- function within(path27, root) {
3487
- return path27 === root || path27.startsWith(root === "/" ? "/" : root + "/");
3814
+ function within(path28, root) {
3815
+ return path28 === root || path28.startsWith(root === "/" ? "/" : root + "/");
3488
3816
  }
3489
3817
  function hits(access, paths, directories, context) {
3490
3818
  const home = context.homeDirs?.[0] ?? "~";
@@ -4292,14 +4620,14 @@ function sensitivePathMatch(originalPath) {
4292
4620
  }
4293
4621
  function assertBuiltinPatternsAreSafe() {
4294
4622
  for (const p of DLP_PATTERNS) {
4295
- if (!safeRegex3(p.regex.source)) {
4623
+ if (!safeRegex2(p.regex.source)) {
4296
4624
  throw new Error(
4297
4625
  `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
4298
4626
  );
4299
4627
  }
4300
4628
  }
4301
4629
  for (const re of SENSITIVE_PATH_PATTERNS) {
4302
- if (!safeRegex3(re.source)) {
4630
+ if (!safeRegex2(re.source)) {
4303
4631
  throw new Error(
4304
4632
  `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
4305
4633
  );
@@ -5045,6 +5373,20 @@ function extractPackageInstalls(command) {
5045
5373
  return true;
5046
5374
  });
5047
5375
  }
5376
+ function tuningFromLegacyPolicy(policy) {
5377
+ const out = {};
5378
+ const p = policy;
5379
+ if (policy.packageCheck?.registrySignals === false) {
5380
+ out["loading.malicious-package"] = { newPackage: "off", installScript: "off" };
5381
+ }
5382
+ for (const f of TUNING_FIELDS) {
5383
+ const [block, key] = f.legacy;
5384
+ const value = p[block]?.[key];
5385
+ if (value === void 0) continue;
5386
+ (out[f.checkId] ??= {})[f.name] = value;
5387
+ }
5388
+ return out;
5389
+ }
5048
5390
  function tuningToLegacyPolicy(policy, tuning) {
5049
5391
  const unknown = [];
5050
5392
  for (const [id, fields] of Object.entries(tuning)) {
@@ -5159,6 +5501,25 @@ function checkToLegacyPolicy(policy, id, v) {
5159
5501
  return false;
5160
5502
  }
5161
5503
  }
5504
+ function knobStatedChecks(policy) {
5505
+ const out = /* @__PURE__ */ new Set();
5506
+ const has = (o, key) => !!o && typeof o === "object" && o[key] !== void 0;
5507
+ const { dlp, egress, loopDetection, injectionScan, skillPinning, packageCheck } = policy;
5508
+ const uc = policy.untrustedContent;
5509
+ if (has(dlp, "enabled")) out.add("data.secrets");
5510
+ if (has(dlp, "reviewAction") || dlp?.enabled === false) out.add("data.secrets-weak");
5511
+ if (has(dlp, "pii")) out.add("data.pii");
5512
+ if (has(egress, "enabled") || has(egress, "mode")) out.add("network.unknown-host");
5513
+ if (has(egress, "ssrfStrict")) out.add("network.internal-addresses");
5514
+ if (has(loopDetection, "enabled")) out.add("behavior.loops");
5515
+ if (has(injectionScan, "enabled")) out.add("behavior.prompt-injection");
5516
+ if (has(skillPinning, "enabled") || has(skillPinning, "mode")) out.add("loading.skill-tamper");
5517
+ if (has(packageCheck, "enabled") || has(packageCheck, "onMalicious"))
5518
+ out.add("loading.malicious-package");
5519
+ if (uc && typeof uc === "object" && (uc.enabled === false || has(uc, "afterExposure")))
5520
+ out.add("behavior.untrusted-content");
5521
+ return out;
5522
+ }
5162
5523
  function isKnobGoverned(id) {
5163
5524
  return id in KNOB_GOVERNED;
5164
5525
  }
@@ -5423,9 +5784,9 @@ function bareToolName(toolName) {
5423
5784
  const parts = toolName.split("__");
5424
5785
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
5425
5786
  }
5426
- function valuesAt(args, path27) {
5787
+ function valuesAt(args, path28) {
5427
5788
  let cursors = [args];
5428
- for (const rawSegment of path27.split(".")) {
5789
+ for (const rawSegment of path28.split(".")) {
5429
5790
  const isArray = rawSegment.endsWith("[]");
5430
5791
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
5431
5792
  const next = [];
@@ -5456,8 +5817,8 @@ function extractToolDestinations(toolName, args) {
5456
5817
  try {
5457
5818
  const out = [];
5458
5819
  const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
5459
- for (const path27 of paths) {
5460
- for (const value of valuesAt(args, path27)) {
5820
+ for (const path28 of paths) {
5821
+ for (const value of valuesAt(args, path28)) {
5461
5822
  const host2 = hostOf(value);
5462
5823
  if (host2) out.push({ host: host2, binary: toolName, raw: value });
5463
5824
  }
@@ -5483,8 +5844,8 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5483
5844
  try {
5484
5845
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
5485
5846
  if (!paths) return null;
5486
- for (const path27 of paths) {
5487
- for (const value of valuesAt(args, path27)) {
5847
+ for (const path28 of paths) {
5848
+ for (const value of valuesAt(args, path28)) {
5488
5849
  const host2 = hostOf(value);
5489
5850
  if (!host2) continue;
5490
5851
  const m = classifySsrf(host2);
@@ -5493,7 +5854,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5493
5854
  const decision = ssrfFloorDecision(m, written, opts);
5494
5855
  if (decision === null) continue;
5495
5856
  const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
5496
- return { ...m, argPath: path27, host: host2, reason, decision };
5857
+ return { ...m, argPath: path28, host: host2, reason, decision };
5497
5858
  }
5498
5859
  }
5499
5860
  return null;
@@ -5873,20 +6234,26 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5873
6234
  };
5874
6235
  const ruleCheck = (rule) => {
5875
6236
  if (rule.verdict === "allow") return void 0;
5876
- if (!rule.builtin || rule.pinned) return void 0;
6237
+ if (!rule.builtin) return void 0;
5877
6238
  const id = checkIdForRule(rule.name);
5878
6239
  if (!id) return void 0;
5879
6240
  const def = getCheck(id);
5880
6241
  if (!def) return void 0;
5881
- if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
6242
+ if (rule.pinned) {
6243
+ const explicit = config.policy.checks?.[id];
6244
+ return def.pack && config.policy.checkSources?.[id] === "workspace" && isVerdict(explicit) ? { id, value: explicit } : void 0;
6245
+ }
6246
+ if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
5882
6247
  if (def.pack) {
5883
6248
  const explicit = config.policy.checks?.[id];
5884
6249
  return isVerdict(explicit) ? { id, value: explicit } : void 0;
5885
6250
  }
5886
6251
  return { id, value: cv(id) };
5887
6252
  };
6253
+ const serverLabel = context.mcpServer ?? mcpServerFromToolName(toolName);
5888
6254
  const matches = config.policy.smartRules.filter((rule) => {
5889
6255
  if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
6256
+ if (!ruleServerMatches(rule, serverLabel)) return false;
5890
6257
  if (astSuppressed(rule)) return false;
5891
6258
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
5892
6259
  if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
@@ -5963,11 +6330,13 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5963
6330
  if (v?.logged) noteLogged(v);
5964
6331
  else if (v) candidates.push(v);
5965
6332
  };
5966
- if (detectInlineExec(shellCommand)) {
6333
+ const inline = reviewsEveryInline(config.settings.mode) ? detectInlineExec(shellCommand) ? void 0 : null : inlineExecReview(shellCommand);
6334
+ if (inline !== null) {
5967
6335
  consider("commands.inline-exec", {
5968
6336
  decision: "review",
5969
6337
  blockedByLabel: "Node9 Standard (Inline Execution)",
5970
- ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
6338
+ ...inline ? { reason: inlineExecReason(inline) } : {},
6339
+ ruleDescription: inlineExecDescription(inline),
5971
6340
  tier: 3,
5972
6341
  checkId: "commands.inline-exec"
5973
6342
  });
@@ -6021,21 +6390,17 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
6021
6390
  if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
6022
6391
  const prov = checkProvenance2(firstToken, cwd);
6023
6392
  if (prov.trustLevel === "suspect") {
6024
- const value = cv("commands.temp-binary");
6025
- const v = applyCheckValue(
6026
- value === "review" && config.settings.mode === "strict" ? "block" : value,
6027
- {
6028
- decision: "review",
6029
- blockedByLabel: "Node9: Suspect Binary",
6030
- reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
6031
- tier: 3,
6032
- checkId: "commands.temp-binary"
6033
- }
6034
- );
6393
+ const v = applyCheckValue(cv("commands.temp-binary"), {
6394
+ decision: "review",
6395
+ blockedByLabel: "Node9: Suspect Binary",
6396
+ reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
6397
+ tier: 3,
6398
+ checkId: "commands.temp-binary"
6399
+ });
6035
6400
  if (v?.logged) noteLogged(v);
6036
6401
  else if (v) return v;
6037
6402
  }
6038
- if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
6403
+ if (prov.trustLevel === "unknown" && isBlockAll(config.settings.mode)) {
6039
6404
  return {
6040
6405
  decision: "review",
6041
6406
  blockedByLabel: "Node9: Unknown Binary (strict mode)",
@@ -6126,7 +6491,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
6126
6491
  if (v?.logged) noteLogged(v);
6127
6492
  else if (v) return v;
6128
6493
  }
6129
- if (config.settings.mode === "strict") {
6494
+ if (isBlockAll(config.settings.mode)) {
6130
6495
  if (activeEnvironment?.requireApproval === false) return allowed();
6131
6496
  return {
6132
6497
  decision: "review",
@@ -7057,10 +7422,13 @@ function matchCanaryArgs(args, values) {
7057
7422
  return null;
7058
7423
  }
7059
7424
  }
7060
- 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, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, 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, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_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, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, MAX_PAYLOAD_NESTING, payloadNesting, SCRIPT_RUNNERS, SCRIPT_RUNNER_VALUE_FLAGS, MAX_SCRIPT_OPERANDS, NET_BINARIES, VALUE_FLAGS, 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, stripDotSlash, 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, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, SECRET_VIEW_LIMITS, 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, SPANISH_LETTERS, HETU_CHARS, MAX, UNTRUSTED_TOOLS, SIGNALS, MENTION_QUOTE_RE, MENTION_WORD_RE, 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, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, MCP_DESTINATION_KEYS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE, CARD_PATTERNS, PII_ROUTING_RE, ROUTING_LABEL_RE, ROUTING_LABEL_WINDOW, NATIONAL_ID_SHAPES, NATIONAL_ID_LABEL_RE, NATIONAL_ID_LABEL_WINDOW, NATIONAL_ID_GUARD_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, IBAN_DESTINATION_KEYS, WHOLE_IBAN_RE, JSON_ESCAPE_RE, JSON_ESCAPES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, 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;
7425
+ 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, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, 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, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, MAX_INLINE_CODE, WHAT, OUTSIDE, pyImport, either, PYTHON, NODE, PERL, RUBY, PHP, TABLES, STRING_LITERAL, 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, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, MAX_PAYLOAD_NESTING, payloadNesting, MAX_INLINE_CALLS, INLINE_EXEC_FALLBACK, UNREADABLE_SOURCE, SCRIPT_RUNNERS, SCRIPT_RUNNER_VALUE_FLAGS, MAX_SCRIPT_OPERANDS, NET_BINARIES, VALUE_FLAGS, 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, stripDotSlash, 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, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, SECRET_VIEW_LIMITS, 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, SPANISH_LETTERS, HETU_CHARS, MAX, UNTRUSTED_TOOLS, SIGNALS, MENTION_QUOTE_RE, MENTION_WORD_RE, 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, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, MCP_DESTINATION_KEYS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE, CARD_PATTERNS, PII_ROUTING_RE, ROUTING_LABEL_RE, ROUTING_LABEL_WINDOW, NATIONAL_ID_SHAPES, NATIONAL_ID_LABEL_RE, NATIONAL_ID_LABEL_WINDOW, NATIONAL_ID_GUARD_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, IBAN_DESTINATION_KEYS, WHOLE_IBAN_RE, JSON_ESCAPE_RE, JSON_ESCAPES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, 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;
7061
7426
  var init_dist = __esm({
7062
7427
  "packages/policy-engine/dist/index.mjs"() {
7063
7428
  "use strict";
7429
+ MAX_REGEX_LENGTH = 256;
7430
+ REGEX_CACHE_MAX = 500;
7431
+ regexCache = /* @__PURE__ */ new Map();
7064
7432
  aws_default = {
7065
7433
  name: "aws",
7066
7434
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -7362,77 +7730,6 @@ var init_dist = __esm({
7362
7730
  ],
7363
7731
  dangerousWords: ["wipefs"]
7364
7732
  };
7365
- finance_default = {
7366
- name: "finance",
7367
- 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.",
7368
- aliases: ["payments", "money"],
7369
- smartRules: [
7370
- {
7371
- name: "shield:finance:review-refund",
7372
- tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
7373
- conditions: [],
7374
- verdict: "review",
7375
- reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
7376
- },
7377
- {
7378
- name: "shield:finance:review-payout",
7379
- tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
7380
- conditions: [],
7381
- verdict: "review",
7382
- reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
7383
- },
7384
- {
7385
- name: "shield:finance:review-transfer",
7386
- 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}",
7387
- conditions: [],
7388
- verdict: "review",
7389
- reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
7390
- },
7391
- {
7392
- name: "shield:finance:review-charge",
7393
- 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}",
7394
- conditions: [],
7395
- verdict: "review",
7396
- reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
7397
- },
7398
- {
7399
- name: "shield:finance:review-payment-link",
7400
- tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
7401
- conditions: [],
7402
- verdict: "review",
7403
- reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
7404
- },
7405
- {
7406
- name: "shield:finance:review-subscription",
7407
- tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
7408
- conditions: [],
7409
- verdict: "review",
7410
- reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
7411
- },
7412
- {
7413
- name: "shield:finance:review-dispute",
7414
- tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
7415
- conditions: [],
7416
- verdict: "review",
7417
- reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
7418
- },
7419
- {
7420
- name: "shield:finance:review-card",
7421
- tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
7422
- conditions: [],
7423
- verdict: "review",
7424
- reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
7425
- },
7426
- {
7427
- name: "shield:finance:review-withdraw",
7428
- tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
7429
- conditions: [],
7430
- verdict: "review",
7431
- reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
7432
- }
7433
- ],
7434
- dangerousWords: []
7435
- };
7436
7733
  github_default = {
7437
7734
  name: "github",
7438
7735
  description: "Protects GitHub repositories from destructive AI operations",
@@ -7459,56 +7756,6 @@ var init_dist = __esm({
7459
7756
  ],
7460
7757
  dangerousWords: []
7461
7758
  };
7462
- identity_default = {
7463
- name: "identity",
7464
- 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.",
7465
- aliases: ["access", "iam"],
7466
- smartRules: [
7467
- {
7468
- name: "shield:identity:review-password",
7469
- tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
7470
- conditions: [],
7471
- verdict: "review",
7472
- reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
7473
- },
7474
- {
7475
- name: "shield:identity:review-key",
7476
- 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}",
7477
- conditions: [],
7478
- verdict: "review",
7479
- reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
7480
- },
7481
- {
7482
- name: "shield:identity:review-access",
7483
- 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}",
7484
- conditions: [],
7485
- verdict: "review",
7486
- reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
7487
- },
7488
- {
7489
- name: "shield:identity:review-membership",
7490
- 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}",
7491
- conditions: [],
7492
- verdict: "review",
7493
- reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
7494
- },
7495
- {
7496
- name: "shield:identity:review-user",
7497
- tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
7498
- conditions: [],
7499
- verdict: "review",
7500
- reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
7501
- },
7502
- {
7503
- name: "shield:identity:review-mfa",
7504
- tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
7505
- conditions: [],
7506
- verdict: "review",
7507
- reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
7508
- }
7509
- ],
7510
- dangerousWords: []
7511
- };
7512
7759
  k8s_default = {
7513
7760
  name: "k8s",
7514
7761
  description: "Protects Kubernetes clusters from destructive AI operations",
@@ -7761,7 +8008,7 @@ var init_dist = __esm({
7761
8008
  {
7762
8009
  field: "command",
7763
8010
  op: "matches",
7764
- 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|$|[;&|>)<])",
8011
+ 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;&|>)<]|$)",
7765
8012
  flags: "i"
7766
8013
  }
7767
8014
  ],
@@ -7775,7 +8022,7 @@ var init_dist = __esm({
7775
8022
  {
7776
8023
  field: "command",
7777
8024
  op: "matches",
7778
- 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)",
8025
+ 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)",
7779
8026
  flags: "i"
7780
8027
  }
7781
8028
  ],
@@ -7841,272 +8088,879 @@ var init_dist = __esm({
7841
8088
  ],
7842
8089
  dangerousWords: []
7843
8090
  };
7844
- publishing_default = {
7845
- name: "publishing",
7846
- 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.",
7847
- aliases: ["release", "deploy"],
8091
+ redis_default = {
8092
+ name: "redis",
8093
+ description: "Protects Redis instances from destructive AI operations",
8094
+ aliases: [],
8095
+ _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.",
7848
8096
  smartRules: [
7849
8097
  {
7850
- name: "shield:publishing:review-release",
7851
- tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
7852
- conditions: [],
7853
- verdict: "review",
7854
- reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
7855
- },
7856
- {
7857
- name: "shield:publishing:review-package",
7858
- tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
7859
- conditions: [],
7860
- verdict: "review",
7861
- reason: "This publishes a package. node9 asks before publishing (Publishing shield)."
7862
- },
7863
- {
7864
- name: "shield:publishing:review-deploy",
7865
- tool: "{,mcp__*__}{deploy,create_deployment,deploy_site,promote_deployment,trigger_deploy,deploy_to_production,rollout}",
7866
- conditions: [],
7867
- verdict: "review",
7868
- reason: "This deploys to an environment other people use. node9 asks before publishing (Publishing shield)."
7869
- },
7870
- {
7871
- name: "shield:publishing:review-post",
7872
- tool: "{,mcp__*__}{post_tweet,create_tweet,post_webpage,publish_post,create_post,publish_page,send_announcement,post_announcement,create_linkedin_post,schedule_post}",
7873
- conditions: [],
7874
- verdict: "review",
7875
- reason: "This posts outside the organisation. node9 asks before publishing (Publishing shield)."
7876
- },
7877
- {
7878
- name: "shield:publishing:review-visibility",
7879
- tool: "{,mcp__*__}{set_visibility,make_public,set_public,publish_repository,change_visibility,update_visibility}",
7880
- conditions: [],
7881
- verdict: "review",
7882
- reason: "This makes something public. node9 asks before publishing (Publishing shield)."
7883
- },
7884
- {
7885
- name: "shield:publishing:review-npm-publish",
7886
- tool: "bash",
8098
+ name: "shield:redis:block-flushall",
8099
+ tool: "*",
7887
8100
  conditions: [
7888
8101
  {
7889
8102
  field: "command",
7890
8103
  op: "matches",
7891
- value: "(?:^|[^\\w-])(?:npm|pnpm)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8104
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
7892
8105
  flags: "i"
7893
8106
  }
7894
8107
  ],
7895
- verdict: "review",
7896
- reason: "npm publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
8108
+ verdict: "block",
8109
+ reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
7897
8110
  },
7898
8111
  {
7899
- name: "shield:publishing:review-yarn-publish",
7900
- tool: "bash",
8112
+ name: "shield:redis:block-flushdb",
8113
+ tool: "*",
7901
8114
  conditions: [
7902
8115
  {
7903
8116
  field: "command",
7904
8117
  op: "matches",
7905
- value: "(?:^|[^\\w-])(?:yarn)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8118
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
7906
8119
  flags: "i"
7907
8120
  }
7908
8121
  ],
7909
- verdict: "review",
7910
- reason: "yarn publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
8122
+ verdict: "block",
8123
+ reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
7911
8124
  },
7912
8125
  {
7913
- name: "shield:publishing:review-twine-upload",
7914
- tool: "bash",
8126
+ name: "shield:redis:block-config-resetstat",
8127
+ tool: "*",
7915
8128
  conditions: [
7916
8129
  {
7917
8130
  field: "command",
7918
8131
  op: "matches",
7919
- value: "(?:^|[^\\w-])(?:twine)(?![\\w-])[^;&|]{0,200}?\\s(?:upload)(?![\\w-])",
8132
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
7920
8133
  flags: "i"
7921
8134
  }
7922
8135
  ],
7923
- verdict: "review",
7924
- reason: "twine upload publishes to PyPI. node9 asks before publishing (Publishing shield)."
8136
+ verdict: "block",
8137
+ reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
7925
8138
  },
7926
8139
  {
7927
- name: "shield:publishing:review-python-publish",
7928
- tool: "bash",
8140
+ name: "shield:redis:review-config-set",
8141
+ tool: "*",
7929
8142
  conditions: [
7930
8143
  {
7931
8144
  field: "command",
7932
8145
  op: "matches",
7933
- value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8146
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
7934
8147
  flags: "i"
7935
8148
  }
7936
8149
  ],
7937
8150
  verdict: "review",
7938
- reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
8151
+ reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
7939
8152
  },
7940
8153
  {
7941
- name: "shield:publishing:review-release-tool-publish",
7942
- tool: "bash",
8154
+ name: "shield:redis:review-del-wildcard",
8155
+ tool: "*",
7943
8156
  conditions: [
7944
8157
  {
7945
8158
  field: "command",
7946
8159
  op: "matches",
7947
- value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8160
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
7948
8161
  flags: "i"
7949
8162
  }
7950
8163
  ],
7951
8164
  verdict: "review",
7952
- reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
8165
+ reason: "Wildcard key deletion requires human approval (Redis shield)"
8166
+ }
8167
+ ],
8168
+ dangerousWords: ["FLUSHALL", "FLUSHDB"]
8169
+ };
8170
+ finance_default = {
8171
+ name: "finance",
8172
+ 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.",
8173
+ aliases: ["payments", "money"],
8174
+ smartRules: [
8175
+ {
8176
+ name: "shield:finance:review-refund",
8177
+ tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
8178
+ conditions: [],
8179
+ verdict: "review",
8180
+ reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
7953
8181
  },
7954
8182
  {
7955
- name: "shield:publishing:review-cargo-publish",
7956
- tool: "bash",
7957
- conditions: [
7958
- {
7959
- field: "command",
7960
- op: "matches",
7961
- value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7962
- flags: "i"
7963
- }
7964
- ],
8183
+ name: "shield:finance:review-payout",
8184
+ tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
8185
+ conditions: [],
7965
8186
  verdict: "review",
7966
- reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
8187
+ reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
7967
8188
  },
7968
8189
  {
7969
- name: "shield:publishing:review-gem-push",
7970
- tool: "bash",
7971
- conditions: [
7972
- {
7973
- field: "command",
7974
- op: "matches",
7975
- value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
8190
+ name: "shield:finance:review-transfer",
8191
+ tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
8192
+ conditions: [],
8193
+ verdict: "review",
8194
+ reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
8195
+ },
8196
+ {
8197
+ name: "shield:finance:review-charge",
8198
+ 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}",
8199
+ conditions: [],
8200
+ verdict: "review",
8201
+ reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
8202
+ },
8203
+ {
8204
+ name: "shield:finance:review-payment-link",
8205
+ tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
8206
+ conditions: [],
8207
+ verdict: "review",
8208
+ reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
8209
+ },
8210
+ {
8211
+ name: "shield:finance:review-subscription",
8212
+ tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
8213
+ conditions: [],
8214
+ verdict: "review",
8215
+ reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
8216
+ },
8217
+ {
8218
+ name: "shield:finance:review-dispute",
8219
+ tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
8220
+ conditions: [],
8221
+ verdict: "review",
8222
+ reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
8223
+ },
8224
+ {
8225
+ name: "shield:finance:review-card",
8226
+ tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
8227
+ conditions: [],
8228
+ verdict: "review",
8229
+ reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
8230
+ },
8231
+ {
8232
+ name: "shield:finance:review-withdraw",
8233
+ tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
8234
+ conditions: [],
8235
+ verdict: "review",
8236
+ reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
8237
+ }
8238
+ ],
8239
+ dangerousWords: []
8240
+ };
8241
+ identity_default = {
8242
+ name: "identity",
8243
+ 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.",
8244
+ aliases: ["access", "iam"],
8245
+ smartRules: [
8246
+ {
8247
+ name: "shield:identity:review-password",
8248
+ tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
8249
+ conditions: [],
8250
+ verdict: "review",
8251
+ reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
8252
+ },
8253
+ {
8254
+ name: "shield:identity:review-key",
8255
+ 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}",
8256
+ conditions: [],
8257
+ verdict: "review",
8258
+ reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
8259
+ },
8260
+ {
8261
+ name: "shield:identity:review-access",
8262
+ 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}",
8263
+ conditions: [],
8264
+ verdict: "review",
8265
+ reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
8266
+ },
8267
+ {
8268
+ name: "shield:identity:review-membership",
8269
+ 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}",
8270
+ conditions: [],
8271
+ verdict: "review",
8272
+ reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
8273
+ },
8274
+ {
8275
+ name: "shield:identity:review-user",
8276
+ 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}",
8277
+ conditions: [],
8278
+ verdict: "review",
8279
+ reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
8280
+ },
8281
+ {
8282
+ name: "shield:identity:review-mfa",
8283
+ tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
8284
+ conditions: [],
8285
+ verdict: "review",
8286
+ reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
8287
+ }
8288
+ ],
8289
+ dangerousWords: []
8290
+ };
8291
+ publishing_default = {
8292
+ name: "publishing",
8293
+ 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.",
8294
+ aliases: ["release", "deploy"],
8295
+ smartRules: [
8296
+ {
8297
+ name: "shield:publishing:review-release",
8298
+ tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
8299
+ conditions: [],
8300
+ verdict: "review",
8301
+ reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
8302
+ },
8303
+ {
8304
+ name: "shield:publishing:review-package",
8305
+ tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
8306
+ conditions: [],
8307
+ verdict: "review",
8308
+ reason: "This publishes a package. node9 asks before publishing (Publishing shield)."
8309
+ },
8310
+ {
8311
+ name: "shield:publishing:review-deploy",
8312
+ tool: "{,mcp__*__}{deploy,create_deployment,deploy_site,promote_deployment,trigger_deploy,deploy_to_production,rollout}",
8313
+ conditions: [],
8314
+ verdict: "review",
8315
+ reason: "This deploys to an environment other people use. node9 asks before publishing (Publishing shield)."
8316
+ },
8317
+ {
8318
+ name: "shield:publishing:review-post",
8319
+ tool: "{,mcp__*__}{post_tweet,create_tweet,post_webpage,publish_post,create_post,publish_page,send_announcement,post_announcement,create_linkedin_post,schedule_post}",
8320
+ conditions: [],
8321
+ verdict: "review",
8322
+ reason: "This posts outside the organisation. node9 asks before publishing (Publishing shield)."
8323
+ },
8324
+ {
8325
+ name: "shield:publishing:review-visibility",
8326
+ tool: "{,mcp__*__}{set_visibility,make_public,set_public,publish_repository,change_visibility,update_visibility}",
8327
+ conditions: [],
8328
+ verdict: "review",
8329
+ reason: "This makes something public. node9 asks before publishing (Publishing shield)."
8330
+ },
8331
+ {
8332
+ name: "shield:publishing:review-npm-publish",
8333
+ tool: "bash",
8334
+ conditions: [
8335
+ {
8336
+ field: "command",
8337
+ op: "matches",
8338
+ value: "(?:^|[^\\w-])(?:npm|pnpm)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7976
8339
  flags: "i"
7977
8340
  }
7978
8341
  ],
7979
8342
  verdict: "review",
7980
- reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
8343
+ reason: "npm publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
7981
8344
  },
7982
8345
  {
7983
- name: "shield:publishing:review-docker-push",
8346
+ name: "shield:publishing:review-yarn-publish",
7984
8347
  tool: "bash",
7985
8348
  conditions: [
7986
8349
  {
7987
8350
  field: "command",
7988
8351
  op: "matches",
7989
- value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
8352
+ value: "(?:^|[^\\w-])(?:yarn)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7990
8353
  flags: "i"
7991
8354
  }
7992
8355
  ],
7993
8356
  verdict: "review",
7994
- reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
8357
+ reason: "yarn publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
7995
8358
  },
7996
8359
  {
7997
- name: "shield:publishing:review-gh-release",
8360
+ name: "shield:publishing:review-twine-upload",
7998
8361
  tool: "bash",
7999
8362
  conditions: [
8000
8363
  {
8001
8364
  field: "command",
8002
8365
  op: "matches",
8003
- value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
8366
+ value: "(?:^|[^\\w-])(?:twine)(?![\\w-])[^;&|]{0,200}?\\s(?:upload)(?![\\w-])",
8004
8367
  flags: "i"
8005
8368
  }
8006
8369
  ],
8007
8370
  verdict: "review",
8008
- reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
8009
- }
8010
- ],
8011
- dangerousWords: []
8012
- };
8013
- redis_default = {
8014
- name: "redis",
8015
- description: "Protects Redis instances from destructive AI operations",
8016
- aliases: [],
8017
- _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.",
8018
- smartRules: [
8371
+ reason: "twine upload publishes to PyPI. node9 asks before publishing (Publishing shield)."
8372
+ },
8019
8373
  {
8020
- name: "shield:redis:block-flushall",
8021
- tool: "*",
8374
+ name: "shield:publishing:review-python-publish",
8375
+ tool: "bash",
8022
8376
  conditions: [
8023
8377
  {
8024
8378
  field: "command",
8025
8379
  op: "matches",
8026
- value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
8380
+ value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8027
8381
  flags: "i"
8028
8382
  }
8029
8383
  ],
8030
- verdict: "block",
8031
- reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
8384
+ verdict: "review",
8385
+ reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
8032
8386
  },
8033
8387
  {
8034
- name: "shield:redis:block-flushdb",
8035
- tool: "*",
8388
+ name: "shield:publishing:review-release-tool-publish",
8389
+ tool: "bash",
8036
8390
  conditions: [
8037
8391
  {
8038
8392
  field: "command",
8039
8393
  op: "matches",
8040
- value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
8394
+ value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8041
8395
  flags: "i"
8042
8396
  }
8043
8397
  ],
8044
- verdict: "block",
8045
- reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
8398
+ verdict: "review",
8399
+ reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
8046
8400
  },
8047
8401
  {
8048
- name: "shield:redis:block-config-resetstat",
8049
- tool: "*",
8402
+ name: "shield:publishing:review-cargo-publish",
8403
+ tool: "bash",
8050
8404
  conditions: [
8051
8405
  {
8052
8406
  field: "command",
8053
8407
  op: "matches",
8054
- value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
8408
+ value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8055
8409
  flags: "i"
8056
8410
  }
8057
8411
  ],
8058
- verdict: "block",
8059
- reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
8412
+ verdict: "review",
8413
+ reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
8060
8414
  },
8061
8415
  {
8062
- name: "shield:redis:review-config-set",
8063
- tool: "*",
8416
+ name: "shield:publishing:review-gem-push",
8417
+ tool: "bash",
8064
8418
  conditions: [
8065
8419
  {
8066
8420
  field: "command",
8067
8421
  op: "matches",
8068
- value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
8422
+ value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
8069
8423
  flags: "i"
8070
8424
  }
8071
8425
  ],
8072
8426
  verdict: "review",
8073
- reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
8427
+ reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
8074
8428
  },
8075
8429
  {
8076
- name: "shield:redis:review-del-wildcard",
8077
- tool: "*",
8430
+ name: "shield:publishing:review-docker-push",
8431
+ tool: "bash",
8078
8432
  conditions: [
8079
8433
  {
8080
8434
  field: "command",
8081
8435
  op: "matches",
8082
- value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
8436
+ value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
8083
8437
  flags: "i"
8084
8438
  }
8085
8439
  ],
8086
8440
  verdict: "review",
8087
- reason: "Wildcard key deletion requires human approval (Redis shield)"
8441
+ reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
8442
+ },
8443
+ {
8444
+ name: "shield:publishing:review-gh-release",
8445
+ tool: "bash",
8446
+ conditions: [
8447
+ {
8448
+ field: "command",
8449
+ op: "matches",
8450
+ value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
8451
+ flags: "i"
8452
+ }
8453
+ ],
8454
+ verdict: "review",
8455
+ reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
8088
8456
  }
8089
8457
  ],
8090
- dangerousWords: ["FLUSHALL", "FLUSHDB"]
8458
+ dangerousWords: []
8459
+ };
8460
+ ACTION_TEMPLATES = {
8461
+ finance: finance_default.smartRules,
8462
+ identity: identity_default.smartRules,
8463
+ publishing: publishing_default.smartRules
8464
+ };
8465
+ ACTION_CHECKS = {
8466
+ finance: { checkId: "actions.money", slug: "money", label: "Money check" },
8467
+ identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
8468
+ publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
8091
8469
  };
8470
+ GAP = "[^;&|]{0,200}?";
8471
+ prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
8472
+ httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
8473
+ httpWrite = (methods) => ({
8474
+ field: "command",
8475
+ op: "matches",
8476
+ 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)`
8477
+ });
8478
+ HTTP_WRITE = httpWrite("POST|DELETE");
8479
+ HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
8480
+ NOT_STRIPE_TEST = {
8481
+ field: "command",
8482
+ op: "notMatches",
8483
+ value: "\\b[sr]k_test_"
8484
+ };
8485
+ postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
8486
+ STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
8487
+ APP_SHIELD_SPECS = [
8488
+ {
8489
+ name: "stripe",
8490
+ label: "Stripe",
8491
+ 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.",
8492
+ template: "finance",
8493
+ server: "*stripe*",
8494
+ doors: [
8495
+ {
8496
+ name: "shield:stripe:review-cli",
8497
+ tool: "bash",
8498
+ conditions: [
8499
+ {
8500
+ field: "command",
8501
+ op: "matches",
8502
+ value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
8503
+ flags: "i"
8504
+ },
8505
+ {
8506
+ field: "command",
8507
+ op: "matches",
8508
+ // The stripe CLI works in test mode unless told otherwise.
8509
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8510
+ }
8511
+ ],
8512
+ conditionMode: "all",
8513
+ verdict: "review",
8514
+ reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
8515
+ },
8516
+ {
8517
+ name: "shield:stripe:review-cli-request",
8518
+ tool: "bash",
8519
+ conditions: [
8520
+ {
8521
+ field: "command",
8522
+ op: "matches",
8523
+ value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
8524
+ flags: "i"
8525
+ },
8526
+ {
8527
+ field: "command",
8528
+ op: "matches",
8529
+ // The stripe CLI works in test mode unless told otherwise.
8530
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8531
+ }
8532
+ ],
8533
+ conditionMode: "all",
8534
+ verdict: "review",
8535
+ reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
8536
+ },
8537
+ {
8538
+ name: "shield:stripe:review-api",
8539
+ tool: "bash",
8540
+ conditions: [
8541
+ {
8542
+ field: "command",
8543
+ op: "matches",
8544
+ value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
8545
+ flags: "i"
8546
+ },
8547
+ HTTP_WRITE,
8548
+ NOT_STRIPE_TEST
8549
+ ],
8550
+ conditionMode: "all",
8551
+ verdict: "review",
8552
+ reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
8553
+ },
8554
+ {
8555
+ name: "shield:stripe:review-code",
8556
+ tool: "bash",
8557
+ conditions: [
8558
+ {
8559
+ field: "command",
8560
+ op: "matches",
8561
+ // Python: stripe.Refund.create(...)
8562
+ value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
8563
+ },
8564
+ NOT_STRIPE_TEST
8565
+ ],
8566
+ conditionMode: "all",
8567
+ verdict: "review",
8568
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8569
+ },
8570
+ {
8571
+ name: "shield:stripe:review-code-node",
8572
+ tool: "bash",
8573
+ conditions: [
8574
+ {
8575
+ field: "command",
8576
+ op: "matches",
8577
+ // Node: stripe.refunds.create(...)
8578
+ value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
8579
+ },
8580
+ NOT_STRIPE_TEST
8581
+ ],
8582
+ conditionMode: "all",
8583
+ verdict: "review",
8584
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8585
+ },
8586
+ {
8587
+ name: "shield:stripe:review-code-http",
8588
+ tool: "bash",
8589
+ conditions: [
8590
+ {
8591
+ field: "command",
8592
+ op: "matches",
8593
+ // requests.post("https://api.stripe.com/v1/refunds", ...)
8594
+ value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
8595
+ },
8596
+ NOT_STRIPE_TEST
8597
+ ],
8598
+ conditionMode: "all",
8599
+ verdict: "review",
8600
+ reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
8601
+ }
8602
+ ]
8603
+ },
8604
+ {
8605
+ name: "paypal",
8606
+ label: "PayPal",
8607
+ 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.",
8608
+ template: "finance",
8609
+ server: "*paypal*",
8610
+ doors: [
8611
+ {
8612
+ name: "shield:paypal:review-api",
8613
+ tool: "bash",
8614
+ conditions: [
8615
+ {
8616
+ field: "command",
8617
+ op: "matches",
8618
+ // The live hosts only: api.paypal.com and api-m.paypal.com. The
8619
+ // sandbox (api-m.sandbox.paypal.com) moves no money.
8620
+ value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
8621
+ flags: "i"
8622
+ },
8623
+ {
8624
+ field: "command",
8625
+ op: "matches",
8626
+ // The endpoints that move money.
8627
+ value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
8628
+ flags: "i"
8629
+ },
8630
+ HTTP_WRITE
8631
+ ],
8632
+ conditionMode: "all",
8633
+ verdict: "review",
8634
+ reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
8635
+ },
8636
+ {
8637
+ name: "shield:paypal:review-code",
8638
+ tool: "bash",
8639
+ conditions: [
8640
+ {
8641
+ field: "command",
8642
+ op: "matches",
8643
+ // The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
8644
+ // Python paypalrestsdk.
8645
+ value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
8646
+ }
8647
+ ],
8648
+ verdict: "review",
8649
+ reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
8650
+ },
8651
+ {
8652
+ name: "shield:paypal:review-code-http",
8653
+ tool: "bash",
8654
+ conditions: [
8655
+ {
8656
+ field: "command",
8657
+ op: "matches",
8658
+ // The endpoints that move money; the token call every script
8659
+ // starts with (/v1/oauth2/token) is not one.
8660
+ value: postTo(
8661
+ "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)"
8662
+ )
8663
+ }
8664
+ ],
8665
+ verdict: "review",
8666
+ reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
8667
+ }
8668
+ ]
8669
+ },
8670
+ {
8671
+ name: "square",
8672
+ label: "Square",
8673
+ 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.",
8674
+ template: "finance",
8675
+ // Square's servers, never Squarespace (a website builder).
8676
+ server: "{*square,*square[-_]*,*squareup*}",
8677
+ mcpTools: [
8678
+ {
8679
+ // Square's official MCP server has one tool for every API call; the
8680
+ // service and method name what it does.
8681
+ name: "shield:square:review-mcp-request",
8682
+ tool: "{,mcp__*__}make_api_request",
8683
+ conditions: [
8684
+ {
8685
+ field: "service",
8686
+ op: "matches",
8687
+ value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
8688
+ flags: "i"
8689
+ },
8690
+ {
8691
+ field: "method",
8692
+ op: "notMatches",
8693
+ value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
8694
+ flags: "i"
8695
+ }
8696
+ ],
8697
+ conditionMode: "all",
8698
+ verdict: "review",
8699
+ reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
8700
+ }
8701
+ ],
8702
+ doors: [
8703
+ {
8704
+ name: "shield:square:review-api",
8705
+ tool: "bash",
8706
+ conditions: [
8707
+ {
8708
+ field: "command",
8709
+ op: "matches",
8710
+ // The live host only; the sandbox is connect.squareupsandbox.com.
8711
+ value: httpTo(
8712
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8713
+ ),
8714
+ flags: "i"
8715
+ },
8716
+ HTTP_WRITE_ANY
8717
+ ],
8718
+ conditionMode: "all",
8719
+ verdict: "review",
8720
+ reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
8721
+ },
8722
+ {
8723
+ name: "shield:square:review-code",
8724
+ tool: "bash",
8725
+ conditions: [
8726
+ {
8727
+ field: "command",
8728
+ op: "matches",
8729
+ // The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
8730
+ value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
8731
+ },
8732
+ {
8733
+ field: "command",
8734
+ op: "matches",
8735
+ 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*\\("
8736
+ }
8737
+ ],
8738
+ conditionMode: "all",
8739
+ verdict: "review",
8740
+ reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
8741
+ },
8742
+ {
8743
+ name: "shield:square:review-code-http",
8744
+ tool: "bash",
8745
+ conditions: [
8746
+ {
8747
+ field: "command",
8748
+ op: "matches",
8749
+ value: postTo(
8750
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8751
+ )
8752
+ }
8753
+ ],
8754
+ verdict: "review",
8755
+ reason: "This code posts to the Square API. node9 asks before money moves (Square)."
8756
+ }
8757
+ ]
8758
+ },
8759
+ {
8760
+ name: "plaid",
8761
+ label: "Plaid",
8762
+ 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.",
8763
+ template: "finance",
8764
+ server: "*plaid*",
8765
+ doors: [
8766
+ {
8767
+ name: "shield:plaid:review-api",
8768
+ tool: "bash",
8769
+ conditions: [
8770
+ {
8771
+ field: "command",
8772
+ op: "matches",
8773
+ // Every Plaid endpoint is a POST, so the path is what tells a
8774
+ // write; the live host only, never sandbox.plaid.com. A script
8775
+ // that posts to the URL meets this door too.
8776
+ value: httpTo(
8777
+ "production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
8778
+ ),
8779
+ flags: "i"
8780
+ }
8781
+ ],
8782
+ verdict: "review",
8783
+ reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
8784
+ },
8785
+ {
8786
+ name: "shield:plaid:review-code",
8787
+ tool: "bash",
8788
+ conditions: [
8789
+ { field: "command", op: "matches", value: "\\bplaid", flags: "i" },
8790
+ {
8791
+ field: "command",
8792
+ op: "matches",
8793
+ value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
8794
+ }
8795
+ ],
8796
+ conditionMode: "all",
8797
+ verdict: "review",
8798
+ reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
8799
+ }
8800
+ ]
8801
+ },
8802
+ {
8803
+ name: "okta",
8804
+ label: "Okta",
8805
+ 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.",
8806
+ template: "identity",
8807
+ server: "*okta*",
8808
+ doors: [
8809
+ {
8810
+ name: "shield:okta:review-api",
8811
+ tool: "bash",
8812
+ conditions: [
8813
+ {
8814
+ field: "command",
8815
+ op: "matches",
8816
+ // The management API only: sign-in (/api/v1/authn, /sessions,
8817
+ // /oauth2) is not under these paths.
8818
+ value: httpTo(
8819
+ "[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
8820
+ ),
8821
+ flags: "i"
8822
+ },
8823
+ HTTP_WRITE_ANY
8824
+ ],
8825
+ conditionMode: "all",
8826
+ verdict: "review",
8827
+ reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
8828
+ },
8829
+ {
8830
+ name: "shield:okta:review-code",
8831
+ tool: "bash",
8832
+ conditions: [
8833
+ {
8834
+ field: "command",
8835
+ op: "matches",
8836
+ // The Okta SDK in the script: a method named createUser alone is
8837
+ // in half the applications ever written.
8838
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8839
+ },
8840
+ {
8841
+ field: "command",
8842
+ op: "matches",
8843
+ value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
8844
+ }
8845
+ ],
8846
+ conditionMode: "all",
8847
+ verdict: "review",
8848
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8849
+ },
8850
+ {
8851
+ name: "shield:okta:review-code-py",
8852
+ tool: "bash",
8853
+ conditions: [
8854
+ {
8855
+ field: "command",
8856
+ op: "matches",
8857
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8858
+ },
8859
+ {
8860
+ field: "command",
8861
+ op: "matches",
8862
+ 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*\\("
8863
+ }
8864
+ ],
8865
+ conditionMode: "all",
8866
+ verdict: "review",
8867
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8868
+ }
8869
+ ]
8870
+ },
8871
+ {
8872
+ name: "auth0",
8873
+ label: "Auth0",
8874
+ 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.",
8875
+ template: "identity",
8876
+ server: "*auth0*",
8877
+ mcpTools: [
8878
+ {
8879
+ // Auth0's official MCP server: applications (their secrets and
8880
+ // callbacks), APIs and their scopes, and Actions (code that runs at
8881
+ // every sign-in).
8882
+ name: "shield:auth0:review-tenant-config",
8883
+ 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}",
8884
+ conditions: [],
8885
+ verdict: "review",
8886
+ reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
8887
+ }
8888
+ ],
8889
+ doors: [
8890
+ {
8891
+ name: "shield:auth0:review-cli",
8892
+ tool: "bash",
8893
+ conditions: [
8894
+ {
8895
+ field: "command",
8896
+ op: "matches",
8897
+ 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-])",
8898
+ flags: "i"
8899
+ }
8900
+ ],
8901
+ verdict: "review",
8902
+ reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
8903
+ },
8904
+ {
8905
+ name: "shield:auth0:review-api",
8906
+ tool: "bash",
8907
+ conditions: [
8908
+ {
8909
+ field: "command",
8910
+ op: "matches",
8911
+ // The Management API only; /oauth/token is signing in.
8912
+ value: httpTo(
8913
+ "\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
8914
+ ),
8915
+ flags: "i"
8916
+ },
8917
+ HTTP_WRITE_ANY
8918
+ ],
8919
+ conditionMode: "all",
8920
+ verdict: "review",
8921
+ reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
8922
+ },
8923
+ {
8924
+ name: "shield:auth0:review-code",
8925
+ tool: "bash",
8926
+ conditions: [
8927
+ {
8928
+ field: "command",
8929
+ op: "matches",
8930
+ // The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
8931
+ value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
8932
+ },
8933
+ {
8934
+ field: "command",
8935
+ op: "matches",
8936
+ 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*\\("
8937
+ }
8938
+ ],
8939
+ conditionMode: "all",
8940
+ verdict: "review",
8941
+ reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
8942
+ }
8943
+ ]
8944
+ }
8945
+ ];
8946
+ APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
8947
+ APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
8948
+ APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
8949
+ GENERAL_ACTION_RULES = buildGeneralRules();
8092
8950
  BUILTIN_SHIELDS = {
8093
8951
  [aws_default.name]: aws_default,
8094
8952
  [bash_safe_default.name]: bash_safe_default,
8095
8953
  [docker_default.name]: docker_default,
8096
8954
  [filesystem_default.name]: filesystem_default,
8097
- // Dangerous actions as category shields (node9Firewall#385): an MCP app's
8098
- // money, credential and publishing tools, asked about whatever the request
8099
- // said. One glob per rule covers the bare name the gateway sends and the
8100
- // mcp__<server>__ name the agent hook sends.
8101
- [finance_default.name]: finance_default,
8102
8955
  [github_default.name]: github_default,
8103
- [identity_default.name]: identity_default,
8104
8956
  [k8s_default.name]: k8s_default,
8105
8957
  [mongodb_default.name]: mongodb_default,
8106
8958
  [postgres_default.name]: postgres_default,
8107
8959
  [project_jail_default.name]: project_jail_default,
8108
- [publishing_default.name]: publishing_default,
8109
- [redis_default.name]: redis_default
8960
+ [redis_default.name]: redis_default,
8961
+ // App shields generated from the action templates (node9Firewall#390): one
8962
+ // per app, every door (MCP server, CLI, API, library), off by default.
8963
+ ...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
8110
8964
  };
8111
8965
  assertBuiltinShieldRegexesAreSafe();
8112
8966
  SSRF_MAX_HOST = 253;
@@ -8162,9 +9016,9 @@ var init_dist = __esm({
8162
9016
  },
8163
9017
  // ── Commands ──────────────────────────────────────────────────────────────
8164
9018
  "commands.inline-exec": {
8165
- plain: "The agent runs a small program written inside the command itself.",
9019
+ 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.",
8166
9020
  example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
8167
- 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."
9021
+ advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
8168
9022
  },
8169
9023
  "commands.eval-dynamic": {
8170
9024
  plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
@@ -8196,6 +9050,21 @@ var init_dist = __esm({
8196
9050
  example: "chmod 777 deploy.sh",
8197
9051
  advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
8198
9052
  },
9053
+ "actions.money": {
9054
+ 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.",
9055
+ example: "refund_order on a server called acme-billing",
9056
+ 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."
9057
+ },
9058
+ "actions.access": {
9059
+ plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
9060
+ example: "invite_user or grant_role on a company admin server",
9061
+ advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
9062
+ },
9063
+ "actions.publish": {
9064
+ 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.",
9065
+ example: "create_release or deploy_to_production on a company MCP server",
9066
+ advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
9067
+ },
8199
9068
  "commands.sudo": {
8200
9069
  plain: "The agent runs a command as the administrator (root).",
8201
9070
  example: "sudo apt-get install -y nginx",
@@ -8356,11 +9225,57 @@ var init_dist = __esm({
8356
9225
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
8357
9226
  },
8358
9227
  "loading.malicious-package": {
8359
- 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.",
9228
+ 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.",
8360
9229
  example: "npm install of a package flagged as malware",
8361
9230
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
8362
9231
  }
8363
9232
  };
9233
+ LEVELS = ["audit", "soft", "standard", "strict", "block-all"];
9234
+ MODE_VALUES = ["observe", ...LEVELS];
9235
+ SOFT = {
9236
+ "commands.inline-exec": "log",
9237
+ "commands.eval-dynamic": "log",
9238
+ "commands.rm": "log",
9239
+ "commands.chmod": "log",
9240
+ "commands.sudo": "log",
9241
+ "commands.git-destructive": "log",
9242
+ "commands.sql-ddl": "log",
9243
+ "commands.sql-no-where": "log",
9244
+ "commands.temp-binary": "log",
9245
+ "commands.disk-destroy": "log",
9246
+ "commands.dangerous-word": "log",
9247
+ "files.system-dirs": "log",
9248
+ "files.shell-startup": "log",
9249
+ "files.password-hashes": "log",
9250
+ "data.pipe-chain": "log",
9251
+ "behavior.untrusted-content": "log"
9252
+ };
9253
+ STRICT = {
9254
+ "commands.sql-ddl": "block",
9255
+ "commands.disk-destroy": "block",
9256
+ "commands.node9-disarm": "block",
9257
+ "files.system-dirs": "block",
9258
+ "files.shell-startup": "block",
9259
+ "files.password-hashes": "block",
9260
+ "data.secrets-weak": "block",
9261
+ "data.pipe-chain": "block",
9262
+ "network.unknown-host": "review",
9263
+ "network.internal-addresses": "block",
9264
+ "behavior.prompt-injection": "log",
9265
+ "loading.skill-tamper": "block"
9266
+ };
9267
+ BLOCK_ALL = {
9268
+ ...STRICT,
9269
+ "commands.unknown": "review",
9270
+ "commands.temp-binary": "block"
9271
+ };
9272
+ LEVEL_CHECKS = {
9273
+ audit: {},
9274
+ soft: SOFT,
9275
+ standard: {},
9276
+ strict: STRICT,
9277
+ "block-all": BLOCK_ALL
9278
+ };
8364
9279
  VERDICTS = ["off", "log", "review", "block"];
8365
9280
  CHECK_VERDICT_RANK = { off: 0, log: 1, review: 2, block: 3 };
8366
9281
  ALL = VERDICTS;
@@ -8408,7 +9323,7 @@ var init_dist = __esm({
8408
9323
  id: "commands.inline-exec",
8409
9324
  group: "commands",
8410
9325
  title: "Inline code execution",
8411
- catches: "node -e, python -c, bash -c, a heredoc into an interpreter",
9326
+ 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",
8412
9327
  values: ALL,
8413
9328
  defaultValue: "review",
8414
9329
  legacy: ["Node9 Standard (Inline Execution)"]
@@ -8765,6 +9680,38 @@ var init_dist = __esm({
8765
9680
  values: ALL,
8766
9681
  defaultValue: "block",
8767
9682
  legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
9683
+ },
9684
+ // ── Actions in apps (node9Firewall#390) ──────────────────────────────────
9685
+ // The safety net under the app shields: an action that moves money, changes
9686
+ // access or publishes, on an MCP server no app shield claims (an internal
9687
+ // billing server, a vendor with no shield, Zapier). Review by default; the
9688
+ // rules come from the action templates (shields/apps.ts).
9689
+ {
9690
+ id: "actions.money",
9691
+ group: "actions",
9692
+ title: "Money moves",
9693
+ catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
9694
+ values: ALL,
9695
+ defaultValue: "review",
9696
+ legacy: generalActionRuleNames("finance")
9697
+ },
9698
+ {
9699
+ id: "actions.access",
9700
+ group: "actions",
9701
+ title: "Accounts and access change",
9702
+ catches: "a password, key, role, permission or user changed through an app with no protection of its own",
9703
+ values: ALL,
9704
+ defaultValue: "review",
9705
+ legacy: generalActionRuleNames("identity")
9706
+ },
9707
+ {
9708
+ id: "actions.publish",
9709
+ group: "actions",
9710
+ title: "Something is published",
9711
+ catches: "a release, package, deploy or public post through an app with no protection of its own",
9712
+ values: ALL,
9713
+ defaultValue: "review",
9714
+ legacy: generalActionRuleNames("publishing")
8768
9715
  }
8769
9716
  ];
8770
9717
  SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
@@ -8785,7 +9732,7 @@ var init_dist = __esm({
8785
9732
  "commands.rm": (s) => knob(s.commandChecks?.rmAdvisory),
8786
9733
  "commands.chmod": (s) => knob(s.commandChecks?.chmod),
8787
9734
  "commands.sql-ddl": (s) => knob(s.commandChecks?.sqlDdl),
8788
- "commands.unknown": (s) => s.mode === void 0 ? void 0 : s.mode === "strict" ? "review" : "off",
9735
+ "commands.unknown": (s) => s.mode === void 0 ? void 0 : isBlockAll(s.mode) ? "review" : "off",
8789
9736
  "data.secrets": (s) => enabledOr(s.dlp?.enabled, "block"),
8790
9737
  "data.secrets-weak": (s) => {
8791
9738
  if (s.dlp?.enabled === false) return "off";
@@ -8893,6 +9840,217 @@ var init_dist = __esm({
8893
9840
  BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
8894
9841
  HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
8895
9842
  HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
9843
+ SYSTEM_AUTH_FILES = [
9844
+ "/etc/passwd",
9845
+ "/etc/shadow",
9846
+ "/etc/group",
9847
+ "/etc/gshadow",
9848
+ "/etc/sudoers"
9849
+ ];
9850
+ SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9851
+ PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9852
+ STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9853
+ SYSTEM_DIRS = [
9854
+ "/usr",
9855
+ "/etc",
9856
+ "/boot",
9857
+ "/bin",
9858
+ "/sbin",
9859
+ "/lib",
9860
+ "/lib64",
9861
+ "/var/log"
9862
+ ];
9863
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9864
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9865
+ AGENT_INSTRUCTION_FILES = [
9866
+ ".gitlab-ci.yml",
9867
+ "CLAUDE.md",
9868
+ "AGENTS.md",
9869
+ ".cursorrules",
9870
+ ".mcp.json",
9871
+ ".vscode/settings.json",
9872
+ "package.json"
9873
+ ];
9874
+ AGENT_INSTRUCTION_DIRS = [
9875
+ ".github/workflows",
9876
+ ".claude",
9877
+ ".cursor",
9878
+ ".git/hooks",
9879
+ ".husky"
9880
+ ];
9881
+ LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9882
+ MAX_INLINE_CODE = 64 * 1024;
9883
+ WHAT = {
9884
+ deletes: "deletes files",
9885
+ "writes-outside": "writes a file outside the project",
9886
+ sends: "sends data over the network",
9887
+ listens: "opens a listening socket or a shell",
9888
+ "runs-commands": "runs other commands or processes",
9889
+ "reads-protected": "reads a protected file",
9890
+ "builds-code": "runs code it builds at run time",
9891
+ permissions: "changes permissions or ownership",
9892
+ monitors: "captures the screen, the clipboard or the keyboard",
9893
+ amplifies: "writes or sends in an endless loop"
9894
+ };
9895
+ OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
9896
+ pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
9897
+ either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
9898
+ PYTHON = [
9899
+ [
9900
+ "deletes",
9901
+ /\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/
9902
+ ],
9903
+ [
9904
+ "writes-outside",
9905
+ new RegExp(
9906
+ 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}`
9907
+ )
9908
+ ],
9909
+ [
9910
+ "sends",
9911
+ either(
9912
+ pyImport(
9913
+ String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
9914
+ ),
9915
+ /\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
9916
+ )
9917
+ ],
9918
+ [
9919
+ "listens",
9920
+ either(
9921
+ pyImport(String.raw`pty|socketserver|http\.server`),
9922
+ /\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
9923
+ )
9924
+ ],
9925
+ [
9926
+ "runs-commands",
9927
+ either(
9928
+ pyImport("subprocess|pexpect|commands|psutil"),
9929
+ /\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+\*/
9930
+ )
9931
+ ],
9932
+ [
9933
+ "builds-code",
9934
+ /(?<![\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*\[/
9935
+ ],
9936
+ [
9937
+ // Spyware shapes: a screenshot, the clipboard, a key logger.
9938
+ "monitors",
9939
+ either(
9940
+ pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
9941
+ /\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
9942
+ )
9943
+ ],
9944
+ [
9945
+ "permissions",
9946
+ /\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
9947
+ ]
9948
+ ];
9949
+ NODE = [
9950
+ [
9951
+ "deletes",
9952
+ /\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
9953
+ ],
9954
+ [
9955
+ "writes-outside",
9956
+ new RegExp(
9957
+ 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}`
9958
+ )
9959
+ ],
9960
+ [
9961
+ "sends",
9962
+ /\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*\(/
9963
+ ],
9964
+ ["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
9965
+ [
9966
+ "runs-commands",
9967
+ /\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
9968
+ ],
9969
+ [
9970
+ "builds-code",
9971
+ /(?<![\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)]/
9972
+ ],
9973
+ [
9974
+ "permissions",
9975
+ /\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
9976
+ ]
9977
+ ];
9978
+ PERL = [
9979
+ ["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
9980
+ [
9981
+ "writes-outside",
9982
+ new RegExp(
9983
+ String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
9984
+ )
9985
+ ],
9986
+ [
9987
+ "sends",
9988
+ /\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
9989
+ ],
9990
+ [
9991
+ "runs-commands",
9992
+ /\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
9993
+ ],
9994
+ ["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
9995
+ ["permissions", /\b(?:chmod|chown)\b/]
9996
+ ];
9997
+ RUBY = [
9998
+ [
9999
+ "deletes",
10000
+ /\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
10001
+ ],
10002
+ [
10003
+ "writes-outside",
10004
+ new RegExp(
10005
+ 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}`
10006
+ )
10007
+ ],
10008
+ [
10009
+ "sends",
10010
+ /\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)['"]/
10011
+ ],
10012
+ ["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
10013
+ [
10014
+ "runs-commands",
10015
+ /(?<![\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/
10016
+ ],
10017
+ [
10018
+ "builds-code",
10019
+ /(?<![\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/
10020
+ ],
10021
+ ["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
10022
+ ];
10023
+ PHP = [
10024
+ ["deletes", /\b(?:unlink|rmdir)\s*\(/],
10025
+ [
10026
+ "writes-outside",
10027
+ new RegExp(
10028
+ String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
10029
+ )
10030
+ ],
10031
+ [
10032
+ "sends",
10033
+ /\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):/
10034
+ ],
10035
+ ["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
10036
+ [
10037
+ "runs-commands",
10038
+ /\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
10039
+ ],
10040
+ [
10041
+ "builds-code",
10042
+ /\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
10043
+ ],
10044
+ ["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
10045
+ ];
10046
+ TABLES = {
10047
+ python: PYTHON,
10048
+ node: NODE,
10049
+ perl: PERL,
10050
+ ruby: RUBY,
10051
+ php: PHP
10052
+ };
10053
+ STRING_LITERAL = /(['"`])((?:\\.|(?!\1)[^\\\n]){1,4096})\1/g;
8896
10054
  ({ syntax } = mvdanSh);
8897
10055
  sharedParser = syntax.NewParser();
8898
10056
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -8930,6 +10088,10 @@ var init_dist = __esm({
8930
10088
  AST_CACHE_MAX = 5e3;
8931
10089
  astCache = /* @__PURE__ */ new Map();
8932
10090
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
10091
+ INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
10092
+ PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
10093
+ ACCESS_VIEW_CACHE_MAX = 1e3;
10094
+ accessViewCache = /* @__PURE__ */ new Map();
8933
10095
  MAX_READING_PAYLOADS = 8;
8934
10096
  defaultExpansionOps = null;
8935
10097
  VARIABLE_READERS = /* @__PURE__ */ new Set([
@@ -9639,6 +10801,14 @@ var init_dist = __esm({
9639
10801
  INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
9640
10802
  MAX_PAYLOAD_NESTING = 8;
9641
10803
  payloadNesting = 0;
10804
+ MAX_INLINE_CALLS = 16;
10805
+ INLINE_EXEC_FALLBACK = /(^|[|;&]|&&)\s*(?:[\w./-]*\/)?(python[\d.]*|perl|ruby|node|php|lua|deno|bun|pwsh|osascript|rscript|bash|sh|zsh)\s+-{1,2}[a-z]*[ceEr]/i;
10806
+ UNREADABLE_SOURCE = {
10807
+ pipe: "reads its code from another program through a pipe",
10808
+ file: "reads its code from a file on stdin",
10809
+ variable: "runs code the shell fills in at run time",
10810
+ payload: "runs a shell payload that evaluates, sources or runs further code"
10811
+ };
9642
10812
  SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
9643
10813
  SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
9644
10814
  MAX_SCRIPT_OPERANDS = 8;
@@ -9838,9 +11008,6 @@ var init_dist = __esm({
9838
11008
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
9839
11009
  NONE = { kind: "none" };
9840
11010
  UNKNOWN = { kind: "unknown" };
9841
- MAX_REGEX_LENGTH = 256;
9842
- REGEX_CACHE_MAX = 500;
9843
- regexCache = /* @__PURE__ */ new Map();
9844
11011
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
9845
11012
  BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
9846
11013
  "bash",
@@ -9955,45 +11122,6 @@ var init_dist = __esm({
9955
11122
  (s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
9956
11123
  )
9957
11124
  );
9958
- SYSTEM_AUTH_FILES = [
9959
- "/etc/passwd",
9960
- "/etc/shadow",
9961
- "/etc/group",
9962
- "/etc/gshadow",
9963
- "/etc/sudoers"
9964
- ];
9965
- SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9966
- PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9967
- STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9968
- SYSTEM_DIRS = [
9969
- "/usr",
9970
- "/etc",
9971
- "/boot",
9972
- "/bin",
9973
- "/sbin",
9974
- "/lib",
9975
- "/lib64",
9976
- "/var/log"
9977
- ];
9978
- SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9979
- UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9980
- AGENT_INSTRUCTION_FILES = [
9981
- ".gitlab-ci.yml",
9982
- "CLAUDE.md",
9983
- "AGENTS.md",
9984
- ".cursorrules",
9985
- ".mcp.json",
9986
- ".vscode/settings.json",
9987
- "package.json"
9988
- ];
9989
- AGENT_INSTRUCTION_DIRS = [
9990
- ".github/workflows",
9991
- ".claude",
9992
- ".cursor",
9993
- ".git/hooks",
9994
- ".husky"
9995
- ];
9996
- LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9997
11125
  exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
9998
11126
  RANK = { allow: 0, review: 1, block: 2 };
9999
11127
  BIP39_ENGLISH = `abandon
@@ -13486,7 +14614,11 @@ zoo`.split("\n");
13486
14614
  "commands.disk-destroy",
13487
14615
  "commands.dangerous-word",
13488
14616
  "commands.node9-disarm",
13489
- "data.pipe-chain"
14617
+ "data.pipe-chain",
14618
+ // The general action checks (node9Firewall#390): smart rules the map governs.
14619
+ "actions.money",
14620
+ "actions.access",
14621
+ "actions.publish"
13490
14622
  ]);
13491
14623
  KNOB_GOVERNED = {
13492
14624
  "data.secrets": ["off", "block"],
@@ -14194,7 +15326,8 @@ var SmartConditionSchema = z.object({
14194
15326
  }
14195
15327
  ),
14196
15328
  value: z.string().optional(),
14197
- flags: z.string().optional()
15329
+ flags: z.string().optional(),
15330
+ reading: z.enum(["access"]).optional()
14198
15331
  }).refine(
14199
15332
  (c) => {
14200
15333
  if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
@@ -14205,6 +15338,9 @@ var SmartConditionSchema = z.object({
14205
15338
  var SmartRuleSchema = z.object({
14206
15339
  name: z.string().optional(),
14207
15340
  tool: z.string().min(1, "Smart rule tool must not be empty"),
15341
+ // Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
15342
+ server: z.string().min(1).optional(),
15343
+ notServer: z.string().min(1).optional(),
14208
15344
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
14209
15345
  conditionMode: z.enum(["all", "any"]).optional(),
14210
15346
  verdict: z.enum(["allow", "review", "block"], {
@@ -14225,7 +15361,8 @@ var SmartRuleSchema = z.object({
14225
15361
  var ConfigFileSchema = z.object({
14226
15362
  version: z.string().optional(),
14227
15363
  settings: z.object({
14228
- mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
15364
+ // The protection level (levels.ts in the engine); `observe` is Audit's older name.
15365
+ mode: z.enum(["observe", "audit", "soft", "standard", "strict", "block-all"]).optional(),
14229
15366
  autoStartDaemon: z.boolean().optional(),
14230
15367
  // enableUndo / policy.snapshot: removed with the undo feature. The
14231
15368
  // schema strips undeclared keys silently (plain zod object — no
@@ -14354,8 +15491,8 @@ var ConfigFileSchema = z.object({
14354
15491
  }).strict();
14355
15492
  function formatIssues(issues) {
14356
15493
  const lines = issues.map((issue) => {
14357
- const path27 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
14358
- return ` \u2022 ${path27}: ${issue.message}`;
15494
+ const path28 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
15495
+ return ` \u2022 ${path28}: ${issue.message}`;
14359
15496
  });
14360
15497
  return `Invalid config:
14361
15498
  ${lines.join("\n")}`;
@@ -14368,9 +15505,9 @@ function prunePaths(root, paths) {
14368
15505
  const yi = y[y.length - 1];
14369
15506
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
14370
15507
  });
14371
- for (const path27 of ordered) {
15508
+ for (const path28 of ordered) {
14372
15509
  let cur = root;
14373
- for (const key of path27.slice(0, -1)) {
15510
+ for (const key of path28.slice(0, -1)) {
14374
15511
  if (cur === null || typeof cur !== "object") {
14375
15512
  cur = void 0;
14376
15513
  break;
@@ -14378,7 +15515,7 @@ function prunePaths(root, paths) {
14378
15515
  cur = cur[key];
14379
15516
  }
14380
15517
  if (cur === null || typeof cur !== "object") continue;
14381
- const last = path27[path27.length - 1];
15518
+ const last = path28[path28.length - 1];
14382
15519
  if (Array.isArray(cur)) {
14383
15520
  const i = Number(last);
14384
15521
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -14414,7 +15551,7 @@ function sanitizeConfig(raw) {
14414
15551
  return issue.keys.map((k) => [...at, k]);
14415
15552
  }
14416
15553
  return [at.slice(0, -level || void 0)];
14417
- }).filter((path27) => path27.length > 0);
15554
+ }).filter((path28) => path28.length > 0);
14418
15555
  if (paths.length === 0 || !prunePaths(working, paths)) break;
14419
15556
  }
14420
15557
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -14440,6 +15577,85 @@ import fs2 from "fs";
14440
15577
  import path2 from "path";
14441
15578
  import os2 from "os";
14442
15579
  init_dist();
15580
+
15581
+ // src/shields/build.ts
15582
+ init_dist();
15583
+ function escapeRegex(s) {
15584
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15585
+ }
15586
+ function slug(s) {
15587
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
15588
+ }
15589
+ var B = "[\\s/\\\\]";
15590
+ var SEP = "[/\\\\]";
15591
+ function pathToRegexFragment(rawPath) {
15592
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
15593
+ const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
15594
+ if (segments2.length === 0) return "";
15595
+ return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
15596
+ }
15597
+ function pathMatchesFragment(candidate, rawPath) {
15598
+ const value = pathToRegexFragment(rawPath);
15599
+ if (!value || !candidate) return false;
15600
+ const re = getCompiledRegex(value);
15601
+ if (!re) return false;
15602
+ return re.test(candidate);
15603
+ }
15604
+ function pathRules(rawPath, verdict, reason) {
15605
+ const value = pathToRegexFragment(rawPath);
15606
+ if (!value) return [];
15607
+ const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
15608
+ const s = slug(rawPath);
15609
+ return [
15610
+ // `reading: 'access'`: a script's interpreter line and the program a
15611
+ // command runs are not reads of the jailed path (node9Firewall#385).
15612
+ {
15613
+ name: `${verdict}-path-${s}-bash`,
15614
+ tool: "bash",
15615
+ conditions: [{ field: "command", op: "matches", value, reading: "access" }],
15616
+ verdict,
15617
+ reason: why
15618
+ },
15619
+ // Keep the historical `-anytool` name for the file_path rule: the
15620
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
15621
+ {
15622
+ name: `${verdict}-path-${s}-anytool`,
15623
+ tool: "*",
15624
+ conditions: [{ field: "file_path", op: "matches", value }],
15625
+ verdict,
15626
+ reason: why
15627
+ },
15628
+ {
15629
+ name: `${verdict}-path-${s}-anytool-path`,
15630
+ tool: "*",
15631
+ conditions: [{ field: "path", op: "matches", value }],
15632
+ verdict,
15633
+ reason: why
15634
+ },
15635
+ {
15636
+ name: `${verdict}-path-${s}-anytool-pattern`,
15637
+ tool: "*",
15638
+ conditions: [{ field: "pattern", op: "matches", value }],
15639
+ verdict,
15640
+ reason: why
15641
+ }
15642
+ ];
15643
+ }
15644
+ var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
15645
+ function withAccessReading(def) {
15646
+ let changed = false;
15647
+ const smartRules = def.smartRules.map((r) => {
15648
+ const [c, ...rest] = r.conditions ?? [];
15649
+ 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) {
15650
+ return r;
15651
+ }
15652
+ changed = true;
15653
+ return { ...r, conditions: [{ ...c, reading: "access" }] };
15654
+ });
15655
+ return changed ? { ...def, smartRules } : def;
15656
+ }
15657
+
15658
+ // src/shields.ts
14443
15659
  init_dist();
14444
15660
  var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
14445
15661
  function loadUserShields() {
@@ -14466,7 +15682,7 @@ function loadUserShields() {
14466
15682
  `);
14467
15683
  continue;
14468
15684
  }
14469
- result[v.ok.name] = v.ok;
15685
+ result[v.ok.name] = withAccessReading(v.ok);
14470
15686
  } catch (err) {
14471
15687
  process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
14472
15688
  `);
@@ -14521,7 +15737,7 @@ function readShieldOverrides() {
14521
15737
  }
14522
15738
 
14523
15739
  // src/config/managed.ts
14524
- var MODE_ORDER = ["observe", "audit", "standard", "strict"];
15740
+ var MODE_ORDER = ["observe", "audit", "soft", "standard", "strict", "block-all"];
14525
15741
  var EGRESS_MODE_ORDER = ["off", "review", "block"];
14526
15742
  function rankIn(order, value) {
14527
15743
  return value === void 0 ? -1 : order.indexOf(value);
@@ -14634,74 +15850,12 @@ function applyManagedCommandChecks(local, managed, locked) {
14634
15850
  }
14635
15851
  function applyManagedApprovers(local, managed) {
14636
15852
  return {
14637
- ...local,
14638
- native: typeof managed.native === "boolean" ? managed.native : local.native,
14639
- browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
14640
- cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
14641
- terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
14642
- };
14643
- }
14644
-
14645
- // src/shields/build.ts
14646
- init_dist();
14647
- function escapeRegex(s) {
14648
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14649
- }
14650
- function slug(s) {
14651
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
14652
- }
14653
- var B = "[\\s/\\\\]";
14654
- var SEP = "[/\\\\]";
14655
- function pathToRegexFragment(rawPath) {
14656
- const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
14657
- const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
14658
- if (segments2.length === 0) return "";
14659
- return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
14660
- }
14661
- function pathMatchesFragment(candidate, rawPath) {
14662
- const value = pathToRegexFragment(rawPath);
14663
- if (!value || !candidate) return false;
14664
- const re = getCompiledRegex(value);
14665
- if (!re) return false;
14666
- return re.test(candidate);
14667
- }
14668
- function pathRules(rawPath, verdict, reason) {
14669
- const value = pathToRegexFragment(rawPath);
14670
- if (!value) return [];
14671
- const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
14672
- const s = slug(rawPath);
14673
- return [
14674
- {
14675
- name: `${verdict}-path-${s}-bash`,
14676
- tool: "bash",
14677
- conditions: [{ field: "command", op: "matches", value }],
14678
- verdict,
14679
- reason: why
14680
- },
14681
- // Keep the historical `-anytool` name for the file_path rule: the
14682
- // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
14683
- {
14684
- name: `${verdict}-path-${s}-anytool`,
14685
- tool: "*",
14686
- conditions: [{ field: "file_path", op: "matches", value }],
14687
- verdict,
14688
- reason: why
14689
- },
14690
- {
14691
- name: `${verdict}-path-${s}-anytool-path`,
14692
- tool: "*",
14693
- conditions: [{ field: "path", op: "matches", value }],
14694
- verdict,
14695
- reason: why
14696
- },
14697
- {
14698
- name: `${verdict}-path-${s}-anytool-pattern`,
14699
- tool: "*",
14700
- conditions: [{ field: "pattern", op: "matches", value }],
14701
- verdict,
14702
- reason: why
14703
- }
14704
- ];
15853
+ ...local,
15854
+ native: typeof managed.native === "boolean" ? managed.native : local.native,
15855
+ browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
15856
+ cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
15857
+ terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
15858
+ };
14705
15859
  }
14706
15860
 
14707
15861
  // src/auth/trusted-hosts.ts
@@ -14762,6 +15916,7 @@ function isTrustedHost(host2) {
14762
15916
 
14763
15917
  // src/config/index.ts
14764
15918
  init_dist();
15919
+ init_dist();
14765
15920
 
14766
15921
  // src/config/v2.ts
14767
15922
  init_dist();
@@ -14845,7 +16000,7 @@ function v2ToLegacy(raw, options = {}) {
14845
16000
  continue;
14846
16001
  }
14847
16002
  if (id === "commands.unknown") {
14848
- warnings.push(`checks.${id}: set "mode": "strict" instead`);
16003
+ warnings.push(`checks.${id}: set "mode": "block-all" instead`);
14849
16004
  continue;
14850
16005
  }
14851
16006
  const allowed = configurableValues(id);
@@ -15074,7 +16229,9 @@ var DEFAULT_CONFIG = {
15074
16229
  op: "matches",
15075
16230
  // Single spaces on purpose: a nested `\\s+` inside the alternation
15076
16231
  // fails the engine's ReDoS check and the rule would be dropped.
15077
- value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|egress allow|approver set)\\b",
16232
+ // `apps`: turning an app off, allowing one of its actions, or
16233
+ // linking a server to an app (the general checks leave it).
16234
+ 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",
15078
16235
  flags: "i"
15079
16236
  }
15080
16237
  ],
@@ -15160,7 +16317,12 @@ var DEFAULT_CONFIG = {
15160
16317
  verdict: "block",
15161
16318
  reason: "Piping remote script into a shell is a supply-chain attack vector",
15162
16319
  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."
15163
- }
16320
+ },
16321
+ // ── Actions in apps (node9Firewall#390) ───────────────────────────────
16322
+ // actions.money / actions.access / actions.publish: the action templates
16323
+ // on every MCP server no app shield claims, plus the shell publishing
16324
+ // rules. Review by default; the check value governs them.
16325
+ ...GENERAL_ACTION_RULES
15164
16326
  ],
15165
16327
  dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
15166
16328
  egress: {
@@ -15197,6 +16359,7 @@ var DEFAULT_CONFIG = {
15197
16359
  trustedHosts: [],
15198
16360
  trustedHostsManaged: false,
15199
16361
  appPermissions: {},
16362
+ appLinks: {},
15200
16363
  managedJailPaths: []
15201
16364
  },
15202
16365
  environments: {}
@@ -15376,6 +16539,11 @@ var KNOB_TO_CHECK = {
15376
16539
  evalDynamic: "commands.eval-dynamic",
15377
16540
  pipeChainHigh: "data.pipe-chain"
15378
16541
  };
16542
+ function statedSignalFields(policy) {
16543
+ return Object.keys(tuningFromLegacyPolicy(policy)["loading.malicious-package"] ?? {}).filter(
16544
+ (k) => k === "newPackage" || k === "installScript"
16545
+ );
16546
+ }
15379
16547
  function catalogSettingsFrom(settings, policy) {
15380
16548
  return catalogSettingsFromConfig(settings, policy);
15381
16549
  }
@@ -15431,6 +16599,7 @@ function getConfig(cwd) {
15431
16599
  trustedHosts: [],
15432
16600
  trustedHostsManaged: false,
15433
16601
  appPermissions: {},
16602
+ appLinks: {},
15434
16603
  managedJailPaths: []
15435
16604
  };
15436
16605
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
@@ -15442,6 +16611,8 @@ function getConfig(cwd) {
15442
16611
  const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
15443
16612
  let ssrfStrictSource = "default";
15444
16613
  const workspaceChecks = {};
16614
+ const managedStatedChecks = /* @__PURE__ */ new Set();
16615
+ const managedStatedSignals = /* @__PURE__ */ new Set();
15445
16616
  const applyLayer = (source, isProject = false, isCloud = false) => {
15446
16617
  if (!source) return;
15447
16618
  const s = source.settings || {};
@@ -15649,9 +16820,11 @@ function getConfig(cwd) {
15649
16820
  if (raw.managedConfig && typeof raw.managedConfig === "object") {
15650
16821
  const mc = raw.managedConfig;
15651
16822
  const locked = Array.isArray(mc.locked) ? mc.locked.filter((f) => typeof f === "string") : [];
16823
+ for (const id of knobStatedChecks(mc)) managedStatedChecks.add(id);
16824
+ for (const k of statedSignalFields(mc)) managedStatedSignals.add(k);
15652
16825
  if (typeof mc.mode === "string") {
15653
16826
  if (keyed) {
15654
- if (["observe", "audit", "standard", "strict"].includes(mc.mode)) {
16827
+ if (isModeValue(mc.mode)) {
15655
16828
  mergedSettings.mode = mc.mode;
15656
16829
  }
15657
16830
  } else {
@@ -15815,19 +16988,26 @@ function getConfig(cwd) {
15815
16988
  }
15816
16989
  if (Array.isArray(mc.jailPaths)) {
15817
16990
  for (const jp of mc.jailPaths) {
15818
- const path27 = typeof jp?.path === "string" ? jp.path.trim() : "";
15819
- if (!path27) continue;
16991
+ const path28 = typeof jp?.path === "string" ? jp.path.trim() : "";
16992
+ if (!path28) continue;
15820
16993
  const verdict = jp?.verdict === "review" ? "review" : "block";
15821
- for (const r of pathRules(path27, verdict, "org-managed jail")) {
16994
+ for (const r of pathRules(path28, verdict, "org-managed jail")) {
15822
16995
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
15823
16996
  }
15824
- mergedPolicy.managedJailPaths.push({ path: path27, verdict });
16997
+ mergedPolicy.managedJailPaths.push({ path: path28, verdict });
15825
16998
  }
15826
16999
  }
15827
17000
  if (Array.isArray(mc.trustedHosts)) {
15828
17001
  mergedPolicy.trustedHostsManaged = true;
15829
17002
  mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
15830
17003
  }
17004
+ if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
17005
+ const links = {};
17006
+ for (const [key, slug2] of Object.entries(mc.appLinks)) {
17007
+ if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
17008
+ }
17009
+ mergedPolicy.appLinks = links;
17010
+ }
15831
17011
  if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
15832
17012
  const coerced = {};
15833
17013
  for (const [srv, tools] of Object.entries(mc.appPermissions)) {
@@ -15926,14 +17106,15 @@ function getConfig(cwd) {
15926
17106
  const envMode = process.env.NODE9_MODE;
15927
17107
  if (envMode && !modeCloudControlled && // PR-2: one listening point includes the env var — a keyed machine's
15928
17108
  // mode comes from the workspace (or the shipped default), never the shell.
15929
- !keyed && ["observe", "audit", "standard", "strict"].includes(envMode)) {
17109
+ !keyed && isModeValue(envMode)) {
15930
17110
  mergedSettings.mode = envMode;
15931
17111
  }
15932
- if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
17112
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && isRecordOnly(mergedSettings.mode)) {
15933
17113
  mergedSettings.mode = "standard";
15934
17114
  }
15935
- const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
15936
- if (modeCloudControlled && mergedSettings.mode === "strict") {
17115
+ const cloudStrictLevel = levelOf(mergedSettings.mode) === "strict" || levelOf(mergedSettings.mode) === "block-all";
17116
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && cloudStrictLevel;
17117
+ if (modeCloudControlled && cloudStrictLevel) {
15937
17118
  for (const name of Object.keys(mergedEnvironments)) {
15938
17119
  if (mergedEnvironments[name]?.requireApproval === false) {
15939
17120
  const cleaned = { ...mergedEnvironments[name] };
@@ -15951,6 +17132,35 @@ function getConfig(cwd) {
15951
17132
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
15952
17133
  mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
15953
17134
  const resolvedSsrfStrictSource = keyed && ssrfStrictSource === "local" ? "default" : ssrfStrictSource;
17135
+ const level = levelOf(mergedSettings.mode);
17136
+ const levelKnobChecks = Object.entries(levelChecks(level)).filter(([id]) => isKnobGoverned(id));
17137
+ const signals = levelPackageSignals(level);
17138
+ if (levelKnobChecks.length > 0 || signals) {
17139
+ const policyOf = (layer) => layer?.policy ?? {};
17140
+ const trusted = /* @__PURE__ */ new Set([...managedStatedChecks, ...Object.keys(workspaceChecks)]);
17141
+ const trustedSignals = new Set(managedStatedSignals);
17142
+ const project = /* @__PURE__ */ new Set();
17143
+ const projectSignals = /* @__PURE__ */ new Set();
17144
+ if (!keyed) {
17145
+ for (const id of knobStatedChecks(policyOf(globalConfig))) trusted.add(id);
17146
+ for (const k of statedSignalFields(policyOf(globalConfig))) trustedSignals.add(k);
17147
+ for (const id of knobStatedChecks(policyOf(projectConfig))) project.add(id);
17148
+ for (const k of statedSignalFields(policyOf(projectConfig))) projectSignals.add(k);
17149
+ }
17150
+ const before = resolveCheckMap(catalogSettingsFromConfig({}, mergedPolicy));
17151
+ for (const [id, v] of levelKnobChecks) {
17152
+ if (trusted.has(id)) continue;
17153
+ if (project.has(id) && CHECK_VERDICT_RANK[v] <= CHECK_VERDICT_RANK[before[id] ?? "off"])
17154
+ continue;
17155
+ checkToLegacyPolicy(mergedPolicy, id, v);
17156
+ }
17157
+ if (signals) {
17158
+ for (const key of ["newPackage", "installScript"]) {
17159
+ if (trustedSignals.has(key) || projectSignals.has(key)) continue;
17160
+ mergedPolicy.packageCheck[key] = signals[key];
17161
+ }
17162
+ }
17163
+ }
15954
17164
  const explicit = {};
15955
17165
  const checkSources = {};
15956
17166
  if (keyed) {
@@ -16122,6 +17332,9 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
16122
17332
  // src/utils/regex.ts
16123
17333
  init_dist();
16124
17334
 
17335
+ // src/policy/index.ts
17336
+ init_dist();
17337
+
16125
17338
  // src/dlp.ts
16126
17339
  init_dist();
16127
17340
  init_dist();
@@ -16373,7 +17586,8 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
16373
17586
  cwd: prepared.cwd,
16374
17587
  activeEnvironment,
16375
17588
  skipIgnoredFastPath: opts?.skipIgnoredFastPath,
16376
- argumentScan: opts?.argumentScan
17589
+ argumentScan: opts?.argumentScan,
17590
+ mcpServer: opts?.mcpServer
16377
17591
  },
16378
17592
  {
16379
17593
  checkProvenance,
@@ -17041,14 +18255,272 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
17041
18255
 
17042
18256
  // src/auth/orchestrator.ts
17043
18257
  import { randomUUID as randomUUID2 } from "crypto";
17044
- import os19 from "os";
18258
+
18259
+ // src/apps/state.ts
18260
+ import fs12 from "fs";
18261
+ import os10 from "os";
18262
+ import path12 from "path";
18263
+
18264
+ // src/apps/catalog.ts
18265
+ var APP_CATALOG = [
18266
+ {
18267
+ slug: "redis",
18268
+ label: "Redis",
18269
+ shield: "redis",
18270
+ servers: ["redis", "valkey"],
18271
+ commands: ["redis-cli", "valkey-cli"],
18272
+ packages: ["redis", "ioredis", "valkey"],
18273
+ envPrefixes: ["REDIS_"]
18274
+ },
18275
+ {
18276
+ slug: "postgres",
18277
+ label: "PostgreSQL",
18278
+ shield: "postgres",
18279
+ servers: ["postgres", "pgsql"],
18280
+ commands: ["psql", "pg_dump", "pg_restore", "pgcli"],
18281
+ packages: ["pg", "postgres", "psycopg", "psycopg2", "psycopg2-binary", "asyncpg"],
18282
+ envPrefixes: ["PGHOST", "PGDATABASE", "POSTGRES_"]
18283
+ },
18284
+ {
18285
+ slug: "mongodb",
18286
+ label: "MongoDB",
18287
+ shield: "mongodb",
18288
+ servers: ["mongo"],
18289
+ commands: ["mongosh", "mongo", "mongodump", "mongorestore"],
18290
+ packages: ["mongodb", "mongoose", "pymongo", "motor"],
18291
+ envPrefixes: ["MONGO"]
18292
+ },
18293
+ {
18294
+ slug: "github",
18295
+ label: "GitHub",
18296
+ shield: "github",
18297
+ servers: ["github"],
18298
+ commands: ["gh"],
18299
+ packages: ["@octokit/", "octokit", "pygithub"],
18300
+ envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"]
18301
+ },
18302
+ {
18303
+ slug: "aws",
18304
+ label: "AWS",
18305
+ shield: "aws",
18306
+ servers: ["aws"],
18307
+ commands: ["aws", "sam", "cdk"],
18308
+ packages: ["aws-sdk", "@aws-sdk/", "boto3", "aws-cdk-lib"],
18309
+ envPrefixes: ["AWS_ACCESS_KEY_ID", "AWS_PROFILE"]
18310
+ },
18311
+ {
18312
+ slug: "k8s",
18313
+ label: "Kubernetes",
18314
+ shield: "k8s",
18315
+ servers: ["kubernetes", "k8s", "kubectl"],
18316
+ commands: ["kubectl", "helm", "k9s"],
18317
+ packages: ["@kubernetes/client-node", "kubernetes"],
18318
+ envPrefixes: ["KUBECONFIG"]
18319
+ },
18320
+ {
18321
+ slug: "docker",
18322
+ label: "Docker",
18323
+ shield: "docker",
18324
+ servers: ["docker"],
18325
+ commands: ["docker", "docker-compose", "podman"],
18326
+ packages: ["dockerode", "docker"],
18327
+ envPrefixes: ["DOCKER_HOST"]
18328
+ },
18329
+ {
18330
+ slug: "stripe",
18331
+ label: "Stripe",
18332
+ shield: "stripe",
18333
+ servers: ["stripe"],
18334
+ commands: ["stripe"],
18335
+ packages: ["stripe", "@stripe/"],
18336
+ envPrefixes: ["STRIPE_"]
18337
+ },
18338
+ {
18339
+ slug: "paypal",
18340
+ label: "PayPal",
18341
+ shield: "paypal",
18342
+ servers: ["paypal"],
18343
+ commands: [],
18344
+ packages: ["@paypal/", "paypalrestsdk", "paypal-checkout-serversdk", "paypal-server-sdk"],
18345
+ envPrefixes: ["PAYPAL_"]
18346
+ },
18347
+ {
18348
+ slug: "square",
18349
+ label: "Square",
18350
+ shield: "square",
18351
+ servers: ["square"],
18352
+ // Squarespace is a website builder. SQUARE_ already misses SQUARESPACE_.
18353
+ notServers: ["squarespace"],
18354
+ commands: [],
18355
+ packages: ["square", "squareup"],
18356
+ envPrefixes: ["SQUARE_"]
18357
+ },
18358
+ {
18359
+ slug: "plaid",
18360
+ label: "Plaid",
18361
+ shield: "plaid",
18362
+ servers: ["plaid"],
18363
+ commands: [],
18364
+ packages: ["plaid", "plaid-python"],
18365
+ envPrefixes: ["PLAID_"]
18366
+ },
18367
+ {
18368
+ slug: "okta",
18369
+ label: "Okta",
18370
+ shield: "okta",
18371
+ servers: ["okta"],
18372
+ commands: ["okta"],
18373
+ packages: ["@okta/", "okta"],
18374
+ envPrefixes: ["OKTA_"]
18375
+ },
18376
+ {
18377
+ slug: "auth0",
18378
+ label: "Auth0",
18379
+ shield: "auth0",
18380
+ servers: ["auth0"],
18381
+ commands: ["auth0"],
18382
+ packages: ["auth0", "@auth0/"],
18383
+ envPrefixes: ["AUTH0_"]
18384
+ }
18385
+ ];
18386
+ var APP_BY_SLUG = new Map(
18387
+ APP_CATALOG.map((a) => [a.slug, a])
18388
+ );
18389
+
18390
+ // src/apps/state.ts
18391
+ init_audit();
18392
+
18393
+ // src/daemon/mcp-tools.ts
18394
+ import fs11 from "fs";
18395
+ import path11 from "path";
18396
+ import os9 from "os";
18397
+ function getMcpToolsFile() {
18398
+ return path11.join(os9.homedir(), ".node9", "mcp-tools.json");
18399
+ }
18400
+ function readMcpToolsConfig() {
18401
+ try {
18402
+ const file2 = getMcpToolsFile();
18403
+ if (!fs11.existsSync(file2)) return {};
18404
+ const raw = fs11.readFileSync(file2, "utf-8");
18405
+ return JSON.parse(raw);
18406
+ } catch {
18407
+ return {};
18408
+ }
18409
+ }
18410
+
18411
+ // src/apps/state.ts
18412
+ function appsFilePath(home = os10.homedir()) {
18413
+ return path12.join(home, ".node9", "apps.json");
18414
+ }
18415
+ var SIGNAL_KINDS = /* @__PURE__ */ new Set(["mcp-server", "command", "package", "env-name"]);
18416
+ function emptyFile() {
18417
+ return { version: 1, apps: /* @__PURE__ */ Object.create(null), links: /* @__PURE__ */ Object.create(null) };
18418
+ }
18419
+ function ownString(map, key) {
18420
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return void 0;
18421
+ const v = map[key];
18422
+ return typeof v === "string" ? v : void 0;
18423
+ }
18424
+ function readSignals(raw) {
18425
+ if (!Array.isArray(raw)) return [];
18426
+ const out = [];
18427
+ for (const x of raw) {
18428
+ if (!x || typeof x.kind !== "string" || typeof x.value !== "string") continue;
18429
+ if (!SIGNAL_KINDS.has(x.kind)) continue;
18430
+ out.push({
18431
+ kind: x.kind,
18432
+ value: x.value.slice(0, 256),
18433
+ ...typeof x.project === "string" ? { project: x.project } : {}
18434
+ });
18435
+ }
18436
+ return out;
18437
+ }
18438
+ function parseAppsFile(text) {
18439
+ const raw = JSON.parse(text);
18440
+ const file2 = emptyFile();
18441
+ for (const [slug2, r] of Object.entries(raw.apps ?? {})) {
18442
+ if (!APP_BY_SLUG.has(slug2) || !r || r.state !== "on" && r.state !== "off") continue;
18443
+ file2.apps[slug2] = {
18444
+ state: r.state,
18445
+ by: r.by === "user" || r.by === "workspace" ? r.by : "detected",
18446
+ at: typeof r.at === "string" ? r.at : "",
18447
+ signals: readSignals(r.signals)
18448
+ };
18449
+ }
18450
+ for (const [server, slug2] of Object.entries(raw.links ?? {})) {
18451
+ if (typeof slug2 === "string" && APP_BY_SLUG.has(slug2)) file2.links[server.toLowerCase()] = slug2;
18452
+ }
18453
+ return file2;
18454
+ }
18455
+ function readAppsFile(home) {
18456
+ return readAppsFileStrict(home) ?? emptyFile();
18457
+ }
18458
+ function readAppsFileStrict(home) {
18459
+ let text;
18460
+ try {
18461
+ text = fs12.readFileSync(appsFilePath(home), "utf8");
18462
+ } catch (e) {
18463
+ return e.code === "ENOENT" ? emptyFile() : null;
18464
+ }
18465
+ try {
18466
+ return parseAppsFile(text);
18467
+ } catch {
18468
+ return null;
18469
+ }
18470
+ }
18471
+ var linkCache = null;
18472
+ var NO_LINKS = /* @__PURE__ */ Object.create(null);
18473
+ function localLinks() {
18474
+ const file2 = appsFilePath();
18475
+ try {
18476
+ const mtimeMs = fs12.statSync(file2).mtimeMs;
18477
+ if (!linkCache || linkCache.file !== file2 || linkCache.mtimeMs !== mtimeMs) {
18478
+ linkCache = { mtimeMs, file: file2, links: readAppsFile().links };
18479
+ }
18480
+ return linkCache.links;
18481
+ } catch {
18482
+ return NO_LINKS;
18483
+ }
18484
+ }
18485
+ function resolveLinkedServer(label2, opts = {}) {
18486
+ if (typeof label2 !== "string" || !label2) return label2;
18487
+ const applied = new Set(opts.appliedShields ?? []);
18488
+ const live = (slug2) => {
18489
+ const app = slug2 ? APP_BY_SLUG.get(slug2) : void 0;
18490
+ return app && applied.has(app.shield) ? slug2 : void 0;
18491
+ };
18492
+ const lower2 = label2.toLowerCase();
18493
+ if (!opts.workspaceGoverned) {
18494
+ const local = live(ownString(localLinks(), lower2));
18495
+ if (local) return local;
18496
+ }
18497
+ const managed = opts.managedLinks ?? {};
18498
+ if (opts.serverKey) {
18499
+ const byKey = live(ownString(managed, opts.serverKey));
18500
+ if (byKey) return byKey;
18501
+ }
18502
+ const keys = Object.keys(managed);
18503
+ if (keys.length === 0) return label2;
18504
+ const tools = readMcpToolsConfig();
18505
+ for (const key of keys) {
18506
+ const name = key.startsWith("cfg:") ? key.split(":").slice(2).join(":") : Object.prototype.hasOwnProperty.call(tools, key) ? tools[key]?.name : void 0;
18507
+ if (typeof name === "string" && name.toLowerCase() === lower2) {
18508
+ const linked = live(ownString(managed, key));
18509
+ if (linked) return linked;
18510
+ }
18511
+ }
18512
+ return label2;
18513
+ }
18514
+
18515
+ // src/auth/orchestrator.ts
18516
+ import os20 from "os";
17045
18517
 
17046
18518
  // src/ui/native.ts
17047
18519
  import { spawn } from "child_process";
17048
- import path12 from "path";
18520
+ import path14 from "path";
17049
18521
 
17050
18522
  // src/context-sniper.ts
17051
- import path11 from "path";
18523
+ import path13 from "path";
17052
18524
  function smartTruncate(str, maxLen = 500) {
17053
18525
  if (str.length <= maxLen) return str;
17054
18526
  const edge = Math.floor(maxLen / 2) - 3;
@@ -17116,7 +18588,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
17116
18588
  intent = "EDIT";
17117
18589
  if (obj.file_path) {
17118
18590
  editFilePath = String(obj.file_path);
17119
- editFileName = path11.basename(editFilePath);
18591
+ editFileName = path13.basename(editFilePath);
17120
18592
  }
17121
18593
  const result = extractContext(String(obj.new_string), matchedWord);
17122
18594
  contextSnippet = result.snippet;
@@ -17184,7 +18656,7 @@ function formatArgs(args, matchedField, matchedWord) {
17184
18656
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
17185
18657
  const obj = parsed;
17186
18658
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
17187
- const file2 = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
18659
+ const file2 = obj.file_path ? path14.basename(String(obj.file_path)) : "file";
17188
18660
  const oldPreview = smartTruncate(String(obj.old_string), 120);
17189
18661
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
17190
18662
  return {
@@ -17387,24 +18859,24 @@ end run`;
17387
18859
  }
17388
18860
 
17389
18861
  // src/canary/registry.ts
17390
- import fs12 from "fs";
17391
- import os10 from "os";
17392
- import path14 from "path";
18862
+ import fs14 from "fs";
18863
+ import os12 from "os";
18864
+ import path16 from "path";
17393
18865
  init_dist();
17394
18866
 
17395
18867
  // src/shields/jail.ts
17396
- import fs11 from "fs";
17397
- import os9 from "os";
17398
- import path13 from "path";
18868
+ import fs13 from "fs";
18869
+ import os11 from "os";
18870
+ import path15 from "path";
17399
18871
  init_dist();
17400
18872
  var USER_JAIL_SHIELD = "user-jail";
17401
18873
  function jailStorePath() {
17402
- return path13.join(os9.homedir(), ".node9", "jail-paths.json");
18874
+ return path15.join(os11.homedir(), ".node9", "jail-paths.json");
17403
18875
  }
17404
18876
  function readJailPaths() {
17405
18877
  let text;
17406
18878
  try {
17407
- text = fs11.readFileSync(jailStorePath(), "utf8");
18879
+ text = fs13.readFileSync(jailStorePath(), "utf8");
17408
18880
  } catch (err) {
17409
18881
  if (err.code === "ENOENT") return [];
17410
18882
  throw err;
@@ -17433,7 +18905,7 @@ function findJailedPathIn(candidate, paths) {
17433
18905
 
17434
18906
  // src/canary/registry.ts
17435
18907
  function canaryStorePath() {
17436
- return path14.join(os10.homedir(), ".node9", "canaries.json");
18908
+ return path16.join(os12.homedir(), ".node9", "canaries.json");
17437
18909
  }
17438
18910
  function isRecord(x) {
17439
18911
  if (!x || typeof x !== "object") return false;
@@ -17444,7 +18916,7 @@ function loadCanaries(opts) {
17444
18916
  const p = canaryStorePath();
17445
18917
  let raw;
17446
18918
  try {
17447
- raw = fs12.readFileSync(p, "utf-8");
18919
+ raw = fs14.readFileSync(p, "utf-8");
17448
18920
  } catch (e) {
17449
18921
  if (e.code === "ENOENT") return [];
17450
18922
  throw e;
@@ -17471,21 +18943,21 @@ init_dist();
17471
18943
 
17472
18944
  // src/auth/session-script.ts
17473
18945
  init_dist();
17474
- import fs13 from "fs";
17475
- import os11 from "os";
17476
- import path15 from "path";
18946
+ import fs15 from "fs";
18947
+ import os13 from "os";
18948
+ import path17 from "path";
17477
18949
  var MAX_SCRIPT_BYTES = 64 * 1024;
17478
18950
  var MAX_RECORDED_WRITES = 32;
17479
18951
  function readScriptFile(resolved) {
17480
18952
  let fd;
17481
18953
  try {
17482
- fd = fs13.openSync(resolved, fs13.constants.O_RDONLY | (fs13.constants.O_NONBLOCK ?? 0));
17483
- const st = fs13.fstatSync(fd);
18954
+ fd = fs15.openSync(resolved, fs15.constants.O_RDONLY | (fs15.constants.O_NONBLOCK ?? 0));
18955
+ const st = fs15.fstatSync(fd);
17484
18956
  if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
17485
18957
  const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
17486
18958
  let n = 0;
17487
18959
  while (n < buf.length) {
17488
- const got = fs13.readSync(fd, buf, n, buf.length - n, n);
18960
+ const got = fs15.readSync(fd, buf, n, buf.length - n, n);
17489
18961
  if (got === 0) break;
17490
18962
  n += got;
17491
18963
  }
@@ -17496,7 +18968,7 @@ function readScriptFile(resolved) {
17496
18968
  } finally {
17497
18969
  if (fd !== void 0) {
17498
18970
  try {
17499
- fs13.closeSync(fd);
18971
+ fs15.closeSync(fd);
17500
18972
  } catch {
17501
18973
  }
17502
18974
  }
@@ -17504,9 +18976,9 @@ function readScriptFile(resolved) {
17504
18976
  }
17505
18977
  function absolute(p, cwd, home) {
17506
18978
  const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
17507
- return path15.resolve(cwd, spelled);
18979
+ return path17.resolve(cwd, spelled);
17508
18980
  }
17509
- async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os11.homedir()) {
18981
+ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os13.homedir()) {
17510
18982
  try {
17511
18983
  const d = deps ?? { checkWritten, readScript: readScriptFile };
17512
18984
  const shell = resolveShellInput(toolName, args, layers);
@@ -17535,7 +19007,7 @@ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os
17535
19007
  return null;
17536
19008
  }
17537
19009
  }
17538
- function sessionWriteTargets(toolName, args, fileAccess, cwd, home = os11.homedir()) {
19010
+ function sessionWriteTargets(toolName, args, fileAccess, cwd, home = os13.homedir()) {
17539
19011
  const base2 = cwd ?? process.cwd();
17540
19012
  const out = /* @__PURE__ */ new Set();
17541
19013
  const argsObj = args && typeof args === "object" ? args : {};
@@ -17565,26 +19037,26 @@ init_audit();
17565
19037
  init_dist();
17566
19038
 
17567
19039
  // src/exposure/known-hosts.ts
17568
- import fs15 from "fs";
17569
- import path17 from "path";
19040
+ import fs17 from "fs";
19041
+ import path19 from "path";
17570
19042
 
17571
19043
  // src/utils/git-remotes.ts
17572
19044
  init_dist();
17573
- import fs14 from "fs";
17574
- import path16 from "path";
19045
+ import fs16 from "fs";
19046
+ import path18 from "path";
17575
19047
  function readSmallFile(p, max) {
17576
19048
  let fd;
17577
19049
  try {
17578
- fd = fs14.openSync(p, fs14.constants.O_RDONLY | (fs14.constants.O_NOFOLLOW ?? 0));
17579
- const st = fs14.fstatSync(fd);
19050
+ fd = fs16.openSync(p, fs16.constants.O_RDONLY | (fs16.constants.O_NOFOLLOW ?? 0));
19051
+ const st = fs16.fstatSync(fd);
17580
19052
  if (!st.isFile() || st.size > max) return null;
17581
19053
  const buf = Buffer.alloc(st.size);
17582
- const n = fs14.readSync(fd, buf, 0, st.size, 0);
19054
+ const n = fs16.readSync(fd, buf, 0, st.size, 0);
17583
19055
  return buf.subarray(0, n).toString("utf8");
17584
19056
  } catch {
17585
19057
  return null;
17586
19058
  } finally {
17587
- if (fd !== void 0) fs14.closeSync(fd);
19059
+ if (fd !== void 0) fs16.closeSync(fd);
17588
19060
  }
17589
19061
  }
17590
19062
  function gitRemoteHost(url) {
@@ -17603,20 +19075,20 @@ function gitRemoteHost(url) {
17603
19075
  return null;
17604
19076
  }
17605
19077
  function findGitConfig(dir2, walkUp = true) {
17606
- let cur = path16.resolve(dir2);
19078
+ let cur = path18.resolve(dir2);
17607
19079
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
17608
- const dotGit = path16.join(cur, ".git");
17609
- const direct = readSmallFile(path16.join(dotGit, "config"), 1e6);
19080
+ const dotGit = path18.join(cur, ".git");
19081
+ const direct = readSmallFile(path18.join(dotGit, "config"), 1e6);
17610
19082
  if (direct !== null) return direct;
17611
19083
  const pointer = readSmallFile(dotGit, 4096);
17612
19084
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
17613
19085
  if (m) {
17614
- let gitDir = path16.resolve(cur, m[1]);
17615
- const common = readSmallFile(path16.join(gitDir, "commondir"), 4096);
17616
- if (common !== null) gitDir = path16.resolve(gitDir, common.trim());
17617
- return readSmallFile(path16.join(gitDir, "config"), 1e6);
19086
+ let gitDir = path18.resolve(cur, m[1]);
19087
+ const common = readSmallFile(path18.join(gitDir, "commondir"), 4096);
19088
+ if (common !== null) gitDir = path18.resolve(gitDir, common.trim());
19089
+ return readSmallFile(path18.join(gitDir, "config"), 1e6);
17618
19090
  }
17619
- const parent = path16.dirname(cur);
19091
+ const parent = path18.dirname(cur);
17620
19092
  if (parent === cur) return null;
17621
19093
  cur = parent;
17622
19094
  }
@@ -17626,12 +19098,12 @@ var cache = /* @__PURE__ */ new Map();
17626
19098
  function readGitRemotes(dir2, opts = {}) {
17627
19099
  if (!dir2) return [];
17628
19100
  const walkUp = opts.walkUp !== false;
17629
- const key = `${walkUp ? "up" : "at"}:${path16.resolve(dir2)}`;
19101
+ const key = `${walkUp ? "up" : "at"}:${path18.resolve(dir2)}`;
17630
19102
  const hit = cache.get(key);
17631
19103
  if (hit) return hit;
17632
19104
  const out = [];
17633
19105
  try {
17634
- const cfg = findGitConfig(path16.resolve(dir2), walkUp);
19106
+ const cfg = findGitConfig(path18.resolve(dir2), walkUp);
17635
19107
  if (cfg !== null) {
17636
19108
  let current = null;
17637
19109
  for (const line of cfg.split(/\r?\n/)) {
@@ -17667,15 +19139,15 @@ var ECOSYSTEMS = [
17667
19139
  ];
17668
19140
  var cache2 = /* @__PURE__ */ new Map();
17669
19141
  function manifestRoot(dir2) {
17670
- let cur = path17.resolve(dir2);
19142
+ let cur = path19.resolve(dir2);
17671
19143
  for (let i = 0; i < 64; i++) {
17672
19144
  for (const [m] of ECOSYSTEMS) {
17673
19145
  try {
17674
- if (fs15.existsSync(path17.join(cur, m))) return cur;
19146
+ if (fs17.existsSync(path19.join(cur, m))) return cur;
17675
19147
  } catch {
17676
19148
  }
17677
19149
  }
17678
- const parent = path17.dirname(cur);
19150
+ const parent = path19.dirname(cur);
17679
19151
  if (parent === cur) return null;
17680
19152
  cur = parent;
17681
19153
  }
@@ -17684,7 +19156,7 @@ function manifestRoot(dir2) {
17684
19156
  function knownProjectHosts(dir2) {
17685
19157
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
17686
19158
  if (!dir2) return empty;
17687
- const key = path17.resolve(dir2);
19159
+ const key = path19.resolve(dir2);
17688
19160
  const hit = cache2.get(key);
17689
19161
  if (hit) return hit;
17690
19162
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
@@ -17697,10 +19169,10 @@ function knownProjectHosts(dir2) {
17697
19169
  const root = manifestRoot(key);
17698
19170
  if (root !== null) {
17699
19171
  for (const [m, registry] of ECOSYSTEMS) {
17700
- if (fs15.existsSync(path17.join(root, m))) out.hosts.add(registry);
19172
+ if (fs17.existsSync(path19.join(root, m))) out.hosts.add(registry);
17701
19173
  }
17702
19174
  try {
17703
- if (fs15.readdirSync(root).some((f) => f.endsWith(".gemspec")))
19175
+ if (fs17.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17704
19176
  out.hosts.add("host:rubygems.org");
17705
19177
  } catch {
17706
19178
  }
@@ -17713,9 +19185,9 @@ function knownProjectHosts(dir2) {
17713
19185
 
17714
19186
  // src/exposure/writes.ts
17715
19187
  init_dist();
17716
- import fs16 from "fs";
17717
- import os12 from "os";
17718
- import path18 from "path";
19188
+ import fs18 from "fs";
19189
+ import os14 from "os";
19190
+ import path20 from "path";
17719
19191
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
17720
19192
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
17721
19193
  function writtenContent(args) {
@@ -17744,7 +19216,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17744
19216
  if (typeof a.content === "string") {
17745
19217
  try {
17746
19218
  const next = JSON.parse(a.content);
17747
- const prev = JSON.parse(fs16.readFileSync(absPath, "utf8"));
19219
+ const prev = JSON.parse(fs18.readFileSync(absPath, "utf8"));
17748
19220
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
17749
19221
  } catch {
17750
19222
  return LIFECYCLE_RE.test(a.content);
@@ -17754,8 +19226,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17754
19226
  }
17755
19227
  function judgeWritesAfterExposure(opts) {
17756
19228
  const holds = [];
17757
- const home = opts.home ?? os12.homedir();
17758
- const tmp = normalizeFilePath(os12.tmpdir());
19229
+ const home = opts.home ?? os14.homedir();
19230
+ const tmp = normalizeFilePath(os14.tmpdir());
17759
19231
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
17760
19232
  const seen = /* @__PURE__ */ new Set();
17761
19233
  for (const access of opts.accesses) {
@@ -17765,7 +19237,7 @@ function judgeWritesAfterExposure(opts) {
17765
19237
  seen.add(p);
17766
19238
  if (p === "/dev" || p.startsWith("/dev/")) continue;
17767
19239
  if (root !== null && isInside(root, p)) {
17768
- if (isAgentInstructionPath(p, root) && (path18.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
19240
+ if (isAgentInstructionPath(p, root) && (path20.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17769
19241
  holds.push({ path: p, why: "agent-instruction" });
17770
19242
  continue;
17771
19243
  }
@@ -17787,25 +19259,25 @@ function judgeWritesAfterExposure(opts) {
17787
19259
 
17788
19260
  // src/exposure/marker.ts
17789
19261
  import crypto3 from "crypto";
17790
- import fs17 from "fs";
17791
- import os13 from "os";
17792
- import path19 from "path";
19262
+ import fs19 from "fs";
19263
+ import os15 from "os";
19264
+ import path21 from "path";
17793
19265
 
17794
19266
  // src/daemon/exposure-store.ts
17795
19267
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
17796
19268
 
17797
19269
  // src/exposure/marker.ts
17798
19270
  function dir() {
17799
- return path19.join(os13.homedir(), ".node9", "exposure-sessions");
19271
+ return path21.join(os15.homedir(), ".node9", "exposure-sessions");
17800
19272
  }
17801
19273
  function file(sessionId) {
17802
19274
  const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
17803
- return path19.join(dir(), id);
19275
+ return path21.join(dir(), id);
17804
19276
  }
17805
19277
  function maybeSessionExposed(sessionId) {
17806
19278
  if (!sessionId) return false;
17807
19279
  try {
17808
- const st = fs17.statSync(file(sessionId));
19280
+ const st = fs19.statSync(file(sessionId));
17809
19281
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
17810
19282
  } catch (e) {
17811
19283
  return e.code !== "ENOENT";
@@ -17813,27 +19285,7 @@ function maybeSessionExposed(sessionId) {
17813
19285
  }
17814
19286
 
17815
19287
  // src/exposure/roles.ts
17816
- import fs19 from "fs";
17817
-
17818
- // src/daemon/mcp-tools.ts
17819
- import fs18 from "fs";
17820
- import path20 from "path";
17821
- import os14 from "os";
17822
- function getMcpToolsFile() {
17823
- return path20.join(os14.homedir(), ".node9", "mcp-tools.json");
17824
- }
17825
- function readMcpToolsConfig() {
17826
- try {
17827
- const file2 = getMcpToolsFile();
17828
- if (!fs18.existsSync(file2)) return {};
17829
- const raw = fs18.readFileSync(file2, "utf-8");
17830
- return JSON.parse(raw);
17831
- } catch {
17832
- return {};
17833
- }
17834
- }
17835
-
17836
- // src/exposure/roles.ts
19288
+ import fs20 from "fs";
17837
19289
  var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
17838
19290
  var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
17839
19291
  function foldServerName(name) {
@@ -17843,7 +19295,7 @@ var inventoryCache = null;
17843
19295
  var inventoryStamp = null;
17844
19296
  function inventoryFileStamp() {
17845
19297
  try {
17846
- const st = fs19.statSync(getMcpToolsFile());
19298
+ const st = fs20.statSync(getMcpToolsFile());
17847
19299
  return `${st.mtimeMs}:${st.size}`;
17848
19300
  } catch {
17849
19301
  return "";
@@ -17928,15 +19380,15 @@ function sourceWord(toolName) {
17928
19380
 
17929
19381
  // src/auth/trusted-recipients.ts
17930
19382
  init_dist();
17931
- import fs20 from "fs";
17932
- import path21 from "path";
17933
- import os15 from "os";
19383
+ import fs21 from "fs";
19384
+ import path22 from "path";
19385
+ import os16 from "os";
17934
19386
  function getTrustedRecipientsPath() {
17935
- return path21.join(os15.homedir(), ".node9", "trusted-recipients.json");
19387
+ return path22.join(os16.homedir(), ".node9", "trusted-recipients.json");
17936
19388
  }
17937
19389
  function readTrustedRecipients() {
17938
19390
  try {
17939
- const raw = fs20.readFileSync(getTrustedRecipientsPath(), "utf8");
19391
+ const raw = fs21.readFileSync(getTrustedRecipientsPath(), "utf8");
17940
19392
  const parsed = JSON.parse(raw);
17941
19393
  return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
17942
19394
  } catch {
@@ -17945,10 +19397,10 @@ function readTrustedRecipients() {
17945
19397
  }
17946
19398
  function writeTrustedRecipients(entries) {
17947
19399
  const filePath = getTrustedRecipientsPath();
17948
- fs20.mkdirSync(path21.dirname(filePath), { recursive: true });
19400
+ fs21.mkdirSync(path22.dirname(filePath), { recursive: true });
17949
19401
  const tmp = filePath + ".node9-tmp";
17950
- fs20.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
17951
- fs20.renameSync(tmp, filePath);
19402
+ fs21.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
19403
+ fs21.renameSync(tmp, filePath);
17952
19404
  }
17953
19405
  function addTrustedRecipient(raw, addedBy = "user") {
17954
19406
  const value = normalizeRecipient(raw);
@@ -17963,7 +19415,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
17963
19415
  var cache3 = null;
17964
19416
  function mtimeOf() {
17965
19417
  try {
17966
- return fs20.statSync(getTrustedRecipientsPath()).mtimeMs;
19418
+ return fs21.statSync(getTrustedRecipientsPath()).mtimeMs;
17967
19419
  } catch {
17968
19420
  return 0;
17969
19421
  }
@@ -17978,40 +19430,40 @@ function isTrustedRecipient(value) {
17978
19430
 
17979
19431
  // src/auth/cloud.ts
17980
19432
  init_audit();
17981
- import fs22 from "fs";
17982
- import os16 from "os";
17983
- import path22 from "path";
19433
+ import fs23 from "fs";
19434
+ import os17 from "os";
19435
+ import path23 from "path";
17984
19436
 
17985
19437
  // src/utils/read-capped.ts
17986
- import fs21 from "fs";
19438
+ import fs22 from "fs";
17987
19439
  function readCapped(file2, maxBytes) {
17988
19440
  if (maxBytes < 0) return null;
17989
19441
  let fd;
17990
19442
  try {
17991
- fd = fs21.openSync(file2, "r");
19443
+ fd = fs22.openSync(file2, "r");
17992
19444
  } catch {
17993
19445
  return null;
17994
19446
  }
17995
19447
  try {
17996
- const st = fs21.fstatSync(fd);
19448
+ const st = fs22.fstatSync(fd);
17997
19449
  if (!st.isFile()) return null;
17998
19450
  const want = Math.min(st.size, maxBytes + 1);
17999
19451
  const buf = Buffer.alloc(want);
18000
- let read = 0;
18001
- while (read < want) {
18002
- const n = fs21.readSync(fd, buf, read, want - read, read);
19452
+ let read2 = 0;
19453
+ while (read2 < want) {
19454
+ const n = fs22.readSync(fd, buf, read2, want - read2, read2);
18003
19455
  if (n <= 0) break;
18004
- read += n;
19456
+ read2 += n;
18005
19457
  }
18006
19458
  return {
18007
- bytes: buf.subarray(0, Math.min(read, maxBytes)),
18008
- truncated: read > maxBytes
19459
+ bytes: buf.subarray(0, Math.min(read2, maxBytes)),
19460
+ truncated: read2 > maxBytes
18009
19461
  };
18010
19462
  } catch {
18011
19463
  return null;
18012
19464
  } finally {
18013
19465
  try {
18014
- fs21.closeSync(fd);
19466
+ fs22.closeSync(fd);
18015
19467
  } catch {
18016
19468
  }
18017
19469
  }
@@ -18056,7 +19508,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18056
19508
  let ciContext;
18057
19509
  if (process.env.CI) {
18058
19510
  try {
18059
- const ciContextPath = path22.join(os16.homedir(), ".node9", "ci-context.json");
19511
+ const ciContextPath = path23.join(os17.homedir(), ".node9", "ci-context.json");
18060
19512
  const ci = readCappedText(ciContextPath, 1e4);
18061
19513
  if (!ci) throw new Error("ci-context.json unreadable");
18062
19514
  if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
@@ -18098,9 +19550,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18098
19550
  context: {
18099
19551
  agent: meta?.agent,
18100
19552
  mcpServer: meta?.mcpServer,
18101
- hostname: os16.hostname(),
19553
+ hostname: os17.hostname(),
18102
19554
  cwd: process.cwd(),
18103
- platform: os16.platform()
19555
+ platform: os17.platform()
18104
19556
  },
18105
19557
  // The SaaS validates riskMetadata strictly and would drop it whole on
18106
19558
  // an unknown key, so only fields its schema declares go on the wire.
@@ -18174,14 +19626,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18174
19626
  });
18175
19627
  clearTimeout(timer);
18176
19628
  if (!res.ok) {
18177
- fs22.appendFileSync(
19629
+ fs23.appendFileSync(
18178
19630
  HOOK_DEBUG_LOG,
18179
19631
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
18180
19632
  `
18181
19633
  );
18182
19634
  }
18183
19635
  } catch (err) {
18184
- fs22.appendFileSync(
19636
+ fs23.appendFileSync(
18185
19637
  HOOK_DEBUG_LOG,
18186
19638
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
18187
19639
  `
@@ -18192,16 +19644,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18192
19644
  // src/loop-detector.ts
18193
19645
  init_dist();
18194
19646
  init_dist();
18195
- import fs23 from "fs";
18196
- import path23 from "path";
18197
- import os17 from "os";
19647
+ import fs24 from "fs";
19648
+ import path24 from "path";
19649
+ import os18 from "os";
18198
19650
  function loopStateFile() {
18199
- return path23.join(os17.homedir(), ".node9", "loop-state.json");
19651
+ return path24.join(os18.homedir(), ".node9", "loop-state.json");
18200
19652
  }
18201
19653
  function readState() {
18202
19654
  try {
18203
- if (!fs23.existsSync(loopStateFile())) return [];
18204
- const raw = fs23.readFileSync(loopStateFile(), "utf-8");
19655
+ if (!fs24.existsSync(loopStateFile())) return [];
19656
+ const raw = fs24.readFileSync(loopStateFile(), "utf-8");
18205
19657
  const parsed = JSON.parse(raw);
18206
19658
  if (!Array.isArray(parsed)) return [];
18207
19659
  return parsed;
@@ -18210,11 +19662,11 @@ function readState() {
18210
19662
  }
18211
19663
  }
18212
19664
  function writeState(records) {
18213
- const dir2 = path23.dirname(loopStateFile());
18214
- if (!fs23.existsSync(dir2)) fs23.mkdirSync(dir2, { recursive: true });
18215
- const tmpPath = `${loopStateFile()}.${os17.hostname()}.${process.pid}.tmp`;
18216
- fs23.writeFileSync(tmpPath, JSON.stringify(records));
18217
- fs23.renameSync(tmpPath, loopStateFile());
19665
+ const dir2 = path24.dirname(loopStateFile());
19666
+ if (!fs24.existsSync(dir2)) fs24.mkdirSync(dir2, { recursive: true });
19667
+ const tmpPath = `${loopStateFile()}.${os18.hostname()}.${process.pid}.tmp`;
19668
+ fs24.writeFileSync(tmpPath, JSON.stringify(records));
19669
+ fs24.renameSync(tmpPath, loopStateFile());
18218
19670
  }
18219
19671
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18220
19672
  try {
@@ -18229,20 +19681,20 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18229
19681
  // src/supply-chain/check.ts
18230
19682
  init_dist();
18231
19683
  import picomatch2 from "picomatch";
18232
- import path26 from "path";
19684
+ import path27 from "path";
18233
19685
 
18234
19686
  // src/supply-chain/osv-index.ts
18235
19687
  init_dist();
18236
- import fs24 from "fs";
18237
- import os18 from "os";
18238
- import path24 from "path";
19688
+ import fs25 from "fs";
19689
+ import os19 from "os";
19690
+ import path25 from "path";
18239
19691
  import { createHash as createHash4 } from "crypto";
18240
19692
  var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
18241
19693
  function osvRoot() {
18242
- return path24.join(os18.homedir(), ".node9", "osv");
19694
+ return path25.join(os19.homedir(), ".node9", "osv");
18243
19695
  }
18244
19696
  function ecoDir(eco) {
18245
- return path24.join(osvRoot(), eco);
19697
+ return path25.join(osvRoot(), eco);
18246
19698
  }
18247
19699
  function indexKey(eco, name) {
18248
19700
  switch (eco) {
@@ -18261,11 +19713,11 @@ function shardOf(key) {
18261
19713
  return createHash4("sha1").update(key).digest("hex").slice(0, 2);
18262
19714
  }
18263
19715
  function shardPath(eco, shard) {
18264
- return path24.join(ecoDir(eco), "shards", `${shard}.json`);
19716
+ return path25.join(ecoDir(eco), "shards", `${shard}.json`);
18265
19717
  }
18266
19718
  function readMeta(eco) {
18267
19719
  try {
18268
- const m = JSON.parse(fs24.readFileSync(path24.join(ecoDir(eco), "meta.json"), "utf8"));
19720
+ const m = JSON.parse(fs25.readFileSync(path25.join(ecoDir(eco), "meta.json"), "utf8"));
18269
19721
  if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
18270
19722
  return m;
18271
19723
  } catch {
@@ -18274,7 +19726,7 @@ function readMeta(eco) {
18274
19726
  }
18275
19727
  function readShard(eco, shard) {
18276
19728
  try {
18277
- const s = JSON.parse(fs24.readFileSync(shardPath(eco, shard), "utf8"));
19729
+ const s = JSON.parse(fs25.readFileSync(shardPath(eco, shard), "utf8"));
18278
19730
  return s && typeof s === "object" && !Array.isArray(s) ? s : {};
18279
19731
  } catch {
18280
19732
  return {};
@@ -18401,13 +19853,101 @@ async function queryOsvMalicious(queries) {
18401
19853
 
18402
19854
  // src/supply-chain/registry.ts
18403
19855
  import { maxSatisfying, satisfies, validRange } from "semver";
19856
+
19857
+ // src/supply-chain/provenance.ts
19858
+ import { prerelease, rcompare, valid } from "semver";
18404
19859
  var TIMEOUT_MS2 = 1500;
18405
- var MAX_BYTES = 2 * 1024 * 1024;
19860
+ var MAX_BYTES = 256 * 1024;
19861
+ function normalizeRepo(raw, host2 = "github.com") {
19862
+ let s = raw.trim().toLowerCase();
19863
+ s = s.replace(/^git\+/, "").replace(/^[a-z]+:\/\//, "");
19864
+ s = s.replace(/@refs\/.*$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
19865
+ if (!s.includes(".") || !s.split("/")[0].includes(".")) s = `${host2}/${s}`;
19866
+ return /^[a-z0-9.-]+\/[\w.-]+\/[\w.-]+/.test(s) ? s : null;
19867
+ }
19868
+ function npmRepoOf(doc) {
19869
+ const list = doc?.attestations;
19870
+ if (!Array.isArray(list)) return null;
19871
+ for (const a of list) {
19872
+ const att = a;
19873
+ if (!att.predicateType?.startsWith("https://slsa.dev/provenance/")) continue;
19874
+ const payload = att.bundle?.dsseEnvelope?.payload;
19875
+ if (typeof payload !== "string") continue;
19876
+ try {
19877
+ const statement = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
19878
+ const p = statement.predicate;
19879
+ const repo = p?.buildDefinition?.externalParameters?.workflow?.repository ?? p?.invocation?.configSource?.uri;
19880
+ if (typeof repo === "string") return normalizeRepo(repo);
19881
+ } catch {
19882
+ }
19883
+ }
19884
+ return null;
19885
+ }
19886
+ function pypiRepoOf(doc) {
19887
+ const bundles = doc?.attestation_bundles;
19888
+ if (!Array.isArray(bundles)) return null;
19889
+ for (const b of bundles) {
19890
+ const pub = b.publisher;
19891
+ if (typeof pub?.repository === "string") {
19892
+ const host2 = /gitlab/i.test(pub.kind ?? "") ? "gitlab.com" : "github.com";
19893
+ return normalizeRepo(pub.repository, host2);
19894
+ }
19895
+ if (/^google$/i.test(pub?.kind ?? "") && typeof pub?.email === "string") {
19896
+ return `google:${pub.email.trim().toLowerCase()}`;
19897
+ }
19898
+ }
19899
+ return null;
19900
+ }
19901
+ async function npmSourceRepo(name, version) {
19902
+ const base2 = endpoints().npmRegistry;
19903
+ if (!base2) return "unknown";
19904
+ const url = `${base2}/-/npm/v1/attestations/${name.replaceAll("/", "%2F")}@${encodeURIComponent(version)}`;
19905
+ return read(url, npmRepoOf);
19906
+ }
19907
+ async function read(url, repoOf) {
19908
+ try {
19909
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
19910
+ if (doc === null) return "none";
19911
+ return repoOf(doc) ?? "unknown";
19912
+ } catch {
19913
+ return "unknown";
19914
+ }
19915
+ }
19916
+ async function pypiSourceRepo(name, version, filename) {
19917
+ const base2 = endpoints().pypi;
19918
+ if (!base2) return "unknown";
19919
+ const url = `${base2}/integrity/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}/provenance`;
19920
+ return read(url, pypiRepoOf);
19921
+ }
19922
+ var PYPI_FINAL = /^\d+(?:\.\d+)*$/;
19923
+ function comparePypi(a, b) {
19924
+ const x = a.split(".").map(Number);
19925
+ const y = b.split(".").map(Number);
19926
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
19927
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
19928
+ if (d !== 0) return d;
19929
+ }
19930
+ return 0;
19931
+ }
19932
+ function previousRelease(eco, versions, version) {
19933
+ if (eco === "npm") {
19934
+ if (!valid(version)) return void 0;
19935
+ const below2 = versions.filter((v) => valid(v) && !prerelease(v) && rcompare(v, version) > 0);
19936
+ return below2.sort(rcompare)[0] ?? null;
19937
+ }
19938
+ if (!PYPI_FINAL.test(version)) return void 0;
19939
+ const below = versions.filter((v) => PYPI_FINAL.test(v) && comparePypi(v, version) < 0);
19940
+ return below.sort((a, b) => comparePypi(b, a))[0] ?? null;
19941
+ }
19942
+
19943
+ // src/supply-chain/registry.ts
19944
+ var TIMEOUT_MS3 = 1500;
19945
+ var MAX_BYTES2 = 2 * 1024 * 1024;
18406
19946
  var VERSION_DOC_MAX_BYTES = 256 * 1024;
18407
19947
  var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
18408
19948
  async function npmVersionDocument(url, selector) {
18409
19949
  const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
18410
- timeoutMs: TIMEOUT_MS2,
19950
+ timeoutMs: TIMEOUT_MS3,
18411
19951
  maxBytes: VERSION_DOC_MAX_BYTES
18412
19952
  });
18413
19953
  if (!doc?.version) return null;
@@ -18431,8 +19971,8 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18431
19971
  let doc;
18432
19972
  try {
18433
19973
  doc = await fetchJson(url, {
18434
- timeoutMs: TIMEOUT_MS2,
18435
- maxBytes: MAX_BYTES,
19974
+ timeoutMs: TIMEOUT_MS3,
19975
+ maxBytes: MAX_BYTES2,
18436
19976
  headers: { Accept: "application/vnd.npm.install-v1+json" }
18437
19977
  });
18438
19978
  } catch (err) {
@@ -18460,13 +20000,23 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18460
20000
  version: resolved,
18461
20001
  hasInstallScript: v?.hasInstallScript === true
18462
20002
  };
20003
+ const previous = previousRelease("npm", Object.keys(doc.versions ?? {}), resolved);
20004
+ if (v && previous !== void 0) {
20005
+ const pv = previous ? doc.versions?.[previous] : void 0;
20006
+ info.history = {
20007
+ previous,
20008
+ previousHasInstallScript: pv ? pv.hasInstallScript === true : void 0,
20009
+ provenance: !!v.dist?.attestations?.provenance,
20010
+ previousProvenance: pv ? !!pv.dist?.attestations?.provenance : void 0
20011
+ };
20012
+ }
18463
20013
  if (!Number.isFinite(modifiedMs)) return info;
18464
20014
  if (Date.now() - modifiedMs >= freshWindowMs) {
18465
20015
  info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
18466
20016
  return info;
18467
20017
  }
18468
20018
  try {
18469
- const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
20019
+ const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18470
20020
  const t = full?.time?.[resolved];
18471
20021
  const tMs = t ? Date.parse(t) : NaN;
18472
20022
  if (Number.isFinite(tMs)) {
@@ -18484,15 +20034,37 @@ async function pypiInfo(name, version) {
18484
20034
  const base2 = endpoints().pypi;
18485
20035
  if (!base2) return null;
18486
20036
  const url = version ? `${base2}/pypi/${encodeURIComponent(name)}/${encodeURIComponent(version)}/json` : `${base2}/pypi/${encodeURIComponent(name)}/json`;
18487
- const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
20037
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18488
20038
  if (!doc?.info?.version) return null;
18489
20039
  const times = (doc.urls ?? []).map((u) => u.upload_time_iso_8601 ? Date.parse(u.upload_time_iso_8601) : NaN).filter((t) => Number.isFinite(t));
18490
- return {
18491
- version: version ?? doc.info.version,
20040
+ const resolved = version ?? doc.info.version;
20041
+ const info = {
20042
+ version: resolved,
18492
20043
  // The earliest file of the release is when it became installable.
18493
- publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
20044
+ publishedAtMs: times.length > 0 ? Math.min(...times) : void 0,
20045
+ file: doc.urls?.find((u) => typeof u.filename === "string")?.filename
20046
+ };
20047
+ if (doc.releases) info.history = pypiHistoryOf(doc.releases, resolved) ?? void 0;
20048
+ return info;
20049
+ }
20050
+ function pypiHistoryOf(releases, version) {
20051
+ const versions = Object.keys(releases).filter((v) => (releases[v] ?? []).length > 0);
20052
+ const previous = previousRelease("PyPI", versions, version);
20053
+ if (previous === void 0) return null;
20054
+ return {
20055
+ previous,
20056
+ previousFile: previous ? releases[previous]?.find((f) => typeof f.filename === "string")?.filename : void 0
18494
20057
  };
18495
20058
  }
20059
+ async function pypiHistory(name, version) {
20060
+ const base2 = endpoints().pypi;
20061
+ if (!base2) return null;
20062
+ const doc = await fetchJson(`${base2}/pypi/${encodeURIComponent(name)}/json`, {
20063
+ timeoutMs: TIMEOUT_MS3,
20064
+ maxBytes: MAX_BYTES2
20065
+ });
20066
+ return doc?.releases ? pypiHistoryOf(doc.releases, version) : null;
20067
+ }
18496
20068
  var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
18497
20069
  async function registryInfo(eco, name, version, freshWindowMs, raw) {
18498
20070
  if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
@@ -18501,19 +20073,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
18501
20073
  }
18502
20074
 
18503
20075
  // src/supply-chain/local-resolve.ts
18504
- import fs25 from "fs";
18505
- import path25 from "path";
20076
+ import fs26 from "fs";
20077
+ import path26 from "path";
18506
20078
  var MAX_LEVELS = 32;
18507
20079
  var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
18508
20080
  function resolveInstalled(name, cwd) {
18509
- if (!cwd || !path25.isAbsolute(cwd)) return null;
20081
+ if (!cwd || !path26.isAbsolute(cwd)) return null;
18510
20082
  if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
18511
- let dir2 = path25.resolve(cwd);
20083
+ let dir2 = path26.resolve(cwd);
18512
20084
  for (let i = 0; i < MAX_LEVELS; i++) {
18513
- const pkgDir = path25.join(dir2, "node_modules", ...name.split("/"));
18514
- const manifest = path25.join(pkgDir, "package.json");
20085
+ const pkgDir = path26.join(dir2, "node_modules", ...name.split("/"));
20086
+ const manifest = path26.join(pkgDir, "package.json");
18515
20087
  try {
18516
- const raw = fs25.readFileSync(manifest, "utf8");
20088
+ const raw = fs26.readFileSync(manifest, "utf8");
18517
20089
  const parsed = JSON.parse(raw);
18518
20090
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
18519
20091
  return { version: parsed.version, dir: pkgDir };
@@ -18521,7 +20093,7 @@ function resolveInstalled(name, cwd) {
18521
20093
  return null;
18522
20094
  } catch {
18523
20095
  }
18524
- const parent = path25.dirname(dir2);
20096
+ const parent = path26.dirname(dir2);
18525
20097
  if (parent === dir2) break;
18526
20098
  dir2 = parent;
18527
20099
  }
@@ -18643,7 +20215,7 @@ function checkInstalled(p, version, misses) {
18643
20215
  }
18644
20216
  async function checkOne(p, cfg, misses, networkCapped, cwd) {
18645
20217
  if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
18646
- const localCwd = cwd && path26.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path26.resolve(dir2, step), cwd) : void 0;
20218
+ const localCwd = cwd && path27.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path27.resolve(dir2, step), cwd) : void 0;
18647
20219
  const installed = resolveInstalled(p.name, localCwd);
18648
20220
  if (installed && (!p.version || p.version === installed.version))
18649
20221
  return checkInstalled(p, installed.version, misses);
@@ -18686,8 +20258,61 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
18686
20258
  detail: `${label(p, version)} runs an install script`
18687
20259
  });
18688
20260
  }
20261
+ if (info && version && (cfg.newPackage !== "off" || cfg.installScript !== "off")) {
20262
+ return byHistory(p, version, info, findings, misses);
20263
+ }
18689
20264
  return findings;
18690
20265
  }
20266
+ var SOFT2 = /* @__PURE__ */ new Set(["new", "install-script"]);
20267
+ async function byHistory(p, version, info, findings, misses) {
20268
+ let out = findings;
20269
+ let h = info.history;
20270
+ if (!h && p.ecosystem === "PyPI" && out.some((f) => SOFT2.has(f.kind))) {
20271
+ try {
20272
+ h = await pypiHistory(p.name, version) ?? void 0;
20273
+ } catch (err) {
20274
+ misses.push(`${label(p, version)}: release history ${err.message}`);
20275
+ }
20276
+ }
20277
+ if (!h || !h.previous) return out;
20278
+ const previous = h.previous;
20279
+ if (h.previousHasInstallScript === true) {
20280
+ out = out.filter((f) => f.kind !== "install-script");
20281
+ }
20282
+ const drop = () => ({
20283
+ pkg: p,
20284
+ version,
20285
+ kind: "provenance-drop",
20286
+ detail: `${label(p, version)} was published without the provenance its previous release (${previous}) had`
20287
+ });
20288
+ if (p.ecosystem === "npm") {
20289
+ if (h.previousProvenance && h.provenance === false) return [...out, drop()];
20290
+ if (!out.some((f) => SOFT2.has(f.kind)) || !h.provenance || !h.previousProvenance) return out;
20291
+ const [now, before] = await Promise.all([
20292
+ npmSourceRepo(p.name, version),
20293
+ npmSourceRepo(p.name, previous)
20294
+ ]);
20295
+ return vouched(p, version, now, before, out, misses);
20296
+ }
20297
+ if (p.ecosystem === "PyPI") {
20298
+ if (!out.some((f) => SOFT2.has(f.kind)) || !info.file || !h.previousFile) return out;
20299
+ const [now, before] = await Promise.all([
20300
+ pypiSourceRepo(p.name, version, info.file),
20301
+ pypiSourceRepo(p.name, previous, h.previousFile)
20302
+ ]);
20303
+ if (now === "none" && before !== "none" && before !== "unknown") return [...out, drop()];
20304
+ return vouched(p, version, now, before, out, misses);
20305
+ }
20306
+ return out;
20307
+ }
20308
+ function vouched(p, version, now, before, findings, misses) {
20309
+ if (now === "unknown" || before === "unknown") {
20310
+ misses.push(`${label(p, version)}: provenance unreadable, findings kept`);
20311
+ return findings;
20312
+ }
20313
+ if (now === "none" || before === "none" || now !== before) return findings;
20314
+ return findings.filter((f) => !SOFT2.has(f.kind));
20315
+ }
18691
20316
  async function runPackageCheck(toolName, args, cfg, cwd) {
18692
20317
  if (!cfg.enabled) return ALLOW;
18693
20318
  try {
@@ -19006,7 +20631,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19006
20631
  appendHookDebug(toolName, args, meta, hashAuditArgs);
19007
20632
  }
19008
20633
  const isManual = meta?.agent === "Terminal";
19009
- const isObserveMode = config.settings.mode === "observe";
20634
+ const isObserveMode = isRecordOnly(config.settings.mode);
19010
20635
  let explainableLabel = "Local Config";
19011
20636
  let dlpReviewFlagged = false;
19012
20637
  let packageReview = null;
@@ -19064,13 +20689,20 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19064
20689
  explainableLabel = "Shell inspection incomplete";
19065
20690
  policyRuleDescription = "The executable command could not be inspected. Approve this call explicitly.";
19066
20691
  }
19067
- const fileReview = !isObserveMode && config.settings.mode !== "audit" && fileFinding?.decision === "review";
20692
+ const fileReview = !isObserveMode && fileFinding?.decision === "review";
19068
20693
  const evaluateCurrentPolicy = async (skipIgnoredFastPath = false) => {
19069
20694
  const own = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
19070
20695
  fileAccess,
19071
20696
  config,
19072
20697
  skipIgnoredFastPath,
19073
- argumentScan: argScan
20698
+ argumentScan: argScan,
20699
+ // A server a person linked to an app is judged as that app (fw#390).
20700
+ mcpServer: resolveLinkedServer(meta?.mcpServer, {
20701
+ serverKey: meta?.serverKey,
20702
+ managedLinks: config.policy.appLinks,
20703
+ appliedShields: config.policy.appliedShields,
20704
+ workspaceGoverned: config.policySource === "workspace"
20705
+ })
19074
20706
  });
19075
20707
  if (!sessionScript || own.decision === "block") return own;
19076
20708
  const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
@@ -19084,7 +20716,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19084
20716
  if (fileFinding) {
19085
20717
  meta = { ...meta, ruleName: fileFinding.ruleName };
19086
20718
  hashAuditArgs = true;
19087
- const observing = isObserveMode || config.settings.mode === "audit";
20719
+ const observing = isObserveMode;
19088
20720
  const blocked = !observing && (fileFinding.decision === "block" || fileReview && config.settings.panicMode);
19089
20721
  appendLocalAudit(
19090
20722
  toolName,
@@ -19286,7 +20918,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19286
20918
  const last = rec.sources[rec.sources.length - 1];
19287
20919
  const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
19288
20920
  const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
19289
- const read = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
20921
+ const read2 = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
19290
20922
  if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
19291
20923
  const noun = (d) => {
19292
20924
  const k = destinationKind(d);
@@ -19295,14 +20927,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19295
20927
  const parts = held.map(
19296
20928
  (h) => h.dest ? `sends to ${destinationValue(h.dest)}, ${h.why === "appeared in what was read" ? `${noun(h.dest)} that appeared in that ${last ? readWord : "content"}` : `${noun(h.dest)} this session did not know`}` : h.why === "repository" ? h.repo.endsWith("/*") ? `sends to a repository on ${h.repo.slice(0, -2)} it does not name (a gist, a new repository or an API call)` : `sends to ${h.repo}, a repository that is not one of this project's remotes` : h.why === "unresolved" ? "sends to a destination this session cannot see (built at run time, or a remote of another repository)" : `is ${bare}, a destructive call, with no known destination`
19297
20929
  );
19298
- const home = os19.homedir();
20930
+ const home = os20.homedir();
19299
20931
  const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
19300
20932
  for (const w of writeHolds) {
19301
20933
  parts.push(
19302
20934
  w.why === "outside-project" ? `writes outside the project, to ${short(w.path)}` : w.why === "agent-instruction" ? `writes to ${short(w.path)}, a file that runs code or instructs the agent` : `writes content carrying ${destinationValue(w.destination ?? "")}, ${noun(w.destination ?? "host:x")} that appeared in that ${readWord}`
19303
20935
  );
19304
20936
  }
19305
- exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
20937
+ exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
19306
20938
  exposureFromEid = last?.eid;
19307
20939
  if (exposureFromEid) exposureMeta = { exposureFromEid };
19308
20940
  exposureKeys = [
@@ -19566,15 +21198,6 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19566
21198
  }
19567
21199
  return { approved: true, checkedBy: "audit" };
19568
21200
  }
19569
- if (config.settings.mode === "audit") {
19570
- if (judge) {
19571
- const policyResult = await evaluateCurrentPolicy();
19572
- if (policyResult.decision === "review") {
19573
- appendLocalAudit(toolName, args, "allow", "audit-mode", ownRowMeta(true), hashAuditArgs);
19574
- }
19575
- }
19576
- return { approved: true, checkedBy: "audit" };
19577
- }
19578
21201
  let appPermReview = null;
19579
21202
  let appPermReviewTool = null;
19580
21203
  const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!exposureReview || !!packageReview;