@node9/proxy 2.37.0 → 2.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }
@@ -393,7 +488,7 @@ function isLockedCheck(def) {
393
488
  function ssrfCheckId(tier) {
394
489
  return isStrictGatedTier(tier) ? "network.internal-addresses" : "network.metadata";
395
490
  }
396
- function unwrapRuleLabel(s) {
491
+ function ruleNameInLabel(s) {
397
492
  let rest = s;
398
493
  const override = rest.lastIndexOf(OVERRIDE_PREFIX);
399
494
  if (override >= 0) rest = rest.slice(override + OVERRIDE_PREFIX.length).trimStart();
@@ -409,7 +504,7 @@ function checkIdForRule(nameOrLabel) {
409
504
  const s = nameOrLabel.trim();
410
505
  const exact = LEGACY_TO_ID.get(s);
411
506
  if (exact) return exact;
412
- const unwrapped = unwrapRuleLabel(s);
507
+ const unwrapped = ruleNameInLabel(s);
413
508
  if (unwrapped !== void 0) return checkIdForRule(unwrapped);
414
509
  if (s.startsWith("shield:project-jail:")) {
415
510
  return PROJECT_JAIL_BLOCK_READ.test(s) ? "data.credential-files" : "data.credential-files-other";
@@ -497,9 +592,9 @@ function resolveCheck(settings, id) {
497
592
  const on = settings.appliedShields?.includes(def.pack) ?? false;
498
593
  return { id, value: on ? def.defaultValue : "off", source: "default" };
499
594
  }
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;
595
+ const read2 = LEGACY_READERS[id]?.(settings);
596
+ if (read2 === void 0) return { id, value: def.defaultValue, source: "default" };
597
+ const value = CHECK_VERDICT_RANK[read2] < floor ? def.floor : read2;
503
598
  return { id, value, source: value === def.defaultValue ? "default" : "configured" };
504
599
  }
505
600
  function checkValue(settings, id) {
@@ -567,6 +662,102 @@ function scanViews(text) {
567
662
  }
568
663
  return views;
569
664
  }
665
+ function isAgentInstructionPath(absPath, root) {
666
+ const r = root.replace(/\/$/, "");
667
+ if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
668
+ const rel = absPath.slice(r.length + 1);
669
+ if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
670
+ return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
671
+ }
672
+ function isInside(root, p) {
673
+ const r = root.replace(/\/$/, "") || "/";
674
+ return p === r || p.startsWith(r === "/" ? "/" : r + "/");
675
+ }
676
+ function isPasswordHashPath(p) {
677
+ return PASSWORD_HASH_FILES.includes(p);
678
+ }
679
+ function isLegacySystemPath(p) {
680
+ return LEGACY_SYSTEM_PATH_RE.test(p);
681
+ }
682
+ function normalizeFilePath(p, cwd = "", home = "~") {
683
+ if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
684
+ if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
685
+ const rooted = p.startsWith("/");
686
+ const parts = [];
687
+ for (const part of p.split("/")) {
688
+ if (!part || part === ".") continue;
689
+ if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
690
+ else if (part !== ".." || !rooted) parts.push(part);
691
+ }
692
+ return (rooted ? "/" : "") + parts.join("/");
693
+ }
694
+ function inlineLanguageOf(interp) {
695
+ const i = interp.toLowerCase();
696
+ if (/^python[\d.]*$/.test(i)) return "python";
697
+ if (/^(?:node|tsx|ts-node|deno|bun)$/.test(i)) return "node";
698
+ if (i === "perl") return "perl";
699
+ if (i === "ruby" || i === "irb") return "ruby";
700
+ if (i === "php") return "php";
701
+ return null;
702
+ }
703
+ function resolvePythonAliases(code) {
704
+ const aliases = [...code.matchAll(/\bimport\s+([\w.]+)\s+as\s+(\w+)/g)].slice(0, 32);
705
+ let out = code;
706
+ for (const [, mod, alias] of aliases) {
707
+ if (alias === mod) continue;
708
+ out = out.replace(new RegExp(String.raw`(?<![\w.])${alias}\b`, "g"), mod);
709
+ }
710
+ return out;
711
+ }
712
+ function protectedLiteral(code, isProtected) {
713
+ let n = 0;
714
+ for (const m of code.matchAll(STRING_LITERAL)) {
715
+ if (++n > 2048) break;
716
+ const value = m[2].trim();
717
+ if (value && isProtected(value)) return value;
718
+ }
719
+ return null;
720
+ }
721
+ function amplifies(language, code) {
722
+ 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*\)/;
723
+ const m = loop.exec(code);
724
+ if (!m) return null;
725
+ const body = code.slice(m.index);
726
+ 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(
727
+ body
728
+ ) ? m[0] : null;
729
+ }
730
+ function judgeInlineCode(language, code, isProtected) {
731
+ if (code.length > MAX_INLINE_CODE) {
732
+ return { outcome: "unreadable", what: "is too long for node9 to read" };
733
+ }
734
+ const text = language === "python" ? resolvePythonAliases(code) : code;
735
+ for (const [category, re] of TABLES[language]) {
736
+ const m = re.exec(text);
737
+ if (!m) continue;
738
+ const evidence = m[0].trim().replace(/\s+/g, " ").slice(0, 60);
739
+ return {
740
+ outcome: category === "builds-code" ? "unreadable" : "dangerous",
741
+ what: WHAT[category],
742
+ evidence,
743
+ category
744
+ };
745
+ }
746
+ const path28 = protectedLiteral(text, isProtected);
747
+ if (path28) {
748
+ return {
749
+ outcome: "dangerous",
750
+ what: WHAT["reads-protected"],
751
+ evidence: path28.slice(0, 60),
752
+ category: "reads-protected"
753
+ };
754
+ }
755
+ const loop = amplifies(language, text);
756
+ if (loop) {
757
+ return { outcome: "dangerous", what: WHAT.amplifies, evidence: loop, category: "amplifies" };
758
+ }
759
+ return null;
760
+ }
570
761
  function isCatHeredocOrLit(part) {
571
762
  if (!part) return false;
572
763
  const t = syntax.NodeType(part);
@@ -635,6 +826,55 @@ function cachedNormalize(command, compute) {
635
826
  function normalizeCommandForPolicy(command) {
636
827
  return commandReadingsImpl(command).posix;
637
828
  }
829
+ function fileAccessView(command) {
830
+ const hit = accessViewCache.get(command);
831
+ if (hit !== void 0) return hit;
832
+ const view = fileAccessViewImpl(command);
833
+ if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
834
+ const oldest = accessViewCache.keys().next().value;
835
+ if (oldest !== void 0) accessViewCache.delete(oldest);
836
+ }
837
+ accessViewCache.set(command, view);
838
+ return view;
839
+ }
840
+ function fileAccessViewImpl(command) {
841
+ const text = command.replace(INTERPRETER_LINE, "");
842
+ const f = parseShared(text);
843
+ if (f === PARSE_FAIL) return text;
844
+ const toCharIndex = byteOffsetToCharIndex(text);
845
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
846
+ const edits = [];
847
+ try {
848
+ syntax.Walk(f, (node) => {
849
+ if (!node) return false;
850
+ const n = node;
851
+ if (syntax.NodeType(n) !== "CallExpr") return true;
852
+ const args = n.Args || [];
853
+ const words = args.map((a) => resolveWordLiteral(a));
854
+ const head = unwrapCommandHead(words);
855
+ for (const i of /* @__PURE__ */ new Set([0, head])) {
856
+ const word = words[i];
857
+ if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
858
+ continue;
859
+ }
860
+ if (!PROGRAM_DIR.test(word)) continue;
861
+ const name = word.split(/[\\/]/).pop() ?? "";
862
+ if (!name) continue;
863
+ const s = at(args[i].Pos().Offset());
864
+ const e = at(args[i].End().Offset());
865
+ if (s < 0 || e < 0) continue;
866
+ edits.push([s, e, name]);
867
+ }
868
+ return true;
869
+ });
870
+ } catch {
871
+ return text;
872
+ }
873
+ edits.sort((a, b) => b[0] - a[0]);
874
+ let out = text;
875
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
876
+ return out;
877
+ }
638
878
  function commandReadings(command) {
639
879
  const r = commandReadingsImpl(command);
640
880
  const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
@@ -1090,53 +1330,119 @@ function unwrapCommandHead(words, stopAtFind = false, stopAtDynamic = false) {
1090
1330
  }
1091
1331
  return i;
1092
1332
  }
1093
- function inlineExecStmt(stmt, pipeFed) {
1333
+ function heredocText(word) {
1334
+ const parts = word?.Parts ?? [];
1335
+ let out = "";
1336
+ for (const p of parts) {
1337
+ if (syntax.NodeType(p) !== "Lit") return null;
1338
+ out += p.Value ?? "";
1339
+ }
1340
+ return out;
1341
+ }
1342
+ function stdinCodeOf(redirs) {
1343
+ const heredoc = /* @__PURE__ */ new Set([deriveRedirOp("cat <<X\nX"), deriveRedirOp("cat <<-X\nX")]);
1344
+ const herestring = deriveRedirOp("cat <<< x");
1345
+ for (const r of redirs) {
1346
+ if (!r || !redirStdinOps().has(r.Op)) continue;
1347
+ if (heredoc.has(r.Op)) {
1348
+ const text = heredocText(r.Hdoc);
1349
+ return text === null ? { code: null, source: "variable" } : { code: text };
1350
+ }
1351
+ if (r.Op === herestring) {
1352
+ const text = resolveWordLiteral(r.Word);
1353
+ return text === null ? { code: null, source: "variable" } : { code: text };
1354
+ }
1355
+ return { code: null, source: "file" };
1356
+ }
1357
+ return { code: null, source: "file" };
1358
+ }
1359
+ function attachedCode(interp, flag) {
1360
+ const eq = flag.indexOf("=");
1361
+ if (flag.startsWith("--")) return eq >= 0 ? flag.slice(eq + 1) : null;
1362
+ if (/^(pwsh|powershell)/.test(interp)) return null;
1363
+ const letters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1364
+ const body = flag.slice(1);
1365
+ const at = [...body].findIndex((ch) => letters.includes(ch.toLowerCase()));
1366
+ if (at < 0) return null;
1367
+ let rest = body.slice(at + 1);
1368
+ if (letters === "ep" && /^[ep]*$/i.test(rest)) rest = "";
1369
+ return rest === "" ? null : rest;
1370
+ }
1371
+ function inlineExecCall(stmt, pipeFed) {
1094
1372
  const cmd = stmt?.Cmd;
1095
- if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1373
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return null;
1096
1374
  const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1097
- if (words.length === 0) return false;
1375
+ if (words.length === 0) return null;
1098
1376
  const headIdx = unwrapCommandHead(words);
1099
1377
  const rawHead = words[headIdx];
1100
- if (rawHead == null) return false;
1378
+ if (rawHead == null) return null;
1101
1379
  const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1102
- if (!INLINE_INTERPRETER.test(interp)) return false;
1380
+ if (!INLINE_INTERPRETER.test(interp)) return null;
1103
1381
  let args = words.slice(headIdx + 1);
1104
- if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1382
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") {
1383
+ const code = args[1] ?? null;
1384
+ return { interp, code: [code], source: code === null ? "variable" : void 0 };
1385
+ }
1105
1386
  if (INTERP_LEADING_TARGET.has(interp)) {
1106
1387
  const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1107
1388
  args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1108
1389
  }
1390
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1391
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1392
+ const fromStdin = () => {
1393
+ if (stdinFed) {
1394
+ const { code, source } = stdinCodeOf(redirs);
1395
+ return { interp, code: [code], source };
1396
+ }
1397
+ return { interp, code: [null], source: "pipe" };
1398
+ };
1109
1399
  let positionals2 = 0;
1110
1400
  let selectedProgram = false;
1401
+ const codes = [];
1111
1402
  for (let i = 0; i < args.length; i++) {
1112
1403
  const a = args[i];
1113
1404
  if (a == null) {
1405
+ if (codes.length > 0) break;
1114
1406
  positionals2++;
1115
1407
  selectedProgram = true;
1116
1408
  continue;
1117
1409
  }
1118
1410
  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);
1411
+ if (/^(bash|sh|zsh|dash|ksh)$/.test(interp)) {
1412
+ const payload = args[i + 1];
1413
+ if (payload == null) return { interp, code: [null], source: "variable" };
1414
+ return shellPayloadNeedsReview(payload) ? { interp, code: [null], source: "payload" } : null;
1415
+ }
1416
+ const attached = attachedCode(interp, a);
1417
+ if (attached !== null) {
1418
+ codes.push(attached);
1419
+ } else {
1420
+ codes.push(args[i + 1] ?? null);
1421
+ i++;
1422
+ }
1423
+ continue;
1424
+ }
1425
+ if (codes.length > 0) {
1426
+ if (a.startsWith("-")) continue;
1427
+ break;
1123
1428
  }
1124
1429
  if (a === "-m") {
1125
1430
  selectedProgram = true;
1126
1431
  continue;
1127
1432
  }
1128
- if (a === "-" && !selectedProgram) return true;
1433
+ if (a === "-" && !selectedProgram) return fromStdin();
1129
1434
  if (!a.startsWith("-")) {
1130
1435
  positionals2++;
1131
1436
  selectedProgram = true;
1132
1437
  }
1133
1438
  }
1134
- const redirs = stmt.Redirs || cmd.Redirs || [];
1135
- const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1439
+ if (codes.length > 0) {
1440
+ return { interp, code: codes, source: codes.includes(null) ? "variable" : void 0 };
1441
+ }
1136
1442
  if (positionals2 === 0 && (stdinFed || pipeFed)) {
1137
- if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1443
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return fromStdin();
1138
1444
  }
1139
- return false;
1445
+ return null;
1140
1446
  }
1141
1447
  function shellPayloadNeedsReview(payload) {
1142
1448
  if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
@@ -1169,7 +1475,7 @@ function shellPayloadNeedsReview(payload) {
1169
1475
  payload
1170
1476
  ))
1171
1477
  review = true;
1172
- if (!review && detectInlineExec(payload)) review = true;
1478
+ if (!review && inlineExecReview(payload) !== null) review = true;
1173
1479
  } catch {
1174
1480
  review = true;
1175
1481
  } finally {
@@ -1177,35 +1483,77 @@ function shellPayloadNeedsReview(payload) {
1177
1483
  }
1178
1484
  return review;
1179
1485
  }
1180
- function detectInlineExec(command) {
1486
+ function inlineExecCalls(command) {
1181
1487
  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;
1488
+ if (f === PARSE_FAIL) return PARSE_FAIL;
1489
+ const calls = [];
1188
1490
  try {
1189
1491
  syntax.Walk(f, (node) => {
1190
- if (!node || found) return false;
1492
+ if (!node || calls.length >= MAX_INLINE_CALLS) return false;
1191
1493
  const n = node;
1192
1494
  const t = syntax.NodeType(n);
1193
1495
  if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1194
- if (inlineExecStmt(n.Y, true)) {
1195
- found = true;
1196
- return false;
1197
- }
1496
+ const call = inlineExecCall(n.Y, true);
1497
+ if (call) calls.push(call);
1198
1498
  }
1199
- if (t === "Stmt" && inlineExecStmt(n, false)) {
1200
- found = true;
1201
- return false;
1499
+ if (t === "Stmt") {
1500
+ const call = inlineExecCall(n, false);
1501
+ if (call) calls.push(call);
1202
1502
  }
1203
1503
  return true;
1204
1504
  });
1205
1505
  } catch {
1206
- return found;
1207
1506
  }
1208
- return found;
1507
+ return calls;
1508
+ }
1509
+ function detectInlineExec(command) {
1510
+ const calls = inlineExecCalls(command);
1511
+ if (calls === PARSE_FAIL) return INLINE_EXEC_FALLBACK.test(command);
1512
+ return calls.length > 0;
1513
+ }
1514
+ function isProtectedCodePath(p) {
1515
+ if (matchSensitivePath(p) !== null) return true;
1516
+ return SYSTEM_AUTH_FILES.includes(p) || SYSTEM_AUTH_DIRS.some((d) => p === d || p.startsWith(d + "/"));
1517
+ }
1518
+ function inlineExecDescription(r) {
1519
+ 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.";
1520
+ return `${lead} Review the full script below before allowing it to execute.`;
1521
+ }
1522
+ function inlineExecReason(r) {
1523
+ const who = r.interp ? `This inline ${r.interp} code` : "This inline code";
1524
+ return r.evidence ? `${who} ${r.what} (${r.evidence})` : `${who} ${r.what}`;
1525
+ }
1526
+ function inlineExecReview(command) {
1527
+ const calls = inlineExecCalls(command);
1528
+ if (calls === PARSE_FAIL) {
1529
+ return INLINE_EXEC_FALLBACK.test(command) ? { outcome: "unreadable", interp: "", what: "could not be parsed" } : null;
1530
+ }
1531
+ let unreadable = calls.length >= MAX_INLINE_CALLS ? { outcome: "unreadable", interp: "", what: "runs more inline programs than node9 reads" } : null;
1532
+ for (const call of calls) {
1533
+ const language = inlineLanguageOf(call.interp);
1534
+ for (const code of call.code) {
1535
+ let finding = null;
1536
+ if (code === null) {
1537
+ finding = {
1538
+ outcome: "unreadable",
1539
+ interp: call.interp,
1540
+ what: UNREADABLE_SOURCE[call.source ?? "variable"]
1541
+ };
1542
+ } else if (language === null) {
1543
+ finding = {
1544
+ outcome: "unreadable",
1545
+ interp: call.interp,
1546
+ what: `is ${call.interp} code, which node9 does not read`
1547
+ };
1548
+ } else {
1549
+ const judged = judgeInlineCode(language, code, isProtectedCodePath);
1550
+ if (judged) finding = { ...judged, interp: call.interp };
1551
+ }
1552
+ if (finding?.outcome === "dangerous") return finding;
1553
+ if (finding && !unreadable) unreadable = finding;
1554
+ }
1555
+ }
1556
+ return unreadable;
1209
1557
  }
1210
1558
  function analyzeChmod777(command) {
1211
1559
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
@@ -2454,40 +2802,6 @@ function analyzeShellCommand(command) {
2454
2802
  }
2455
2803
  return { actions, paths, allTokens };
2456
2804
  }
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
2805
  function matchesPattern(text, patterns) {
2492
2806
  const p = Array.isArray(patterns) ? patterns : [patterns];
2493
2807
  if (p.length === 0) return false;
@@ -2498,9 +2812,21 @@ function matchesPattern(text, patterns) {
2498
2812
  const withoutDotSlash = text.replace(/^\.\//, "");
2499
2813
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2500
2814
  }
2501
- function getNestedValue(obj, path27) {
2815
+ function mcpServerFromToolName(toolName) {
2816
+ const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
2817
+ return match?.[1];
2818
+ }
2819
+ function ruleServerMatches(rule, serverLabel) {
2820
+ const label2 = typeof serverLabel === "string" ? serverLabel.trim().replace(/[\\/]/g, "-") || void 0 : void 0;
2821
+ if (rule.server) {
2822
+ if (!label2 || !matchesPattern(label2, rule.server)) return false;
2823
+ }
2824
+ if (rule.notServer && label2 && matchesPattern(label2, rule.notServer)) return false;
2825
+ return true;
2826
+ }
2827
+ function getNestedValue(obj, path28) {
2502
2828
  if (!obj || typeof obj !== "object") return null;
2503
- const segments2 = path27.split(".");
2829
+ const segments2 = path28.split(".");
2504
2830
  for (const seg of segments2) {
2505
2831
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2506
2832
  }
@@ -2520,23 +2846,26 @@ function evaluateSmartConditions(args, rule) {
2520
2846
  return val;
2521
2847
  };
2522
2848
  const readingsCache = /* @__PURE__ */ new Map();
2523
- const resolveFieldReadings = (field) => {
2524
- const cached = readingsCache.get(field);
2849
+ const resolveFieldReadings = (field, reading) => {
2850
+ const access = reading === "access" && field === "command";
2851
+ const key = access ? `${field}\0access` : field;
2852
+ const cached = readingsCache.get(key);
2525
2853
  if (cached) return cached;
2526
2854
  const primary = resolveField(field);
2527
2855
  if (primary === null) {
2528
- readingsCache.set(field, []);
2856
+ readingsCache.set(key, []);
2529
2857
  return [];
2530
2858
  }
2531
2859
  let out = [primary];
2532
2860
  if (field === "command") {
2533
2861
  const raw = getNestedValue(args, field);
2534
2862
  if (typeof raw === "string") {
2535
- const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
2536
- out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2863
+ const text = access ? fileAccessView(raw) : raw;
2864
+ const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
2865
+ out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2537
2866
  }
2538
2867
  }
2539
- readingsCache.set(field, out);
2868
+ readingsCache.set(key, out);
2540
2869
  return out;
2541
2870
  };
2542
2871
  const results = rule.conditions.map((cond) => {
@@ -2554,14 +2883,14 @@ function evaluateSmartConditions(args, rule) {
2554
2883
  if (val === null || !cond.value) return false;
2555
2884
  const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2556
2885
  if (!reM) return false;
2557
- return resolveFieldReadings(cond.field).some((v) => reM.test(v));
2886
+ return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
2558
2887
  }
2559
2888
  case "notMatches": {
2560
2889
  if (!cond.value) return false;
2561
2890
  if (val === null) return true;
2562
2891
  const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2563
2892
  if (!reN) return false;
2564
- return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
2893
+ return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
2565
2894
  }
2566
2895
  case "matchesGlob":
2567
2896
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
@@ -3403,35 +3732,6 @@ function collectShellFileAccesses(command, context = {}) {
3403
3732
  }
3404
3733
  return out;
3405
3734
  }
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
3735
  function incompleteFileVerdict(operation = "write") {
3436
3736
  return {
3437
3737
  decision: "review",
@@ -3483,8 +3783,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
3483
3783
  }
3484
3784
  return out;
3485
3785
  }
3486
- function within(path27, root) {
3487
- return path27 === root || path27.startsWith(root === "/" ? "/" : root + "/");
3786
+ function within(path28, root) {
3787
+ return path28 === root || path28.startsWith(root === "/" ? "/" : root + "/");
3488
3788
  }
3489
3789
  function hits(access, paths, directories, context) {
3490
3790
  const home = context.homeDirs?.[0] ?? "~";
@@ -4292,14 +4592,14 @@ function sensitivePathMatch(originalPath) {
4292
4592
  }
4293
4593
  function assertBuiltinPatternsAreSafe() {
4294
4594
  for (const p of DLP_PATTERNS) {
4295
- if (!safeRegex3(p.regex.source)) {
4595
+ if (!safeRegex2(p.regex.source)) {
4296
4596
  throw new Error(
4297
4597
  `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
4298
4598
  );
4299
4599
  }
4300
4600
  }
4301
4601
  for (const re of SENSITIVE_PATH_PATTERNS) {
4302
- if (!safeRegex3(re.source)) {
4602
+ if (!safeRegex2(re.source)) {
4303
4603
  throw new Error(
4304
4604
  `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
4305
4605
  );
@@ -5423,9 +5723,9 @@ function bareToolName(toolName) {
5423
5723
  const parts = toolName.split("__");
5424
5724
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
5425
5725
  }
5426
- function valuesAt(args, path27) {
5726
+ function valuesAt(args, path28) {
5427
5727
  let cursors = [args];
5428
- for (const rawSegment of path27.split(".")) {
5728
+ for (const rawSegment of path28.split(".")) {
5429
5729
  const isArray = rawSegment.endsWith("[]");
5430
5730
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
5431
5731
  const next = [];
@@ -5456,8 +5756,8 @@ function extractToolDestinations(toolName, args) {
5456
5756
  try {
5457
5757
  const out = [];
5458
5758
  const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
5459
- for (const path27 of paths) {
5460
- for (const value of valuesAt(args, path27)) {
5759
+ for (const path28 of paths) {
5760
+ for (const value of valuesAt(args, path28)) {
5461
5761
  const host2 = hostOf(value);
5462
5762
  if (host2) out.push({ host: host2, binary: toolName, raw: value });
5463
5763
  }
@@ -5483,8 +5783,8 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5483
5783
  try {
5484
5784
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
5485
5785
  if (!paths) return null;
5486
- for (const path27 of paths) {
5487
- for (const value of valuesAt(args, path27)) {
5786
+ for (const path28 of paths) {
5787
+ for (const value of valuesAt(args, path28)) {
5488
5788
  const host2 = hostOf(value);
5489
5789
  if (!host2) continue;
5490
5790
  const m = classifySsrf(host2);
@@ -5493,7 +5793,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5493
5793
  const decision = ssrfFloorDecision(m, written, opts);
5494
5794
  if (decision === null) continue;
5495
5795
  const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
5496
- return { ...m, argPath: path27, host: host2, reason, decision };
5796
+ return { ...m, argPath: path28, host: host2, reason, decision };
5497
5797
  }
5498
5798
  }
5499
5799
  return null;
@@ -5873,11 +6173,15 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5873
6173
  };
5874
6174
  const ruleCheck = (rule) => {
5875
6175
  if (rule.verdict === "allow") return void 0;
5876
- if (!rule.builtin || rule.pinned) return void 0;
6176
+ if (!rule.builtin) return void 0;
5877
6177
  const id = checkIdForRule(rule.name);
5878
6178
  if (!id) return void 0;
5879
6179
  const def = getCheck(id);
5880
6180
  if (!def) return void 0;
6181
+ if (rule.pinned) {
6182
+ const explicit = config.policy.checks?.[id];
6183
+ return def.pack && config.policy.checkSources?.[id] === "workspace" && isVerdict(explicit) ? { id, value: explicit } : void 0;
6184
+ }
5881
6185
  if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
5882
6186
  if (def.pack) {
5883
6187
  const explicit = config.policy.checks?.[id];
@@ -5885,8 +6189,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5885
6189
  }
5886
6190
  return { id, value: cv(id) };
5887
6191
  };
6192
+ const serverLabel = context.mcpServer ?? mcpServerFromToolName(toolName);
5888
6193
  const matches = config.policy.smartRules.filter((rule) => {
5889
6194
  if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
6195
+ if (!ruleServerMatches(rule, serverLabel)) return false;
5890
6196
  if (astSuppressed(rule)) return false;
5891
6197
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
5892
6198
  if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
@@ -5963,11 +6269,13 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5963
6269
  if (v?.logged) noteLogged(v);
5964
6270
  else if (v) candidates.push(v);
5965
6271
  };
5966
- if (detectInlineExec(shellCommand)) {
6272
+ const inline = config.settings.mode === "strict" ? detectInlineExec(shellCommand) ? void 0 : null : inlineExecReview(shellCommand);
6273
+ if (inline !== null) {
5967
6274
  consider("commands.inline-exec", {
5968
6275
  decision: "review",
5969
6276
  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.",
6277
+ ...inline ? { reason: inlineExecReason(inline) } : {},
6278
+ ruleDescription: inlineExecDescription(inline),
5971
6279
  tier: 3,
5972
6280
  checkId: "commands.inline-exec"
5973
6281
  });
@@ -7057,10 +7365,13 @@ function matchCanaryArgs(args, values) {
7057
7365
  return null;
7058
7366
  }
7059
7367
  }
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;
7368
+ 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, 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
7369
  var init_dist = __esm({
7062
7370
  "packages/policy-engine/dist/index.mjs"() {
7063
7371
  "use strict";
7372
+ MAX_REGEX_LENGTH = 256;
7373
+ REGEX_CACHE_MAX = 500;
7374
+ regexCache = /* @__PURE__ */ new Map();
7064
7375
  aws_default = {
7065
7376
  name: "aws",
7066
7377
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -7362,81 +7673,10 @@ var init_dist = __esm({
7362
7673
  ],
7363
7674
  dangerousWords: ["wipefs"]
7364
7675
  };
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
- github_default = {
7437
- name: "github",
7438
- description: "Protects GitHub repositories from destructive AI operations",
7439
- aliases: ["git"],
7676
+ github_default = {
7677
+ name: "github",
7678
+ description: "Protects GitHub repositories from destructive AI operations",
7679
+ aliases: ["git"],
7440
7680
  smartRules: [
7441
7681
  {
7442
7682
  name: "shield:github:review-delete-branch-remote",
@@ -7459,56 +7699,6 @@ var init_dist = __esm({
7459
7699
  ],
7460
7700
  dangerousWords: []
7461
7701
  };
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
7702
  k8s_default = {
7513
7703
  name: "k8s",
7514
7704
  description: "Protects Kubernetes clusters from destructive AI operations",
@@ -7761,7 +7951,7 @@ var init_dist = __esm({
7761
7951
  {
7762
7952
  field: "command",
7763
7953
  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|$|[;&|>)<])",
7954
+ 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
7955
  flags: "i"
7766
7956
  }
7767
7957
  ],
@@ -7775,7 +7965,7 @@ var init_dist = __esm({
7775
7965
  {
7776
7966
  field: "command",
7777
7967
  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)",
7968
+ 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
7969
  flags: "i"
7780
7970
  }
7781
7971
  ],
@@ -7825,18 +8015,218 @@ var init_dist = __esm({
7825
8015
  reason: "Reading .env files is blocked by project-jail shield"
7826
8016
  },
7827
8017
  {
7828
- name: "shield:project-jail:review-read-credentials-any-tool",
7829
- tool: "*",
7830
- conditions: [
7831
- {
7832
- field: "file_path",
7833
- op: "matches",
7834
- value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
7835
- flags: "i"
7836
- }
7837
- ],
8018
+ name: "shield:project-jail:review-read-credentials-any-tool",
8019
+ tool: "*",
8020
+ conditions: [
8021
+ {
8022
+ field: "file_path",
8023
+ op: "matches",
8024
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
8025
+ flags: "i"
8026
+ }
8027
+ ],
8028
+ verdict: "review",
8029
+ reason: "Reading credential files requires approval (project-jail shield)"
8030
+ }
8031
+ ],
8032
+ dangerousWords: []
8033
+ };
8034
+ redis_default = {
8035
+ name: "redis",
8036
+ description: "Protects Redis instances from destructive AI operations",
8037
+ aliases: [],
8038
+ _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.",
8039
+ smartRules: [
8040
+ {
8041
+ name: "shield:redis:block-flushall",
8042
+ tool: "*",
8043
+ conditions: [
8044
+ {
8045
+ field: "command",
8046
+ op: "matches",
8047
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
8048
+ flags: "i"
8049
+ }
8050
+ ],
8051
+ verdict: "block",
8052
+ reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
8053
+ },
8054
+ {
8055
+ name: "shield:redis:block-flushdb",
8056
+ tool: "*",
8057
+ conditions: [
8058
+ {
8059
+ field: "command",
8060
+ op: "matches",
8061
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
8062
+ flags: "i"
8063
+ }
8064
+ ],
8065
+ verdict: "block",
8066
+ reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
8067
+ },
8068
+ {
8069
+ name: "shield:redis:block-config-resetstat",
8070
+ tool: "*",
8071
+ conditions: [
8072
+ {
8073
+ field: "command",
8074
+ op: "matches",
8075
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
8076
+ flags: "i"
8077
+ }
8078
+ ],
8079
+ verdict: "block",
8080
+ reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
8081
+ },
8082
+ {
8083
+ name: "shield:redis:review-config-set",
8084
+ tool: "*",
8085
+ conditions: [
8086
+ {
8087
+ field: "command",
8088
+ op: "matches",
8089
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
8090
+ flags: "i"
8091
+ }
8092
+ ],
8093
+ verdict: "review",
8094
+ reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
8095
+ },
8096
+ {
8097
+ name: "shield:redis:review-del-wildcard",
8098
+ tool: "*",
8099
+ conditions: [
8100
+ {
8101
+ field: "command",
8102
+ op: "matches",
8103
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
8104
+ flags: "i"
8105
+ }
8106
+ ],
8107
+ verdict: "review",
8108
+ reason: "Wildcard key deletion requires human approval (Redis shield)"
8109
+ }
8110
+ ],
8111
+ dangerousWords: ["FLUSHALL", "FLUSHDB"]
8112
+ };
8113
+ finance_default = {
8114
+ name: "finance",
8115
+ 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.",
8116
+ aliases: ["payments", "money"],
8117
+ smartRules: [
8118
+ {
8119
+ name: "shield:finance:review-refund",
8120
+ tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
8121
+ conditions: [],
8122
+ verdict: "review",
8123
+ reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
8124
+ },
8125
+ {
8126
+ name: "shield:finance:review-payout",
8127
+ tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
8128
+ conditions: [],
8129
+ verdict: "review",
8130
+ reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
8131
+ },
8132
+ {
8133
+ name: "shield:finance:review-transfer",
8134
+ tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
8135
+ conditions: [],
8136
+ verdict: "review",
8137
+ reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
8138
+ },
8139
+ {
8140
+ name: "shield:finance:review-charge",
8141
+ 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}",
8142
+ conditions: [],
8143
+ verdict: "review",
8144
+ reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
8145
+ },
8146
+ {
8147
+ name: "shield:finance:review-payment-link",
8148
+ tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
8149
+ conditions: [],
8150
+ verdict: "review",
8151
+ reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
8152
+ },
8153
+ {
8154
+ name: "shield:finance:review-subscription",
8155
+ tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
8156
+ conditions: [],
8157
+ verdict: "review",
8158
+ reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
8159
+ },
8160
+ {
8161
+ name: "shield:finance:review-dispute",
8162
+ tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
8163
+ conditions: [],
8164
+ verdict: "review",
8165
+ reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
8166
+ },
8167
+ {
8168
+ name: "shield:finance:review-card",
8169
+ tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
8170
+ conditions: [],
8171
+ verdict: "review",
8172
+ reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
8173
+ },
8174
+ {
8175
+ name: "shield:finance:review-withdraw",
8176
+ tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
8177
+ conditions: [],
8178
+ verdict: "review",
8179
+ reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
8180
+ }
8181
+ ],
8182
+ dangerousWords: []
8183
+ };
8184
+ identity_default = {
8185
+ name: "identity",
8186
+ 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.",
8187
+ aliases: ["access", "iam"],
8188
+ smartRules: [
8189
+ {
8190
+ name: "shield:identity:review-password",
8191
+ tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
8192
+ conditions: [],
8193
+ verdict: "review",
8194
+ reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
8195
+ },
8196
+ {
8197
+ name: "shield:identity:review-key",
8198
+ 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}",
8199
+ conditions: [],
8200
+ verdict: "review",
8201
+ reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
8202
+ },
8203
+ {
8204
+ name: "shield:identity:review-access",
8205
+ 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}",
8206
+ conditions: [],
8207
+ verdict: "review",
8208
+ reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
8209
+ },
8210
+ {
8211
+ name: "shield:identity:review-membership",
8212
+ 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}",
8213
+ conditions: [],
8214
+ verdict: "review",
8215
+ reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
8216
+ },
8217
+ {
8218
+ name: "shield:identity:review-user",
8219
+ 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}",
8220
+ conditions: [],
7838
8221
  verdict: "review",
7839
- reason: "Reading credential files requires approval (project-jail shield)"
8222
+ reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
8223
+ },
8224
+ {
8225
+ name: "shield:identity:review-mfa",
8226
+ tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
8227
+ conditions: [],
8228
+ verdict: "review",
8229
+ reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
7840
8230
  }
7841
8231
  ],
7842
8232
  dangerousWords: []
@@ -8010,103 +8400,510 @@ var init_dist = __esm({
8010
8400
  ],
8011
8401
  dangerousWords: []
8012
8402
  };
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: [
8019
- {
8020
- name: "shield:redis:block-flushall",
8021
- tool: "*",
8022
- conditions: [
8023
- {
8024
- field: "command",
8025
- op: "matches",
8026
- value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
8027
- flags: "i"
8028
- }
8029
- ],
8030
- verdict: "block",
8031
- reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
8032
- },
8033
- {
8034
- name: "shield:redis:block-flushdb",
8035
- tool: "*",
8036
- conditions: [
8037
- {
8038
- field: "command",
8039
- op: "matches",
8040
- value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
8041
- flags: "i"
8042
- }
8043
- ],
8044
- verdict: "block",
8045
- reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
8046
- },
8047
- {
8048
- name: "shield:redis:block-config-resetstat",
8049
- tool: "*",
8050
- conditions: [
8051
- {
8052
- field: "command",
8053
- op: "matches",
8054
- value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
8055
- flags: "i"
8056
- }
8057
- ],
8058
- verdict: "block",
8059
- reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
8060
- },
8061
- {
8062
- name: "shield:redis:review-config-set",
8063
- tool: "*",
8064
- conditions: [
8065
- {
8066
- field: "command",
8067
- op: "matches",
8068
- value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
8069
- flags: "i"
8070
- }
8071
- ],
8072
- verdict: "review",
8073
- reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
8074
- },
8075
- {
8076
- name: "shield:redis:review-del-wildcard",
8077
- tool: "*",
8078
- conditions: [
8079
- {
8080
- field: "command",
8081
- op: "matches",
8082
- value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
8083
- flags: "i"
8084
- }
8085
- ],
8086
- verdict: "review",
8087
- reason: "Wildcard key deletion requires human approval (Redis shield)"
8088
- }
8089
- ],
8090
- dangerousWords: ["FLUSHALL", "FLUSHDB"]
8403
+ ACTION_TEMPLATES = {
8404
+ finance: finance_default.smartRules,
8405
+ identity: identity_default.smartRules,
8406
+ publishing: publishing_default.smartRules
8407
+ };
8408
+ ACTION_CHECKS = {
8409
+ finance: { checkId: "actions.money", slug: "money", label: "Money check" },
8410
+ identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
8411
+ publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
8412
+ };
8413
+ GAP = "[^;&|]{0,200}?";
8414
+ prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
8415
+ httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
8416
+ httpWrite = (methods) => ({
8417
+ field: "command",
8418
+ op: "matches",
8419
+ 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)`
8420
+ });
8421
+ HTTP_WRITE = httpWrite("POST|DELETE");
8422
+ HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
8423
+ NOT_STRIPE_TEST = {
8424
+ field: "command",
8425
+ op: "notMatches",
8426
+ value: "\\b[sr]k_test_"
8091
8427
  };
8428
+ postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
8429
+ STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
8430
+ APP_SHIELD_SPECS = [
8431
+ {
8432
+ name: "stripe",
8433
+ label: "Stripe",
8434
+ 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.",
8435
+ template: "finance",
8436
+ server: "*stripe*",
8437
+ doors: [
8438
+ {
8439
+ name: "shield:stripe:review-cli",
8440
+ tool: "bash",
8441
+ conditions: [
8442
+ {
8443
+ field: "command",
8444
+ op: "matches",
8445
+ value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
8446
+ flags: "i"
8447
+ },
8448
+ {
8449
+ field: "command",
8450
+ op: "matches",
8451
+ // The stripe CLI works in test mode unless told otherwise.
8452
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8453
+ }
8454
+ ],
8455
+ conditionMode: "all",
8456
+ verdict: "review",
8457
+ reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
8458
+ },
8459
+ {
8460
+ name: "shield:stripe:review-cli-request",
8461
+ tool: "bash",
8462
+ conditions: [
8463
+ {
8464
+ field: "command",
8465
+ op: "matches",
8466
+ value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
8467
+ flags: "i"
8468
+ },
8469
+ {
8470
+ field: "command",
8471
+ op: "matches",
8472
+ // The stripe CLI works in test mode unless told otherwise.
8473
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8474
+ }
8475
+ ],
8476
+ conditionMode: "all",
8477
+ verdict: "review",
8478
+ reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
8479
+ },
8480
+ {
8481
+ name: "shield:stripe:review-api",
8482
+ tool: "bash",
8483
+ conditions: [
8484
+ {
8485
+ field: "command",
8486
+ op: "matches",
8487
+ value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
8488
+ flags: "i"
8489
+ },
8490
+ HTTP_WRITE,
8491
+ NOT_STRIPE_TEST
8492
+ ],
8493
+ conditionMode: "all",
8494
+ verdict: "review",
8495
+ reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
8496
+ },
8497
+ {
8498
+ name: "shield:stripe:review-code",
8499
+ tool: "bash",
8500
+ conditions: [
8501
+ {
8502
+ field: "command",
8503
+ op: "matches",
8504
+ // Python: stripe.Refund.create(...)
8505
+ value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
8506
+ },
8507
+ NOT_STRIPE_TEST
8508
+ ],
8509
+ conditionMode: "all",
8510
+ verdict: "review",
8511
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8512
+ },
8513
+ {
8514
+ name: "shield:stripe:review-code-node",
8515
+ tool: "bash",
8516
+ conditions: [
8517
+ {
8518
+ field: "command",
8519
+ op: "matches",
8520
+ // Node: stripe.refunds.create(...)
8521
+ value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
8522
+ },
8523
+ NOT_STRIPE_TEST
8524
+ ],
8525
+ conditionMode: "all",
8526
+ verdict: "review",
8527
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8528
+ },
8529
+ {
8530
+ name: "shield:stripe:review-code-http",
8531
+ tool: "bash",
8532
+ conditions: [
8533
+ {
8534
+ field: "command",
8535
+ op: "matches",
8536
+ // requests.post("https://api.stripe.com/v1/refunds", ...)
8537
+ value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
8538
+ },
8539
+ NOT_STRIPE_TEST
8540
+ ],
8541
+ conditionMode: "all",
8542
+ verdict: "review",
8543
+ reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
8544
+ }
8545
+ ]
8546
+ },
8547
+ {
8548
+ name: "paypal",
8549
+ label: "PayPal",
8550
+ 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.",
8551
+ template: "finance",
8552
+ server: "*paypal*",
8553
+ doors: [
8554
+ {
8555
+ name: "shield:paypal:review-api",
8556
+ tool: "bash",
8557
+ conditions: [
8558
+ {
8559
+ field: "command",
8560
+ op: "matches",
8561
+ // The live hosts only: api.paypal.com and api-m.paypal.com. The
8562
+ // sandbox (api-m.sandbox.paypal.com) moves no money.
8563
+ value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
8564
+ flags: "i"
8565
+ },
8566
+ {
8567
+ field: "command",
8568
+ op: "matches",
8569
+ // The endpoints that move money.
8570
+ value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
8571
+ flags: "i"
8572
+ },
8573
+ HTTP_WRITE
8574
+ ],
8575
+ conditionMode: "all",
8576
+ verdict: "review",
8577
+ reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
8578
+ },
8579
+ {
8580
+ name: "shield:paypal:review-code",
8581
+ tool: "bash",
8582
+ conditions: [
8583
+ {
8584
+ field: "command",
8585
+ op: "matches",
8586
+ // The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
8587
+ // Python paypalrestsdk.
8588
+ value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
8589
+ }
8590
+ ],
8591
+ verdict: "review",
8592
+ reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
8593
+ },
8594
+ {
8595
+ name: "shield:paypal:review-code-http",
8596
+ tool: "bash",
8597
+ conditions: [
8598
+ {
8599
+ field: "command",
8600
+ op: "matches",
8601
+ // The endpoints that move money; the token call every script
8602
+ // starts with (/v1/oauth2/token) is not one.
8603
+ value: postTo(
8604
+ "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)"
8605
+ )
8606
+ }
8607
+ ],
8608
+ verdict: "review",
8609
+ reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
8610
+ }
8611
+ ]
8612
+ },
8613
+ {
8614
+ name: "square",
8615
+ label: "Square",
8616
+ 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.",
8617
+ template: "finance",
8618
+ // Square's servers, never Squarespace (a website builder).
8619
+ server: "{*square,*square[-_]*,*squareup*}",
8620
+ mcpTools: [
8621
+ {
8622
+ // Square's official MCP server has one tool for every API call; the
8623
+ // service and method name what it does.
8624
+ name: "shield:square:review-mcp-request",
8625
+ tool: "{,mcp__*__}make_api_request",
8626
+ conditions: [
8627
+ {
8628
+ field: "service",
8629
+ op: "matches",
8630
+ value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
8631
+ flags: "i"
8632
+ },
8633
+ {
8634
+ field: "method",
8635
+ op: "notMatches",
8636
+ value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
8637
+ flags: "i"
8638
+ }
8639
+ ],
8640
+ conditionMode: "all",
8641
+ verdict: "review",
8642
+ reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
8643
+ }
8644
+ ],
8645
+ doors: [
8646
+ {
8647
+ name: "shield:square:review-api",
8648
+ tool: "bash",
8649
+ conditions: [
8650
+ {
8651
+ field: "command",
8652
+ op: "matches",
8653
+ // The live host only; the sandbox is connect.squareupsandbox.com.
8654
+ value: httpTo(
8655
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8656
+ ),
8657
+ flags: "i"
8658
+ },
8659
+ HTTP_WRITE_ANY
8660
+ ],
8661
+ conditionMode: "all",
8662
+ verdict: "review",
8663
+ reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
8664
+ },
8665
+ {
8666
+ name: "shield:square:review-code",
8667
+ tool: "bash",
8668
+ conditions: [
8669
+ {
8670
+ field: "command",
8671
+ op: "matches",
8672
+ // The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
8673
+ value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
8674
+ },
8675
+ {
8676
+ field: "command",
8677
+ op: "matches",
8678
+ 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*\\("
8679
+ }
8680
+ ],
8681
+ conditionMode: "all",
8682
+ verdict: "review",
8683
+ reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
8684
+ },
8685
+ {
8686
+ name: "shield:square:review-code-http",
8687
+ tool: "bash",
8688
+ conditions: [
8689
+ {
8690
+ field: "command",
8691
+ op: "matches",
8692
+ value: postTo(
8693
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8694
+ )
8695
+ }
8696
+ ],
8697
+ verdict: "review",
8698
+ reason: "This code posts to the Square API. node9 asks before money moves (Square)."
8699
+ }
8700
+ ]
8701
+ },
8702
+ {
8703
+ name: "plaid",
8704
+ label: "Plaid",
8705
+ 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.",
8706
+ template: "finance",
8707
+ server: "*plaid*",
8708
+ doors: [
8709
+ {
8710
+ name: "shield:plaid:review-api",
8711
+ tool: "bash",
8712
+ conditions: [
8713
+ {
8714
+ field: "command",
8715
+ op: "matches",
8716
+ // Every Plaid endpoint is a POST, so the path is what tells a
8717
+ // write; the live host only, never sandbox.plaid.com. A script
8718
+ // that posts to the URL meets this door too.
8719
+ value: httpTo(
8720
+ "production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
8721
+ ),
8722
+ flags: "i"
8723
+ }
8724
+ ],
8725
+ verdict: "review",
8726
+ reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
8727
+ },
8728
+ {
8729
+ name: "shield:plaid:review-code",
8730
+ tool: "bash",
8731
+ conditions: [
8732
+ { field: "command", op: "matches", value: "\\bplaid", flags: "i" },
8733
+ {
8734
+ field: "command",
8735
+ op: "matches",
8736
+ value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
8737
+ }
8738
+ ],
8739
+ conditionMode: "all",
8740
+ verdict: "review",
8741
+ reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
8742
+ }
8743
+ ]
8744
+ },
8745
+ {
8746
+ name: "okta",
8747
+ label: "Okta",
8748
+ 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.",
8749
+ template: "identity",
8750
+ server: "*okta*",
8751
+ doors: [
8752
+ {
8753
+ name: "shield:okta:review-api",
8754
+ tool: "bash",
8755
+ conditions: [
8756
+ {
8757
+ field: "command",
8758
+ op: "matches",
8759
+ // The management API only: sign-in (/api/v1/authn, /sessions,
8760
+ // /oauth2) is not under these paths.
8761
+ value: httpTo(
8762
+ "[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
8763
+ ),
8764
+ flags: "i"
8765
+ },
8766
+ HTTP_WRITE_ANY
8767
+ ],
8768
+ conditionMode: "all",
8769
+ verdict: "review",
8770
+ reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
8771
+ },
8772
+ {
8773
+ name: "shield:okta:review-code",
8774
+ tool: "bash",
8775
+ conditions: [
8776
+ {
8777
+ field: "command",
8778
+ op: "matches",
8779
+ // The Okta SDK in the script: a method named createUser alone is
8780
+ // in half the applications ever written.
8781
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8782
+ },
8783
+ {
8784
+ field: "command",
8785
+ op: "matches",
8786
+ value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
8787
+ }
8788
+ ],
8789
+ conditionMode: "all",
8790
+ verdict: "review",
8791
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8792
+ },
8793
+ {
8794
+ name: "shield:okta:review-code-py",
8795
+ tool: "bash",
8796
+ conditions: [
8797
+ {
8798
+ field: "command",
8799
+ op: "matches",
8800
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8801
+ },
8802
+ {
8803
+ field: "command",
8804
+ op: "matches",
8805
+ 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*\\("
8806
+ }
8807
+ ],
8808
+ conditionMode: "all",
8809
+ verdict: "review",
8810
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8811
+ }
8812
+ ]
8813
+ },
8814
+ {
8815
+ name: "auth0",
8816
+ label: "Auth0",
8817
+ 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.",
8818
+ template: "identity",
8819
+ server: "*auth0*",
8820
+ mcpTools: [
8821
+ {
8822
+ // Auth0's official MCP server: applications (their secrets and
8823
+ // callbacks), APIs and their scopes, and Actions (code that runs at
8824
+ // every sign-in).
8825
+ name: "shield:auth0:review-tenant-config",
8826
+ 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}",
8827
+ conditions: [],
8828
+ verdict: "review",
8829
+ reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
8830
+ }
8831
+ ],
8832
+ doors: [
8833
+ {
8834
+ name: "shield:auth0:review-cli",
8835
+ tool: "bash",
8836
+ conditions: [
8837
+ {
8838
+ field: "command",
8839
+ op: "matches",
8840
+ 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-])",
8841
+ flags: "i"
8842
+ }
8843
+ ],
8844
+ verdict: "review",
8845
+ reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
8846
+ },
8847
+ {
8848
+ name: "shield:auth0:review-api",
8849
+ tool: "bash",
8850
+ conditions: [
8851
+ {
8852
+ field: "command",
8853
+ op: "matches",
8854
+ // The Management API only; /oauth/token is signing in.
8855
+ value: httpTo(
8856
+ "\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
8857
+ ),
8858
+ flags: "i"
8859
+ },
8860
+ HTTP_WRITE_ANY
8861
+ ],
8862
+ conditionMode: "all",
8863
+ verdict: "review",
8864
+ reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
8865
+ },
8866
+ {
8867
+ name: "shield:auth0:review-code",
8868
+ tool: "bash",
8869
+ conditions: [
8870
+ {
8871
+ field: "command",
8872
+ op: "matches",
8873
+ // The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
8874
+ value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
8875
+ },
8876
+ {
8877
+ field: "command",
8878
+ op: "matches",
8879
+ 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*\\("
8880
+ }
8881
+ ],
8882
+ conditionMode: "all",
8883
+ verdict: "review",
8884
+ reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
8885
+ }
8886
+ ]
8887
+ }
8888
+ ];
8889
+ APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
8890
+ APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
8891
+ APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
8892
+ GENERAL_ACTION_RULES = buildGeneralRules();
8092
8893
  BUILTIN_SHIELDS = {
8093
8894
  [aws_default.name]: aws_default,
8094
8895
  [bash_safe_default.name]: bash_safe_default,
8095
8896
  [docker_default.name]: docker_default,
8096
8897
  [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
8898
  [github_default.name]: github_default,
8103
- [identity_default.name]: identity_default,
8104
8899
  [k8s_default.name]: k8s_default,
8105
8900
  [mongodb_default.name]: mongodb_default,
8106
8901
  [postgres_default.name]: postgres_default,
8107
8902
  [project_jail_default.name]: project_jail_default,
8108
- [publishing_default.name]: publishing_default,
8109
- [redis_default.name]: redis_default
8903
+ [redis_default.name]: redis_default,
8904
+ // App shields generated from the action templates (node9Firewall#390): one
8905
+ // per app, every door (MCP server, CLI, API, library), off by default.
8906
+ ...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
8110
8907
  };
8111
8908
  assertBuiltinShieldRegexesAreSafe();
8112
8909
  SSRF_MAX_HOST = 253;
@@ -8162,9 +8959,9 @@ var init_dist = __esm({
8162
8959
  },
8163
8960
  // ── Commands ──────────────────────────────────────────────────────────────
8164
8961
  "commands.inline-exec": {
8165
- plain: "The agent runs a small program written inside the command itself.",
8962
+ 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
8963
  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."
8964
+ 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
8965
  },
8169
8966
  "commands.eval-dynamic": {
8170
8967
  plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
@@ -8196,6 +8993,21 @@ var init_dist = __esm({
8196
8993
  example: "chmod 777 deploy.sh",
8197
8994
  advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
8198
8995
  },
8996
+ "actions.money": {
8997
+ 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.",
8998
+ example: "refund_order on a server called acme-billing",
8999
+ 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."
9000
+ },
9001
+ "actions.access": {
9002
+ plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
9003
+ example: "invite_user or grant_role on a company admin server",
9004
+ advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
9005
+ },
9006
+ "actions.publish": {
9007
+ 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.",
9008
+ example: "create_release or deploy_to_production on a company MCP server",
9009
+ advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
9010
+ },
8199
9011
  "commands.sudo": {
8200
9012
  plain: "The agent runs a command as the administrator (root).",
8201
9013
  example: "sudo apt-get install -y nginx",
@@ -8356,7 +9168,7 @@ var init_dist = __esm({
8356
9168
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
8357
9169
  },
8358
9170
  "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.",
9171
+ 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
9172
  example: "npm install of a package flagged as malware",
8361
9173
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
8362
9174
  }
@@ -8408,7 +9220,7 @@ var init_dist = __esm({
8408
9220
  id: "commands.inline-exec",
8409
9221
  group: "commands",
8410
9222
  title: "Inline code execution",
8411
- catches: "node -e, python -c, bash -c, a heredoc into an interpreter",
9223
+ 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
9224
  values: ALL,
8413
9225
  defaultValue: "review",
8414
9226
  legacy: ["Node9 Standard (Inline Execution)"]
@@ -8765,6 +9577,38 @@ var init_dist = __esm({
8765
9577
  values: ALL,
8766
9578
  defaultValue: "block",
8767
9579
  legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
9580
+ },
9581
+ // ── Actions in apps (node9Firewall#390) ──────────────────────────────────
9582
+ // The safety net under the app shields: an action that moves money, changes
9583
+ // access or publishes, on an MCP server no app shield claims (an internal
9584
+ // billing server, a vendor with no shield, Zapier). Review by default; the
9585
+ // rules come from the action templates (shields/apps.ts).
9586
+ {
9587
+ id: "actions.money",
9588
+ group: "actions",
9589
+ title: "Money moves",
9590
+ catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
9591
+ values: ALL,
9592
+ defaultValue: "review",
9593
+ legacy: generalActionRuleNames("finance")
9594
+ },
9595
+ {
9596
+ id: "actions.access",
9597
+ group: "actions",
9598
+ title: "Accounts and access change",
9599
+ catches: "a password, key, role, permission or user changed through an app with no protection of its own",
9600
+ values: ALL,
9601
+ defaultValue: "review",
9602
+ legacy: generalActionRuleNames("identity")
9603
+ },
9604
+ {
9605
+ id: "actions.publish",
9606
+ group: "actions",
9607
+ title: "Something is published",
9608
+ catches: "a release, package, deploy or public post through an app with no protection of its own",
9609
+ values: ALL,
9610
+ defaultValue: "review",
9611
+ legacy: generalActionRuleNames("publishing")
8768
9612
  }
8769
9613
  ];
8770
9614
  SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
@@ -8887,12 +9731,223 @@ var init_dist = __esm({
8887
9731
  \u03A5: "Y",
8888
9732
  \u03A7: "X"
8889
9733
  };
8890
- HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
8891
- MAX_BLOBS = 32;
8892
- MIN_DECODED = 16;
8893
- BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
8894
- HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
8895
- HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
9734
+ HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
9735
+ MAX_BLOBS = 32;
9736
+ MIN_DECODED = 16;
9737
+ BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
9738
+ HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
9739
+ HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
9740
+ SYSTEM_AUTH_FILES = [
9741
+ "/etc/passwd",
9742
+ "/etc/shadow",
9743
+ "/etc/group",
9744
+ "/etc/gshadow",
9745
+ "/etc/sudoers"
9746
+ ];
9747
+ SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9748
+ PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9749
+ STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9750
+ SYSTEM_DIRS = [
9751
+ "/usr",
9752
+ "/etc",
9753
+ "/boot",
9754
+ "/bin",
9755
+ "/sbin",
9756
+ "/lib",
9757
+ "/lib64",
9758
+ "/var/log"
9759
+ ];
9760
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9761
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9762
+ AGENT_INSTRUCTION_FILES = [
9763
+ ".gitlab-ci.yml",
9764
+ "CLAUDE.md",
9765
+ "AGENTS.md",
9766
+ ".cursorrules",
9767
+ ".mcp.json",
9768
+ ".vscode/settings.json",
9769
+ "package.json"
9770
+ ];
9771
+ AGENT_INSTRUCTION_DIRS = [
9772
+ ".github/workflows",
9773
+ ".claude",
9774
+ ".cursor",
9775
+ ".git/hooks",
9776
+ ".husky"
9777
+ ];
9778
+ LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9779
+ MAX_INLINE_CODE = 64 * 1024;
9780
+ WHAT = {
9781
+ deletes: "deletes files",
9782
+ "writes-outside": "writes a file outside the project",
9783
+ sends: "sends data over the network",
9784
+ listens: "opens a listening socket or a shell",
9785
+ "runs-commands": "runs other commands or processes",
9786
+ "reads-protected": "reads a protected file",
9787
+ "builds-code": "runs code it builds at run time",
9788
+ permissions: "changes permissions or ownership",
9789
+ monitors: "captures the screen, the clipboard or the keyboard",
9790
+ amplifies: "writes or sends in an endless loop"
9791
+ };
9792
+ OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
9793
+ pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
9794
+ either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
9795
+ PYTHON = [
9796
+ [
9797
+ "deletes",
9798
+ /\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/
9799
+ ],
9800
+ [
9801
+ "writes-outside",
9802
+ new RegExp(
9803
+ 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}`
9804
+ )
9805
+ ],
9806
+ [
9807
+ "sends",
9808
+ either(
9809
+ pyImport(
9810
+ String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
9811
+ ),
9812
+ /\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
9813
+ )
9814
+ ],
9815
+ [
9816
+ "listens",
9817
+ either(
9818
+ pyImport(String.raw`pty|socketserver|http\.server`),
9819
+ /\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
9820
+ )
9821
+ ],
9822
+ [
9823
+ "runs-commands",
9824
+ either(
9825
+ pyImport("subprocess|pexpect|commands|psutil"),
9826
+ /\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+\*/
9827
+ )
9828
+ ],
9829
+ [
9830
+ "builds-code",
9831
+ /(?<![\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*\[/
9832
+ ],
9833
+ [
9834
+ // Spyware shapes: a screenshot, the clipboard, a key logger.
9835
+ "monitors",
9836
+ either(
9837
+ pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
9838
+ /\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
9839
+ )
9840
+ ],
9841
+ [
9842
+ "permissions",
9843
+ /\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
9844
+ ]
9845
+ ];
9846
+ NODE = [
9847
+ [
9848
+ "deletes",
9849
+ /\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
9850
+ ],
9851
+ [
9852
+ "writes-outside",
9853
+ new RegExp(
9854
+ 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}`
9855
+ )
9856
+ ],
9857
+ [
9858
+ "sends",
9859
+ /\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*\(/
9860
+ ],
9861
+ ["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
9862
+ [
9863
+ "runs-commands",
9864
+ /\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
9865
+ ],
9866
+ [
9867
+ "builds-code",
9868
+ /(?<![\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)]/
9869
+ ],
9870
+ [
9871
+ "permissions",
9872
+ /\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
9873
+ ]
9874
+ ];
9875
+ PERL = [
9876
+ ["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
9877
+ [
9878
+ "writes-outside",
9879
+ new RegExp(
9880
+ String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
9881
+ )
9882
+ ],
9883
+ [
9884
+ "sends",
9885
+ /\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
9886
+ ],
9887
+ [
9888
+ "runs-commands",
9889
+ /\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
9890
+ ],
9891
+ ["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
9892
+ ["permissions", /\b(?:chmod|chown)\b/]
9893
+ ];
9894
+ RUBY = [
9895
+ [
9896
+ "deletes",
9897
+ /\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
9898
+ ],
9899
+ [
9900
+ "writes-outside",
9901
+ new RegExp(
9902
+ 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}`
9903
+ )
9904
+ ],
9905
+ [
9906
+ "sends",
9907
+ /\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)['"]/
9908
+ ],
9909
+ ["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
9910
+ [
9911
+ "runs-commands",
9912
+ /(?<![\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/
9913
+ ],
9914
+ [
9915
+ "builds-code",
9916
+ /(?<![\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/
9917
+ ],
9918
+ ["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
9919
+ ];
9920
+ PHP = [
9921
+ ["deletes", /\b(?:unlink|rmdir)\s*\(/],
9922
+ [
9923
+ "writes-outside",
9924
+ new RegExp(
9925
+ String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
9926
+ )
9927
+ ],
9928
+ [
9929
+ "sends",
9930
+ /\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):/
9931
+ ],
9932
+ ["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
9933
+ [
9934
+ "runs-commands",
9935
+ /\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
9936
+ ],
9937
+ [
9938
+ "builds-code",
9939
+ /\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
9940
+ ],
9941
+ ["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
9942
+ ];
9943
+ TABLES = {
9944
+ python: PYTHON,
9945
+ node: NODE,
9946
+ perl: PERL,
9947
+ ruby: RUBY,
9948
+ php: PHP
9949
+ };
9950
+ STRING_LITERAL = /(['"`])((?:\\.|(?!\1)[^\\\n]){1,4096})\1/g;
8896
9951
  ({ syntax } = mvdanSh);
8897
9952
  sharedParser = syntax.NewParser();
8898
9953
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -8930,6 +9985,10 @@ var init_dist = __esm({
8930
9985
  AST_CACHE_MAX = 5e3;
8931
9986
  astCache = /* @__PURE__ */ new Map();
8932
9987
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
9988
+ INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
9989
+ PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
9990
+ ACCESS_VIEW_CACHE_MAX = 1e3;
9991
+ accessViewCache = /* @__PURE__ */ new Map();
8933
9992
  MAX_READING_PAYLOADS = 8;
8934
9993
  defaultExpansionOps = null;
8935
9994
  VARIABLE_READERS = /* @__PURE__ */ new Set([
@@ -9639,6 +10698,14 @@ var init_dist = __esm({
9639
10698
  INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
9640
10699
  MAX_PAYLOAD_NESTING = 8;
9641
10700
  payloadNesting = 0;
10701
+ MAX_INLINE_CALLS = 16;
10702
+ 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;
10703
+ UNREADABLE_SOURCE = {
10704
+ pipe: "reads its code from another program through a pipe",
10705
+ file: "reads its code from a file on stdin",
10706
+ variable: "runs code the shell fills in at run time",
10707
+ payload: "runs a shell payload that evaluates, sources or runs further code"
10708
+ };
9642
10709
  SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
9643
10710
  SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
9644
10711
  MAX_SCRIPT_OPERANDS = 8;
@@ -9838,9 +10905,6 @@ var init_dist = __esm({
9838
10905
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
9839
10906
  NONE = { kind: "none" };
9840
10907
  UNKNOWN = { kind: "unknown" };
9841
- MAX_REGEX_LENGTH = 256;
9842
- REGEX_CACHE_MAX = 500;
9843
- regexCache = /* @__PURE__ */ new Map();
9844
10908
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
9845
10909
  BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
9846
10910
  "bash",
@@ -9955,45 +11019,6 @@ var init_dist = __esm({
9955
11019
  (s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
9956
11020
  )
9957
11021
  );
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
11022
  exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
9998
11023
  RANK = { allow: 0, review: 1, block: 2 };
9999
11024
  BIP39_ENGLISH = `abandon
@@ -13486,7 +14511,11 @@ zoo`.split("\n");
13486
14511
  "commands.disk-destroy",
13487
14512
  "commands.dangerous-word",
13488
14513
  "commands.node9-disarm",
13489
- "data.pipe-chain"
14514
+ "data.pipe-chain",
14515
+ // The general action checks (node9Firewall#390): smart rules the map governs.
14516
+ "actions.money",
14517
+ "actions.access",
14518
+ "actions.publish"
13490
14519
  ]);
13491
14520
  KNOB_GOVERNED = {
13492
14521
  "data.secrets": ["off", "block"],
@@ -14194,7 +15223,8 @@ var SmartConditionSchema = z.object({
14194
15223
  }
14195
15224
  ),
14196
15225
  value: z.string().optional(),
14197
- flags: z.string().optional()
15226
+ flags: z.string().optional(),
15227
+ reading: z.enum(["access"]).optional()
14198
15228
  }).refine(
14199
15229
  (c) => {
14200
15230
  if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
@@ -14205,6 +15235,9 @@ var SmartConditionSchema = z.object({
14205
15235
  var SmartRuleSchema = z.object({
14206
15236
  name: z.string().optional(),
14207
15237
  tool: z.string().min(1, "Smart rule tool must not be empty"),
15238
+ // Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
15239
+ server: z.string().min(1).optional(),
15240
+ notServer: z.string().min(1).optional(),
14208
15241
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
14209
15242
  conditionMode: z.enum(["all", "any"]).optional(),
14210
15243
  verdict: z.enum(["allow", "review", "block"], {
@@ -14354,8 +15387,8 @@ var ConfigFileSchema = z.object({
14354
15387
  }).strict();
14355
15388
  function formatIssues(issues) {
14356
15389
  const lines = issues.map((issue) => {
14357
- const path27 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
14358
- return ` \u2022 ${path27}: ${issue.message}`;
15390
+ const path28 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
15391
+ return ` \u2022 ${path28}: ${issue.message}`;
14359
15392
  });
14360
15393
  return `Invalid config:
14361
15394
  ${lines.join("\n")}`;
@@ -14368,9 +15401,9 @@ function prunePaths(root, paths) {
14368
15401
  const yi = y[y.length - 1];
14369
15402
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
14370
15403
  });
14371
- for (const path27 of ordered) {
15404
+ for (const path28 of ordered) {
14372
15405
  let cur = root;
14373
- for (const key of path27.slice(0, -1)) {
15406
+ for (const key of path28.slice(0, -1)) {
14374
15407
  if (cur === null || typeof cur !== "object") {
14375
15408
  cur = void 0;
14376
15409
  break;
@@ -14378,7 +15411,7 @@ function prunePaths(root, paths) {
14378
15411
  cur = cur[key];
14379
15412
  }
14380
15413
  if (cur === null || typeof cur !== "object") continue;
14381
- const last = path27[path27.length - 1];
15414
+ const last = path28[path28.length - 1];
14382
15415
  if (Array.isArray(cur)) {
14383
15416
  const i = Number(last);
14384
15417
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -14414,7 +15447,7 @@ function sanitizeConfig(raw) {
14414
15447
  return issue.keys.map((k) => [...at, k]);
14415
15448
  }
14416
15449
  return [at.slice(0, -level || void 0)];
14417
- }).filter((path27) => path27.length > 0);
15450
+ }).filter((path28) => path28.length > 0);
14418
15451
  if (paths.length === 0 || !prunePaths(working, paths)) break;
14419
15452
  }
14420
15453
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -14440,6 +15473,85 @@ import fs2 from "fs";
14440
15473
  import path2 from "path";
14441
15474
  import os2 from "os";
14442
15475
  init_dist();
15476
+
15477
+ // src/shields/build.ts
15478
+ init_dist();
15479
+ function escapeRegex(s) {
15480
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15481
+ }
15482
+ function slug(s) {
15483
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
15484
+ }
15485
+ var B = "[\\s/\\\\]";
15486
+ var SEP = "[/\\\\]";
15487
+ function pathToRegexFragment(rawPath) {
15488
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
15489
+ const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
15490
+ if (segments2.length === 0) return "";
15491
+ return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
15492
+ }
15493
+ function pathMatchesFragment(candidate, rawPath) {
15494
+ const value = pathToRegexFragment(rawPath);
15495
+ if (!value || !candidate) return false;
15496
+ const re = getCompiledRegex(value);
15497
+ if (!re) return false;
15498
+ return re.test(candidate);
15499
+ }
15500
+ function pathRules(rawPath, verdict, reason) {
15501
+ const value = pathToRegexFragment(rawPath);
15502
+ if (!value) return [];
15503
+ const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
15504
+ const s = slug(rawPath);
15505
+ return [
15506
+ // `reading: 'access'`: a script's interpreter line and the program a
15507
+ // command runs are not reads of the jailed path (node9Firewall#385).
15508
+ {
15509
+ name: `${verdict}-path-${s}-bash`,
15510
+ tool: "bash",
15511
+ conditions: [{ field: "command", op: "matches", value, reading: "access" }],
15512
+ verdict,
15513
+ reason: why
15514
+ },
15515
+ // Keep the historical `-anytool` name for the file_path rule: the
15516
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
15517
+ {
15518
+ name: `${verdict}-path-${s}-anytool`,
15519
+ tool: "*",
15520
+ conditions: [{ field: "file_path", op: "matches", value }],
15521
+ verdict,
15522
+ reason: why
15523
+ },
15524
+ {
15525
+ name: `${verdict}-path-${s}-anytool-path`,
15526
+ tool: "*",
15527
+ conditions: [{ field: "path", op: "matches", value }],
15528
+ verdict,
15529
+ reason: why
15530
+ },
15531
+ {
15532
+ name: `${verdict}-path-${s}-anytool-pattern`,
15533
+ tool: "*",
15534
+ conditions: [{ field: "pattern", op: "matches", value }],
15535
+ verdict,
15536
+ reason: why
15537
+ }
15538
+ ];
15539
+ }
15540
+ var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
15541
+ function withAccessReading(def) {
15542
+ let changed = false;
15543
+ const smartRules = def.smartRules.map((r) => {
15544
+ const [c, ...rest] = r.conditions ?? [];
15545
+ 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) {
15546
+ return r;
15547
+ }
15548
+ changed = true;
15549
+ return { ...r, conditions: [{ ...c, reading: "access" }] };
15550
+ });
15551
+ return changed ? { ...def, smartRules } : def;
15552
+ }
15553
+
15554
+ // src/shields.ts
14443
15555
  init_dist();
14444
15556
  var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
14445
15557
  function loadUserShields() {
@@ -14466,7 +15578,7 @@ function loadUserShields() {
14466
15578
  `);
14467
15579
  continue;
14468
15580
  }
14469
- result[v.ok.name] = v.ok;
15581
+ result[v.ok.name] = withAccessReading(v.ok);
14470
15582
  } catch (err) {
14471
15583
  process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
14472
15584
  `);
@@ -14618,90 +15730,28 @@ var CLASS_B_COMMAND_CHECKS = /* @__PURE__ */ new Set([
14618
15730
  "evalDynamic",
14619
15731
  "pipeChainHigh"
14620
15732
  ]);
14621
- function applyManagedCommandChecks(local, managed, locked) {
14622
- const next = { ...local };
14623
- for (const key of COMMAND_CHECK_KEYS) {
14624
- const m = managed[key];
14625
- if (typeof m !== "string") continue;
14626
- const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
14627
- const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
14628
- locked: locked.includes(lockKey)
14629
- });
14630
- if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
14631
- next[key] = resolved;
14632
- }
14633
- return next;
14634
- }
14635
- function applyManagedApprovers(local, managed) {
14636
- 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
- ];
15733
+ function applyManagedCommandChecks(local, managed, locked) {
15734
+ const next = { ...local };
15735
+ for (const key of COMMAND_CHECK_KEYS) {
15736
+ const m = managed[key];
15737
+ if (typeof m !== "string") continue;
15738
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
15739
+ const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
15740
+ locked: locked.includes(lockKey)
15741
+ });
15742
+ if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
15743
+ next[key] = resolved;
15744
+ }
15745
+ return next;
15746
+ }
15747
+ function applyManagedApprovers(local, managed) {
15748
+ return {
15749
+ ...local,
15750
+ native: typeof managed.native === "boolean" ? managed.native : local.native,
15751
+ browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
15752
+ cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
15753
+ terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
15754
+ };
14705
15755
  }
14706
15756
 
14707
15757
  // src/auth/trusted-hosts.ts
@@ -14762,6 +15812,7 @@ function isTrustedHost(host2) {
14762
15812
 
14763
15813
  // src/config/index.ts
14764
15814
  init_dist();
15815
+ init_dist();
14765
15816
 
14766
15817
  // src/config/v2.ts
14767
15818
  init_dist();
@@ -15074,7 +16125,9 @@ var DEFAULT_CONFIG = {
15074
16125
  op: "matches",
15075
16126
  // Single spaces on purpose: a nested `\\s+` inside the alternation
15076
16127
  // 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",
16128
+ // `apps`: turning an app off, allowing one of its actions, or
16129
+ // linking a server to an app (the general checks leave it).
16130
+ 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
16131
  flags: "i"
15079
16132
  }
15080
16133
  ],
@@ -15160,7 +16213,12 @@ var DEFAULT_CONFIG = {
15160
16213
  verdict: "block",
15161
16214
  reason: "Piping remote script into a shell is a supply-chain attack vector",
15162
16215
  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
- }
16216
+ },
16217
+ // ── Actions in apps (node9Firewall#390) ───────────────────────────────
16218
+ // actions.money / actions.access / actions.publish: the action templates
16219
+ // on every MCP server no app shield claims, plus the shell publishing
16220
+ // rules. Review by default; the check value governs them.
16221
+ ...GENERAL_ACTION_RULES
15164
16222
  ],
15165
16223
  dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
15166
16224
  egress: {
@@ -15197,6 +16255,7 @@ var DEFAULT_CONFIG = {
15197
16255
  trustedHosts: [],
15198
16256
  trustedHostsManaged: false,
15199
16257
  appPermissions: {},
16258
+ appLinks: {},
15200
16259
  managedJailPaths: []
15201
16260
  },
15202
16261
  environments: {}
@@ -15431,6 +16490,7 @@ function getConfig(cwd) {
15431
16490
  trustedHosts: [],
15432
16491
  trustedHostsManaged: false,
15433
16492
  appPermissions: {},
16493
+ appLinks: {},
15434
16494
  managedJailPaths: []
15435
16495
  };
15436
16496
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
@@ -15815,19 +16875,26 @@ function getConfig(cwd) {
15815
16875
  }
15816
16876
  if (Array.isArray(mc.jailPaths)) {
15817
16877
  for (const jp of mc.jailPaths) {
15818
- const path27 = typeof jp?.path === "string" ? jp.path.trim() : "";
15819
- if (!path27) continue;
16878
+ const path28 = typeof jp?.path === "string" ? jp.path.trim() : "";
16879
+ if (!path28) continue;
15820
16880
  const verdict = jp?.verdict === "review" ? "review" : "block";
15821
- for (const r of pathRules(path27, verdict, "org-managed jail")) {
16881
+ for (const r of pathRules(path28, verdict, "org-managed jail")) {
15822
16882
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
15823
16883
  }
15824
- mergedPolicy.managedJailPaths.push({ path: path27, verdict });
16884
+ mergedPolicy.managedJailPaths.push({ path: path28, verdict });
15825
16885
  }
15826
16886
  }
15827
16887
  if (Array.isArray(mc.trustedHosts)) {
15828
16888
  mergedPolicy.trustedHostsManaged = true;
15829
16889
  mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
15830
16890
  }
16891
+ if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
16892
+ const links = {};
16893
+ for (const [key, slug2] of Object.entries(mc.appLinks)) {
16894
+ if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
16895
+ }
16896
+ mergedPolicy.appLinks = links;
16897
+ }
15831
16898
  if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
15832
16899
  const coerced = {};
15833
16900
  for (const [srv, tools] of Object.entries(mc.appPermissions)) {
@@ -16373,7 +17440,8 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
16373
17440
  cwd: prepared.cwd,
16374
17441
  activeEnvironment,
16375
17442
  skipIgnoredFastPath: opts?.skipIgnoredFastPath,
16376
- argumentScan: opts?.argumentScan
17443
+ argumentScan: opts?.argumentScan,
17444
+ mcpServer: opts?.mcpServer
16377
17445
  },
16378
17446
  {
16379
17447
  checkProvenance,
@@ -17041,14 +18109,272 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
17041
18109
 
17042
18110
  // src/auth/orchestrator.ts
17043
18111
  import { randomUUID as randomUUID2 } from "crypto";
17044
- import os19 from "os";
18112
+
18113
+ // src/apps/state.ts
18114
+ import fs12 from "fs";
18115
+ import os10 from "os";
18116
+ import path12 from "path";
18117
+
18118
+ // src/apps/catalog.ts
18119
+ var APP_CATALOG = [
18120
+ {
18121
+ slug: "redis",
18122
+ label: "Redis",
18123
+ shield: "redis",
18124
+ servers: ["redis", "valkey"],
18125
+ commands: ["redis-cli", "valkey-cli"],
18126
+ packages: ["redis", "ioredis", "valkey"],
18127
+ envPrefixes: ["REDIS_"]
18128
+ },
18129
+ {
18130
+ slug: "postgres",
18131
+ label: "PostgreSQL",
18132
+ shield: "postgres",
18133
+ servers: ["postgres", "pgsql"],
18134
+ commands: ["psql", "pg_dump", "pg_restore", "pgcli"],
18135
+ packages: ["pg", "postgres", "psycopg", "psycopg2", "psycopg2-binary", "asyncpg"],
18136
+ envPrefixes: ["PGHOST", "PGDATABASE", "POSTGRES_"]
18137
+ },
18138
+ {
18139
+ slug: "mongodb",
18140
+ label: "MongoDB",
18141
+ shield: "mongodb",
18142
+ servers: ["mongo"],
18143
+ commands: ["mongosh", "mongo", "mongodump", "mongorestore"],
18144
+ packages: ["mongodb", "mongoose", "pymongo", "motor"],
18145
+ envPrefixes: ["MONGO"]
18146
+ },
18147
+ {
18148
+ slug: "github",
18149
+ label: "GitHub",
18150
+ shield: "github",
18151
+ servers: ["github"],
18152
+ commands: ["gh"],
18153
+ packages: ["@octokit/", "octokit", "pygithub"],
18154
+ envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"]
18155
+ },
18156
+ {
18157
+ slug: "aws",
18158
+ label: "AWS",
18159
+ shield: "aws",
18160
+ servers: ["aws"],
18161
+ commands: ["aws", "sam", "cdk"],
18162
+ packages: ["aws-sdk", "@aws-sdk/", "boto3", "aws-cdk-lib"],
18163
+ envPrefixes: ["AWS_ACCESS_KEY_ID", "AWS_PROFILE"]
18164
+ },
18165
+ {
18166
+ slug: "k8s",
18167
+ label: "Kubernetes",
18168
+ shield: "k8s",
18169
+ servers: ["kubernetes", "k8s", "kubectl"],
18170
+ commands: ["kubectl", "helm", "k9s"],
18171
+ packages: ["@kubernetes/client-node", "kubernetes"],
18172
+ envPrefixes: ["KUBECONFIG"]
18173
+ },
18174
+ {
18175
+ slug: "docker",
18176
+ label: "Docker",
18177
+ shield: "docker",
18178
+ servers: ["docker"],
18179
+ commands: ["docker", "docker-compose", "podman"],
18180
+ packages: ["dockerode", "docker"],
18181
+ envPrefixes: ["DOCKER_HOST"]
18182
+ },
18183
+ {
18184
+ slug: "stripe",
18185
+ label: "Stripe",
18186
+ shield: "stripe",
18187
+ servers: ["stripe"],
18188
+ commands: ["stripe"],
18189
+ packages: ["stripe", "@stripe/"],
18190
+ envPrefixes: ["STRIPE_"]
18191
+ },
18192
+ {
18193
+ slug: "paypal",
18194
+ label: "PayPal",
18195
+ shield: "paypal",
18196
+ servers: ["paypal"],
18197
+ commands: [],
18198
+ packages: ["@paypal/", "paypalrestsdk", "paypal-checkout-serversdk", "paypal-server-sdk"],
18199
+ envPrefixes: ["PAYPAL_"]
18200
+ },
18201
+ {
18202
+ slug: "square",
18203
+ label: "Square",
18204
+ shield: "square",
18205
+ servers: ["square"],
18206
+ // Squarespace is a website builder. SQUARE_ already misses SQUARESPACE_.
18207
+ notServers: ["squarespace"],
18208
+ commands: [],
18209
+ packages: ["square", "squareup"],
18210
+ envPrefixes: ["SQUARE_"]
18211
+ },
18212
+ {
18213
+ slug: "plaid",
18214
+ label: "Plaid",
18215
+ shield: "plaid",
18216
+ servers: ["plaid"],
18217
+ commands: [],
18218
+ packages: ["plaid", "plaid-python"],
18219
+ envPrefixes: ["PLAID_"]
18220
+ },
18221
+ {
18222
+ slug: "okta",
18223
+ label: "Okta",
18224
+ shield: "okta",
18225
+ servers: ["okta"],
18226
+ commands: ["okta"],
18227
+ packages: ["@okta/", "okta"],
18228
+ envPrefixes: ["OKTA_"]
18229
+ },
18230
+ {
18231
+ slug: "auth0",
18232
+ label: "Auth0",
18233
+ shield: "auth0",
18234
+ servers: ["auth0"],
18235
+ commands: ["auth0"],
18236
+ packages: ["auth0", "@auth0/"],
18237
+ envPrefixes: ["AUTH0_"]
18238
+ }
18239
+ ];
18240
+ var APP_BY_SLUG = new Map(
18241
+ APP_CATALOG.map((a) => [a.slug, a])
18242
+ );
18243
+
18244
+ // src/apps/state.ts
18245
+ init_audit();
18246
+
18247
+ // src/daemon/mcp-tools.ts
18248
+ import fs11 from "fs";
18249
+ import path11 from "path";
18250
+ import os9 from "os";
18251
+ function getMcpToolsFile() {
18252
+ return path11.join(os9.homedir(), ".node9", "mcp-tools.json");
18253
+ }
18254
+ function readMcpToolsConfig() {
18255
+ try {
18256
+ const file2 = getMcpToolsFile();
18257
+ if (!fs11.existsSync(file2)) return {};
18258
+ const raw = fs11.readFileSync(file2, "utf-8");
18259
+ return JSON.parse(raw);
18260
+ } catch {
18261
+ return {};
18262
+ }
18263
+ }
18264
+
18265
+ // src/apps/state.ts
18266
+ function appsFilePath(home = os10.homedir()) {
18267
+ return path12.join(home, ".node9", "apps.json");
18268
+ }
18269
+ var SIGNAL_KINDS = /* @__PURE__ */ new Set(["mcp-server", "command", "package", "env-name"]);
18270
+ function emptyFile() {
18271
+ return { version: 1, apps: /* @__PURE__ */ Object.create(null), links: /* @__PURE__ */ Object.create(null) };
18272
+ }
18273
+ function ownString(map, key) {
18274
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return void 0;
18275
+ const v = map[key];
18276
+ return typeof v === "string" ? v : void 0;
18277
+ }
18278
+ function readSignals(raw) {
18279
+ if (!Array.isArray(raw)) return [];
18280
+ const out = [];
18281
+ for (const x of raw) {
18282
+ if (!x || typeof x.kind !== "string" || typeof x.value !== "string") continue;
18283
+ if (!SIGNAL_KINDS.has(x.kind)) continue;
18284
+ out.push({
18285
+ kind: x.kind,
18286
+ value: x.value.slice(0, 256),
18287
+ ...typeof x.project === "string" ? { project: x.project } : {}
18288
+ });
18289
+ }
18290
+ return out;
18291
+ }
18292
+ function parseAppsFile(text) {
18293
+ const raw = JSON.parse(text);
18294
+ const file2 = emptyFile();
18295
+ for (const [slug2, r] of Object.entries(raw.apps ?? {})) {
18296
+ if (!APP_BY_SLUG.has(slug2) || !r || r.state !== "on" && r.state !== "off") continue;
18297
+ file2.apps[slug2] = {
18298
+ state: r.state,
18299
+ by: r.by === "user" || r.by === "workspace" ? r.by : "detected",
18300
+ at: typeof r.at === "string" ? r.at : "",
18301
+ signals: readSignals(r.signals)
18302
+ };
18303
+ }
18304
+ for (const [server, slug2] of Object.entries(raw.links ?? {})) {
18305
+ if (typeof slug2 === "string" && APP_BY_SLUG.has(slug2)) file2.links[server.toLowerCase()] = slug2;
18306
+ }
18307
+ return file2;
18308
+ }
18309
+ function readAppsFile(home) {
18310
+ return readAppsFileStrict(home) ?? emptyFile();
18311
+ }
18312
+ function readAppsFileStrict(home) {
18313
+ let text;
18314
+ try {
18315
+ text = fs12.readFileSync(appsFilePath(home), "utf8");
18316
+ } catch (e) {
18317
+ return e.code === "ENOENT" ? emptyFile() : null;
18318
+ }
18319
+ try {
18320
+ return parseAppsFile(text);
18321
+ } catch {
18322
+ return null;
18323
+ }
18324
+ }
18325
+ var linkCache = null;
18326
+ var NO_LINKS = /* @__PURE__ */ Object.create(null);
18327
+ function localLinks() {
18328
+ const file2 = appsFilePath();
18329
+ try {
18330
+ const mtimeMs = fs12.statSync(file2).mtimeMs;
18331
+ if (!linkCache || linkCache.file !== file2 || linkCache.mtimeMs !== mtimeMs) {
18332
+ linkCache = { mtimeMs, file: file2, links: readAppsFile().links };
18333
+ }
18334
+ return linkCache.links;
18335
+ } catch {
18336
+ return NO_LINKS;
18337
+ }
18338
+ }
18339
+ function resolveLinkedServer(label2, opts = {}) {
18340
+ if (typeof label2 !== "string" || !label2) return label2;
18341
+ const applied = new Set(opts.appliedShields ?? []);
18342
+ const live = (slug2) => {
18343
+ const app = slug2 ? APP_BY_SLUG.get(slug2) : void 0;
18344
+ return app && applied.has(app.shield) ? slug2 : void 0;
18345
+ };
18346
+ const lower2 = label2.toLowerCase();
18347
+ if (!opts.workspaceGoverned) {
18348
+ const local = live(ownString(localLinks(), lower2));
18349
+ if (local) return local;
18350
+ }
18351
+ const managed = opts.managedLinks ?? {};
18352
+ if (opts.serverKey) {
18353
+ const byKey = live(ownString(managed, opts.serverKey));
18354
+ if (byKey) return byKey;
18355
+ }
18356
+ const keys = Object.keys(managed);
18357
+ if (keys.length === 0) return label2;
18358
+ const tools = readMcpToolsConfig();
18359
+ for (const key of keys) {
18360
+ const name = key.startsWith("cfg:") ? key.split(":").slice(2).join(":") : Object.prototype.hasOwnProperty.call(tools, key) ? tools[key]?.name : void 0;
18361
+ if (typeof name === "string" && name.toLowerCase() === lower2) {
18362
+ const linked = live(ownString(managed, key));
18363
+ if (linked) return linked;
18364
+ }
18365
+ }
18366
+ return label2;
18367
+ }
18368
+
18369
+ // src/auth/orchestrator.ts
18370
+ import os20 from "os";
17045
18371
 
17046
18372
  // src/ui/native.ts
17047
18373
  import { spawn } from "child_process";
17048
- import path12 from "path";
18374
+ import path14 from "path";
17049
18375
 
17050
18376
  // src/context-sniper.ts
17051
- import path11 from "path";
18377
+ import path13 from "path";
17052
18378
  function smartTruncate(str, maxLen = 500) {
17053
18379
  if (str.length <= maxLen) return str;
17054
18380
  const edge = Math.floor(maxLen / 2) - 3;
@@ -17116,7 +18442,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
17116
18442
  intent = "EDIT";
17117
18443
  if (obj.file_path) {
17118
18444
  editFilePath = String(obj.file_path);
17119
- editFileName = path11.basename(editFilePath);
18445
+ editFileName = path13.basename(editFilePath);
17120
18446
  }
17121
18447
  const result = extractContext(String(obj.new_string), matchedWord);
17122
18448
  contextSnippet = result.snippet;
@@ -17184,7 +18510,7 @@ function formatArgs(args, matchedField, matchedWord) {
17184
18510
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
17185
18511
  const obj = parsed;
17186
18512
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
17187
- const file2 = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
18513
+ const file2 = obj.file_path ? path14.basename(String(obj.file_path)) : "file";
17188
18514
  const oldPreview = smartTruncate(String(obj.old_string), 120);
17189
18515
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
17190
18516
  return {
@@ -17387,24 +18713,24 @@ end run`;
17387
18713
  }
17388
18714
 
17389
18715
  // src/canary/registry.ts
17390
- import fs12 from "fs";
17391
- import os10 from "os";
17392
- import path14 from "path";
18716
+ import fs14 from "fs";
18717
+ import os12 from "os";
18718
+ import path16 from "path";
17393
18719
  init_dist();
17394
18720
 
17395
18721
  // src/shields/jail.ts
17396
- import fs11 from "fs";
17397
- import os9 from "os";
17398
- import path13 from "path";
18722
+ import fs13 from "fs";
18723
+ import os11 from "os";
18724
+ import path15 from "path";
17399
18725
  init_dist();
17400
18726
  var USER_JAIL_SHIELD = "user-jail";
17401
18727
  function jailStorePath() {
17402
- return path13.join(os9.homedir(), ".node9", "jail-paths.json");
18728
+ return path15.join(os11.homedir(), ".node9", "jail-paths.json");
17403
18729
  }
17404
18730
  function readJailPaths() {
17405
18731
  let text;
17406
18732
  try {
17407
- text = fs11.readFileSync(jailStorePath(), "utf8");
18733
+ text = fs13.readFileSync(jailStorePath(), "utf8");
17408
18734
  } catch (err) {
17409
18735
  if (err.code === "ENOENT") return [];
17410
18736
  throw err;
@@ -17433,7 +18759,7 @@ function findJailedPathIn(candidate, paths) {
17433
18759
 
17434
18760
  // src/canary/registry.ts
17435
18761
  function canaryStorePath() {
17436
- return path14.join(os10.homedir(), ".node9", "canaries.json");
18762
+ return path16.join(os12.homedir(), ".node9", "canaries.json");
17437
18763
  }
17438
18764
  function isRecord(x) {
17439
18765
  if (!x || typeof x !== "object") return false;
@@ -17444,7 +18770,7 @@ function loadCanaries(opts) {
17444
18770
  const p = canaryStorePath();
17445
18771
  let raw;
17446
18772
  try {
17447
- raw = fs12.readFileSync(p, "utf-8");
18773
+ raw = fs14.readFileSync(p, "utf-8");
17448
18774
  } catch (e) {
17449
18775
  if (e.code === "ENOENT") return [];
17450
18776
  throw e;
@@ -17471,21 +18797,21 @@ init_dist();
17471
18797
 
17472
18798
  // src/auth/session-script.ts
17473
18799
  init_dist();
17474
- import fs13 from "fs";
17475
- import os11 from "os";
17476
- import path15 from "path";
18800
+ import fs15 from "fs";
18801
+ import os13 from "os";
18802
+ import path17 from "path";
17477
18803
  var MAX_SCRIPT_BYTES = 64 * 1024;
17478
18804
  var MAX_RECORDED_WRITES = 32;
17479
18805
  function readScriptFile(resolved) {
17480
18806
  let fd;
17481
18807
  try {
17482
- fd = fs13.openSync(resolved, fs13.constants.O_RDONLY | (fs13.constants.O_NONBLOCK ?? 0));
17483
- const st = fs13.fstatSync(fd);
18808
+ fd = fs15.openSync(resolved, fs15.constants.O_RDONLY | (fs15.constants.O_NONBLOCK ?? 0));
18809
+ const st = fs15.fstatSync(fd);
17484
18810
  if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
17485
18811
  const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
17486
18812
  let n = 0;
17487
18813
  while (n < buf.length) {
17488
- const got = fs13.readSync(fd, buf, n, buf.length - n, n);
18814
+ const got = fs15.readSync(fd, buf, n, buf.length - n, n);
17489
18815
  if (got === 0) break;
17490
18816
  n += got;
17491
18817
  }
@@ -17496,7 +18822,7 @@ function readScriptFile(resolved) {
17496
18822
  } finally {
17497
18823
  if (fd !== void 0) {
17498
18824
  try {
17499
- fs13.closeSync(fd);
18825
+ fs15.closeSync(fd);
17500
18826
  } catch {
17501
18827
  }
17502
18828
  }
@@ -17504,9 +18830,9 @@ function readScriptFile(resolved) {
17504
18830
  }
17505
18831
  function absolute(p, cwd, home) {
17506
18832
  const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
17507
- return path15.resolve(cwd, spelled);
18833
+ return path17.resolve(cwd, spelled);
17508
18834
  }
17509
- async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os11.homedir()) {
18835
+ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os13.homedir()) {
17510
18836
  try {
17511
18837
  const d = deps ?? { checkWritten, readScript: readScriptFile };
17512
18838
  const shell = resolveShellInput(toolName, args, layers);
@@ -17535,7 +18861,7 @@ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os
17535
18861
  return null;
17536
18862
  }
17537
18863
  }
17538
- function sessionWriteTargets(toolName, args, fileAccess, cwd, home = os11.homedir()) {
18864
+ function sessionWriteTargets(toolName, args, fileAccess, cwd, home = os13.homedir()) {
17539
18865
  const base2 = cwd ?? process.cwd();
17540
18866
  const out = /* @__PURE__ */ new Set();
17541
18867
  const argsObj = args && typeof args === "object" ? args : {};
@@ -17565,26 +18891,26 @@ init_audit();
17565
18891
  init_dist();
17566
18892
 
17567
18893
  // src/exposure/known-hosts.ts
17568
- import fs15 from "fs";
17569
- import path17 from "path";
18894
+ import fs17 from "fs";
18895
+ import path19 from "path";
17570
18896
 
17571
18897
  // src/utils/git-remotes.ts
17572
18898
  init_dist();
17573
- import fs14 from "fs";
17574
- import path16 from "path";
18899
+ import fs16 from "fs";
18900
+ import path18 from "path";
17575
18901
  function readSmallFile(p, max) {
17576
18902
  let fd;
17577
18903
  try {
17578
- fd = fs14.openSync(p, fs14.constants.O_RDONLY | (fs14.constants.O_NOFOLLOW ?? 0));
17579
- const st = fs14.fstatSync(fd);
18904
+ fd = fs16.openSync(p, fs16.constants.O_RDONLY | (fs16.constants.O_NOFOLLOW ?? 0));
18905
+ const st = fs16.fstatSync(fd);
17580
18906
  if (!st.isFile() || st.size > max) return null;
17581
18907
  const buf = Buffer.alloc(st.size);
17582
- const n = fs14.readSync(fd, buf, 0, st.size, 0);
18908
+ const n = fs16.readSync(fd, buf, 0, st.size, 0);
17583
18909
  return buf.subarray(0, n).toString("utf8");
17584
18910
  } catch {
17585
18911
  return null;
17586
18912
  } finally {
17587
- if (fd !== void 0) fs14.closeSync(fd);
18913
+ if (fd !== void 0) fs16.closeSync(fd);
17588
18914
  }
17589
18915
  }
17590
18916
  function gitRemoteHost(url) {
@@ -17603,20 +18929,20 @@ function gitRemoteHost(url) {
17603
18929
  return null;
17604
18930
  }
17605
18931
  function findGitConfig(dir2, walkUp = true) {
17606
- let cur = path16.resolve(dir2);
18932
+ let cur = path18.resolve(dir2);
17607
18933
  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);
18934
+ const dotGit = path18.join(cur, ".git");
18935
+ const direct = readSmallFile(path18.join(dotGit, "config"), 1e6);
17610
18936
  if (direct !== null) return direct;
17611
18937
  const pointer = readSmallFile(dotGit, 4096);
17612
18938
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
17613
18939
  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);
18940
+ let gitDir = path18.resolve(cur, m[1]);
18941
+ const common = readSmallFile(path18.join(gitDir, "commondir"), 4096);
18942
+ if (common !== null) gitDir = path18.resolve(gitDir, common.trim());
18943
+ return readSmallFile(path18.join(gitDir, "config"), 1e6);
17618
18944
  }
17619
- const parent = path16.dirname(cur);
18945
+ const parent = path18.dirname(cur);
17620
18946
  if (parent === cur) return null;
17621
18947
  cur = parent;
17622
18948
  }
@@ -17626,12 +18952,12 @@ var cache = /* @__PURE__ */ new Map();
17626
18952
  function readGitRemotes(dir2, opts = {}) {
17627
18953
  if (!dir2) return [];
17628
18954
  const walkUp = opts.walkUp !== false;
17629
- const key = `${walkUp ? "up" : "at"}:${path16.resolve(dir2)}`;
18955
+ const key = `${walkUp ? "up" : "at"}:${path18.resolve(dir2)}`;
17630
18956
  const hit = cache.get(key);
17631
18957
  if (hit) return hit;
17632
18958
  const out = [];
17633
18959
  try {
17634
- const cfg = findGitConfig(path16.resolve(dir2), walkUp);
18960
+ const cfg = findGitConfig(path18.resolve(dir2), walkUp);
17635
18961
  if (cfg !== null) {
17636
18962
  let current = null;
17637
18963
  for (const line of cfg.split(/\r?\n/)) {
@@ -17667,15 +18993,15 @@ var ECOSYSTEMS = [
17667
18993
  ];
17668
18994
  var cache2 = /* @__PURE__ */ new Map();
17669
18995
  function manifestRoot(dir2) {
17670
- let cur = path17.resolve(dir2);
18996
+ let cur = path19.resolve(dir2);
17671
18997
  for (let i = 0; i < 64; i++) {
17672
18998
  for (const [m] of ECOSYSTEMS) {
17673
18999
  try {
17674
- if (fs15.existsSync(path17.join(cur, m))) return cur;
19000
+ if (fs17.existsSync(path19.join(cur, m))) return cur;
17675
19001
  } catch {
17676
19002
  }
17677
19003
  }
17678
- const parent = path17.dirname(cur);
19004
+ const parent = path19.dirname(cur);
17679
19005
  if (parent === cur) return null;
17680
19006
  cur = parent;
17681
19007
  }
@@ -17684,7 +19010,7 @@ function manifestRoot(dir2) {
17684
19010
  function knownProjectHosts(dir2) {
17685
19011
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
17686
19012
  if (!dir2) return empty;
17687
- const key = path17.resolve(dir2);
19013
+ const key = path19.resolve(dir2);
17688
19014
  const hit = cache2.get(key);
17689
19015
  if (hit) return hit;
17690
19016
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
@@ -17697,10 +19023,10 @@ function knownProjectHosts(dir2) {
17697
19023
  const root = manifestRoot(key);
17698
19024
  if (root !== null) {
17699
19025
  for (const [m, registry] of ECOSYSTEMS) {
17700
- if (fs15.existsSync(path17.join(root, m))) out.hosts.add(registry);
19026
+ if (fs17.existsSync(path19.join(root, m))) out.hosts.add(registry);
17701
19027
  }
17702
19028
  try {
17703
- if (fs15.readdirSync(root).some((f) => f.endsWith(".gemspec")))
19029
+ if (fs17.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17704
19030
  out.hosts.add("host:rubygems.org");
17705
19031
  } catch {
17706
19032
  }
@@ -17713,9 +19039,9 @@ function knownProjectHosts(dir2) {
17713
19039
 
17714
19040
  // src/exposure/writes.ts
17715
19041
  init_dist();
17716
- import fs16 from "fs";
17717
- import os12 from "os";
17718
- import path18 from "path";
19042
+ import fs18 from "fs";
19043
+ import os14 from "os";
19044
+ import path20 from "path";
17719
19045
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
17720
19046
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
17721
19047
  function writtenContent(args) {
@@ -17744,7 +19070,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17744
19070
  if (typeof a.content === "string") {
17745
19071
  try {
17746
19072
  const next = JSON.parse(a.content);
17747
- const prev = JSON.parse(fs16.readFileSync(absPath, "utf8"));
19073
+ const prev = JSON.parse(fs18.readFileSync(absPath, "utf8"));
17748
19074
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
17749
19075
  } catch {
17750
19076
  return LIFECYCLE_RE.test(a.content);
@@ -17754,8 +19080,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17754
19080
  }
17755
19081
  function judgeWritesAfterExposure(opts) {
17756
19082
  const holds = [];
17757
- const home = opts.home ?? os12.homedir();
17758
- const tmp = normalizeFilePath(os12.tmpdir());
19083
+ const home = opts.home ?? os14.homedir();
19084
+ const tmp = normalizeFilePath(os14.tmpdir());
17759
19085
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
17760
19086
  const seen = /* @__PURE__ */ new Set();
17761
19087
  for (const access of opts.accesses) {
@@ -17765,7 +19091,7 @@ function judgeWritesAfterExposure(opts) {
17765
19091
  seen.add(p);
17766
19092
  if (p === "/dev" || p.startsWith("/dev/")) continue;
17767
19093
  if (root !== null && isInside(root, p)) {
17768
- if (isAgentInstructionPath(p, root) && (path18.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
19094
+ if (isAgentInstructionPath(p, root) && (path20.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17769
19095
  holds.push({ path: p, why: "agent-instruction" });
17770
19096
  continue;
17771
19097
  }
@@ -17787,25 +19113,25 @@ function judgeWritesAfterExposure(opts) {
17787
19113
 
17788
19114
  // src/exposure/marker.ts
17789
19115
  import crypto3 from "crypto";
17790
- import fs17 from "fs";
17791
- import os13 from "os";
17792
- import path19 from "path";
19116
+ import fs19 from "fs";
19117
+ import os15 from "os";
19118
+ import path21 from "path";
17793
19119
 
17794
19120
  // src/daemon/exposure-store.ts
17795
19121
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
17796
19122
 
17797
19123
  // src/exposure/marker.ts
17798
19124
  function dir() {
17799
- return path19.join(os13.homedir(), ".node9", "exposure-sessions");
19125
+ return path21.join(os15.homedir(), ".node9", "exposure-sessions");
17800
19126
  }
17801
19127
  function file(sessionId) {
17802
19128
  const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
17803
- return path19.join(dir(), id);
19129
+ return path21.join(dir(), id);
17804
19130
  }
17805
19131
  function maybeSessionExposed(sessionId) {
17806
19132
  if (!sessionId) return false;
17807
19133
  try {
17808
- const st = fs17.statSync(file(sessionId));
19134
+ const st = fs19.statSync(file(sessionId));
17809
19135
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
17810
19136
  } catch (e) {
17811
19137
  return e.code !== "ENOENT";
@@ -17813,27 +19139,7 @@ function maybeSessionExposed(sessionId) {
17813
19139
  }
17814
19140
 
17815
19141
  // 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
19142
+ import fs20 from "fs";
17837
19143
  var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
17838
19144
  var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
17839
19145
  function foldServerName(name) {
@@ -17843,7 +19149,7 @@ var inventoryCache = null;
17843
19149
  var inventoryStamp = null;
17844
19150
  function inventoryFileStamp() {
17845
19151
  try {
17846
- const st = fs19.statSync(getMcpToolsFile());
19152
+ const st = fs20.statSync(getMcpToolsFile());
17847
19153
  return `${st.mtimeMs}:${st.size}`;
17848
19154
  } catch {
17849
19155
  return "";
@@ -17928,15 +19234,15 @@ function sourceWord(toolName) {
17928
19234
 
17929
19235
  // src/auth/trusted-recipients.ts
17930
19236
  init_dist();
17931
- import fs20 from "fs";
17932
- import path21 from "path";
17933
- import os15 from "os";
19237
+ import fs21 from "fs";
19238
+ import path22 from "path";
19239
+ import os16 from "os";
17934
19240
  function getTrustedRecipientsPath() {
17935
- return path21.join(os15.homedir(), ".node9", "trusted-recipients.json");
19241
+ return path22.join(os16.homedir(), ".node9", "trusted-recipients.json");
17936
19242
  }
17937
19243
  function readTrustedRecipients() {
17938
19244
  try {
17939
- const raw = fs20.readFileSync(getTrustedRecipientsPath(), "utf8");
19245
+ const raw = fs21.readFileSync(getTrustedRecipientsPath(), "utf8");
17940
19246
  const parsed = JSON.parse(raw);
17941
19247
  return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
17942
19248
  } catch {
@@ -17945,10 +19251,10 @@ function readTrustedRecipients() {
17945
19251
  }
17946
19252
  function writeTrustedRecipients(entries) {
17947
19253
  const filePath = getTrustedRecipientsPath();
17948
- fs20.mkdirSync(path21.dirname(filePath), { recursive: true });
19254
+ fs21.mkdirSync(path22.dirname(filePath), { recursive: true });
17949
19255
  const tmp = filePath + ".node9-tmp";
17950
- fs20.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
17951
- fs20.renameSync(tmp, filePath);
19256
+ fs21.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
19257
+ fs21.renameSync(tmp, filePath);
17952
19258
  }
17953
19259
  function addTrustedRecipient(raw, addedBy = "user") {
17954
19260
  const value = normalizeRecipient(raw);
@@ -17963,7 +19269,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
17963
19269
  var cache3 = null;
17964
19270
  function mtimeOf() {
17965
19271
  try {
17966
- return fs20.statSync(getTrustedRecipientsPath()).mtimeMs;
19272
+ return fs21.statSync(getTrustedRecipientsPath()).mtimeMs;
17967
19273
  } catch {
17968
19274
  return 0;
17969
19275
  }
@@ -17978,40 +19284,40 @@ function isTrustedRecipient(value) {
17978
19284
 
17979
19285
  // src/auth/cloud.ts
17980
19286
  init_audit();
17981
- import fs22 from "fs";
17982
- import os16 from "os";
17983
- import path22 from "path";
19287
+ import fs23 from "fs";
19288
+ import os17 from "os";
19289
+ import path23 from "path";
17984
19290
 
17985
19291
  // src/utils/read-capped.ts
17986
- import fs21 from "fs";
19292
+ import fs22 from "fs";
17987
19293
  function readCapped(file2, maxBytes) {
17988
19294
  if (maxBytes < 0) return null;
17989
19295
  let fd;
17990
19296
  try {
17991
- fd = fs21.openSync(file2, "r");
19297
+ fd = fs22.openSync(file2, "r");
17992
19298
  } catch {
17993
19299
  return null;
17994
19300
  }
17995
19301
  try {
17996
- const st = fs21.fstatSync(fd);
19302
+ const st = fs22.fstatSync(fd);
17997
19303
  if (!st.isFile()) return null;
17998
19304
  const want = Math.min(st.size, maxBytes + 1);
17999
19305
  const buf = Buffer.alloc(want);
18000
- let read = 0;
18001
- while (read < want) {
18002
- const n = fs21.readSync(fd, buf, read, want - read, read);
19306
+ let read2 = 0;
19307
+ while (read2 < want) {
19308
+ const n = fs22.readSync(fd, buf, read2, want - read2, read2);
18003
19309
  if (n <= 0) break;
18004
- read += n;
19310
+ read2 += n;
18005
19311
  }
18006
19312
  return {
18007
- bytes: buf.subarray(0, Math.min(read, maxBytes)),
18008
- truncated: read > maxBytes
19313
+ bytes: buf.subarray(0, Math.min(read2, maxBytes)),
19314
+ truncated: read2 > maxBytes
18009
19315
  };
18010
19316
  } catch {
18011
19317
  return null;
18012
19318
  } finally {
18013
19319
  try {
18014
- fs21.closeSync(fd);
19320
+ fs22.closeSync(fd);
18015
19321
  } catch {
18016
19322
  }
18017
19323
  }
@@ -18056,7 +19362,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18056
19362
  let ciContext;
18057
19363
  if (process.env.CI) {
18058
19364
  try {
18059
- const ciContextPath = path22.join(os16.homedir(), ".node9", "ci-context.json");
19365
+ const ciContextPath = path23.join(os17.homedir(), ".node9", "ci-context.json");
18060
19366
  const ci = readCappedText(ciContextPath, 1e4);
18061
19367
  if (!ci) throw new Error("ci-context.json unreadable");
18062
19368
  if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
@@ -18098,9 +19404,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18098
19404
  context: {
18099
19405
  agent: meta?.agent,
18100
19406
  mcpServer: meta?.mcpServer,
18101
- hostname: os16.hostname(),
19407
+ hostname: os17.hostname(),
18102
19408
  cwd: process.cwd(),
18103
- platform: os16.platform()
19409
+ platform: os17.platform()
18104
19410
  },
18105
19411
  // The SaaS validates riskMetadata strictly and would drop it whole on
18106
19412
  // an unknown key, so only fields its schema declares go on the wire.
@@ -18174,14 +19480,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18174
19480
  });
18175
19481
  clearTimeout(timer);
18176
19482
  if (!res.ok) {
18177
- fs22.appendFileSync(
19483
+ fs23.appendFileSync(
18178
19484
  HOOK_DEBUG_LOG,
18179
19485
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
18180
19486
  `
18181
19487
  );
18182
19488
  }
18183
19489
  } catch (err) {
18184
- fs22.appendFileSync(
19490
+ fs23.appendFileSync(
18185
19491
  HOOK_DEBUG_LOG,
18186
19492
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
18187
19493
  `
@@ -18192,16 +19498,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18192
19498
  // src/loop-detector.ts
18193
19499
  init_dist();
18194
19500
  init_dist();
18195
- import fs23 from "fs";
18196
- import path23 from "path";
18197
- import os17 from "os";
19501
+ import fs24 from "fs";
19502
+ import path24 from "path";
19503
+ import os18 from "os";
18198
19504
  function loopStateFile() {
18199
- return path23.join(os17.homedir(), ".node9", "loop-state.json");
19505
+ return path24.join(os18.homedir(), ".node9", "loop-state.json");
18200
19506
  }
18201
19507
  function readState() {
18202
19508
  try {
18203
- if (!fs23.existsSync(loopStateFile())) return [];
18204
- const raw = fs23.readFileSync(loopStateFile(), "utf-8");
19509
+ if (!fs24.existsSync(loopStateFile())) return [];
19510
+ const raw = fs24.readFileSync(loopStateFile(), "utf-8");
18205
19511
  const parsed = JSON.parse(raw);
18206
19512
  if (!Array.isArray(parsed)) return [];
18207
19513
  return parsed;
@@ -18210,11 +19516,11 @@ function readState() {
18210
19516
  }
18211
19517
  }
18212
19518
  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());
19519
+ const dir2 = path24.dirname(loopStateFile());
19520
+ if (!fs24.existsSync(dir2)) fs24.mkdirSync(dir2, { recursive: true });
19521
+ const tmpPath = `${loopStateFile()}.${os18.hostname()}.${process.pid}.tmp`;
19522
+ fs24.writeFileSync(tmpPath, JSON.stringify(records));
19523
+ fs24.renameSync(tmpPath, loopStateFile());
18218
19524
  }
18219
19525
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18220
19526
  try {
@@ -18229,20 +19535,20 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18229
19535
  // src/supply-chain/check.ts
18230
19536
  init_dist();
18231
19537
  import picomatch2 from "picomatch";
18232
- import path26 from "path";
19538
+ import path27 from "path";
18233
19539
 
18234
19540
  // src/supply-chain/osv-index.ts
18235
19541
  init_dist();
18236
- import fs24 from "fs";
18237
- import os18 from "os";
18238
- import path24 from "path";
19542
+ import fs25 from "fs";
19543
+ import os19 from "os";
19544
+ import path25 from "path";
18239
19545
  import { createHash as createHash4 } from "crypto";
18240
19546
  var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
18241
19547
  function osvRoot() {
18242
- return path24.join(os18.homedir(), ".node9", "osv");
19548
+ return path25.join(os19.homedir(), ".node9", "osv");
18243
19549
  }
18244
19550
  function ecoDir(eco) {
18245
- return path24.join(osvRoot(), eco);
19551
+ return path25.join(osvRoot(), eco);
18246
19552
  }
18247
19553
  function indexKey(eco, name) {
18248
19554
  switch (eco) {
@@ -18261,11 +19567,11 @@ function shardOf(key) {
18261
19567
  return createHash4("sha1").update(key).digest("hex").slice(0, 2);
18262
19568
  }
18263
19569
  function shardPath(eco, shard) {
18264
- return path24.join(ecoDir(eco), "shards", `${shard}.json`);
19570
+ return path25.join(ecoDir(eco), "shards", `${shard}.json`);
18265
19571
  }
18266
19572
  function readMeta(eco) {
18267
19573
  try {
18268
- const m = JSON.parse(fs24.readFileSync(path24.join(ecoDir(eco), "meta.json"), "utf8"));
19574
+ const m = JSON.parse(fs25.readFileSync(path25.join(ecoDir(eco), "meta.json"), "utf8"));
18269
19575
  if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
18270
19576
  return m;
18271
19577
  } catch {
@@ -18274,7 +19580,7 @@ function readMeta(eco) {
18274
19580
  }
18275
19581
  function readShard(eco, shard) {
18276
19582
  try {
18277
- const s = JSON.parse(fs24.readFileSync(shardPath(eco, shard), "utf8"));
19583
+ const s = JSON.parse(fs25.readFileSync(shardPath(eco, shard), "utf8"));
18278
19584
  return s && typeof s === "object" && !Array.isArray(s) ? s : {};
18279
19585
  } catch {
18280
19586
  return {};
@@ -18401,13 +19707,101 @@ async function queryOsvMalicious(queries) {
18401
19707
 
18402
19708
  // src/supply-chain/registry.ts
18403
19709
  import { maxSatisfying, satisfies, validRange } from "semver";
19710
+
19711
+ // src/supply-chain/provenance.ts
19712
+ import { prerelease, rcompare, valid } from "semver";
18404
19713
  var TIMEOUT_MS2 = 1500;
18405
- var MAX_BYTES = 2 * 1024 * 1024;
19714
+ var MAX_BYTES = 256 * 1024;
19715
+ function normalizeRepo(raw, host2 = "github.com") {
19716
+ let s = raw.trim().toLowerCase();
19717
+ s = s.replace(/^git\+/, "").replace(/^[a-z]+:\/\//, "");
19718
+ s = s.replace(/@refs\/.*$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
19719
+ if (!s.includes(".") || !s.split("/")[0].includes(".")) s = `${host2}/${s}`;
19720
+ return /^[a-z0-9.-]+\/[\w.-]+\/[\w.-]+/.test(s) ? s : null;
19721
+ }
19722
+ function npmRepoOf(doc) {
19723
+ const list = doc?.attestations;
19724
+ if (!Array.isArray(list)) return null;
19725
+ for (const a of list) {
19726
+ const att = a;
19727
+ if (!att.predicateType?.startsWith("https://slsa.dev/provenance/")) continue;
19728
+ const payload = att.bundle?.dsseEnvelope?.payload;
19729
+ if (typeof payload !== "string") continue;
19730
+ try {
19731
+ const statement = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
19732
+ const p = statement.predicate;
19733
+ const repo = p?.buildDefinition?.externalParameters?.workflow?.repository ?? p?.invocation?.configSource?.uri;
19734
+ if (typeof repo === "string") return normalizeRepo(repo);
19735
+ } catch {
19736
+ }
19737
+ }
19738
+ return null;
19739
+ }
19740
+ function pypiRepoOf(doc) {
19741
+ const bundles = doc?.attestation_bundles;
19742
+ if (!Array.isArray(bundles)) return null;
19743
+ for (const b of bundles) {
19744
+ const pub = b.publisher;
19745
+ if (typeof pub?.repository === "string") {
19746
+ const host2 = /gitlab/i.test(pub.kind ?? "") ? "gitlab.com" : "github.com";
19747
+ return normalizeRepo(pub.repository, host2);
19748
+ }
19749
+ if (/^google$/i.test(pub?.kind ?? "") && typeof pub?.email === "string") {
19750
+ return `google:${pub.email.trim().toLowerCase()}`;
19751
+ }
19752
+ }
19753
+ return null;
19754
+ }
19755
+ async function npmSourceRepo(name, version) {
19756
+ const base2 = endpoints().npmRegistry;
19757
+ if (!base2) return "unknown";
19758
+ const url = `${base2}/-/npm/v1/attestations/${name.replaceAll("/", "%2F")}@${encodeURIComponent(version)}`;
19759
+ return read(url, npmRepoOf);
19760
+ }
19761
+ async function read(url, repoOf) {
19762
+ try {
19763
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
19764
+ if (doc === null) return "none";
19765
+ return repoOf(doc) ?? "unknown";
19766
+ } catch {
19767
+ return "unknown";
19768
+ }
19769
+ }
19770
+ async function pypiSourceRepo(name, version, filename) {
19771
+ const base2 = endpoints().pypi;
19772
+ if (!base2) return "unknown";
19773
+ const url = `${base2}/integrity/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}/provenance`;
19774
+ return read(url, pypiRepoOf);
19775
+ }
19776
+ var PYPI_FINAL = /^\d+(?:\.\d+)*$/;
19777
+ function comparePypi(a, b) {
19778
+ const x = a.split(".").map(Number);
19779
+ const y = b.split(".").map(Number);
19780
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
19781
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
19782
+ if (d !== 0) return d;
19783
+ }
19784
+ return 0;
19785
+ }
19786
+ function previousRelease(eco, versions, version) {
19787
+ if (eco === "npm") {
19788
+ if (!valid(version)) return void 0;
19789
+ const below2 = versions.filter((v) => valid(v) && !prerelease(v) && rcompare(v, version) > 0);
19790
+ return below2.sort(rcompare)[0] ?? null;
19791
+ }
19792
+ if (!PYPI_FINAL.test(version)) return void 0;
19793
+ const below = versions.filter((v) => PYPI_FINAL.test(v) && comparePypi(v, version) < 0);
19794
+ return below.sort((a, b) => comparePypi(b, a))[0] ?? null;
19795
+ }
19796
+
19797
+ // src/supply-chain/registry.ts
19798
+ var TIMEOUT_MS3 = 1500;
19799
+ var MAX_BYTES2 = 2 * 1024 * 1024;
18406
19800
  var VERSION_DOC_MAX_BYTES = 256 * 1024;
18407
19801
  var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
18408
19802
  async function npmVersionDocument(url, selector) {
18409
19803
  const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
18410
- timeoutMs: TIMEOUT_MS2,
19804
+ timeoutMs: TIMEOUT_MS3,
18411
19805
  maxBytes: VERSION_DOC_MAX_BYTES
18412
19806
  });
18413
19807
  if (!doc?.version) return null;
@@ -18431,8 +19825,8 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18431
19825
  let doc;
18432
19826
  try {
18433
19827
  doc = await fetchJson(url, {
18434
- timeoutMs: TIMEOUT_MS2,
18435
- maxBytes: MAX_BYTES,
19828
+ timeoutMs: TIMEOUT_MS3,
19829
+ maxBytes: MAX_BYTES2,
18436
19830
  headers: { Accept: "application/vnd.npm.install-v1+json" }
18437
19831
  });
18438
19832
  } catch (err) {
@@ -18460,13 +19854,23 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18460
19854
  version: resolved,
18461
19855
  hasInstallScript: v?.hasInstallScript === true
18462
19856
  };
19857
+ const previous = previousRelease("npm", Object.keys(doc.versions ?? {}), resolved);
19858
+ if (v && previous !== void 0) {
19859
+ const pv = previous ? doc.versions?.[previous] : void 0;
19860
+ info.history = {
19861
+ previous,
19862
+ previousHasInstallScript: pv ? pv.hasInstallScript === true : void 0,
19863
+ provenance: !!v.dist?.attestations?.provenance,
19864
+ previousProvenance: pv ? !!pv.dist?.attestations?.provenance : void 0
19865
+ };
19866
+ }
18463
19867
  if (!Number.isFinite(modifiedMs)) return info;
18464
19868
  if (Date.now() - modifiedMs >= freshWindowMs) {
18465
19869
  info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
18466
19870
  return info;
18467
19871
  }
18468
19872
  try {
18469
- const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
19873
+ const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18470
19874
  const t = full?.time?.[resolved];
18471
19875
  const tMs = t ? Date.parse(t) : NaN;
18472
19876
  if (Number.isFinite(tMs)) {
@@ -18484,15 +19888,37 @@ async function pypiInfo(name, version) {
18484
19888
  const base2 = endpoints().pypi;
18485
19889
  if (!base2) return null;
18486
19890
  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 });
19891
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18488
19892
  if (!doc?.info?.version) return null;
18489
19893
  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,
19894
+ const resolved = version ?? doc.info.version;
19895
+ const info = {
19896
+ version: resolved,
18492
19897
  // The earliest file of the release is when it became installable.
18493
- publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
19898
+ publishedAtMs: times.length > 0 ? Math.min(...times) : void 0,
19899
+ file: doc.urls?.find((u) => typeof u.filename === "string")?.filename
19900
+ };
19901
+ if (doc.releases) info.history = pypiHistoryOf(doc.releases, resolved) ?? void 0;
19902
+ return info;
19903
+ }
19904
+ function pypiHistoryOf(releases, version) {
19905
+ const versions = Object.keys(releases).filter((v) => (releases[v] ?? []).length > 0);
19906
+ const previous = previousRelease("PyPI", versions, version);
19907
+ if (previous === void 0) return null;
19908
+ return {
19909
+ previous,
19910
+ previousFile: previous ? releases[previous]?.find((f) => typeof f.filename === "string")?.filename : void 0
18494
19911
  };
18495
19912
  }
19913
+ async function pypiHistory(name, version) {
19914
+ const base2 = endpoints().pypi;
19915
+ if (!base2) return null;
19916
+ const doc = await fetchJson(`${base2}/pypi/${encodeURIComponent(name)}/json`, {
19917
+ timeoutMs: TIMEOUT_MS3,
19918
+ maxBytes: MAX_BYTES2
19919
+ });
19920
+ return doc?.releases ? pypiHistoryOf(doc.releases, version) : null;
19921
+ }
18496
19922
  var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
18497
19923
  async function registryInfo(eco, name, version, freshWindowMs, raw) {
18498
19924
  if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
@@ -18501,19 +19927,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
18501
19927
  }
18502
19928
 
18503
19929
  // src/supply-chain/local-resolve.ts
18504
- import fs25 from "fs";
18505
- import path25 from "path";
19930
+ import fs26 from "fs";
19931
+ import path26 from "path";
18506
19932
  var MAX_LEVELS = 32;
18507
19933
  var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
18508
19934
  function resolveInstalled(name, cwd) {
18509
- if (!cwd || !path25.isAbsolute(cwd)) return null;
19935
+ if (!cwd || !path26.isAbsolute(cwd)) return null;
18510
19936
  if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
18511
- let dir2 = path25.resolve(cwd);
19937
+ let dir2 = path26.resolve(cwd);
18512
19938
  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");
19939
+ const pkgDir = path26.join(dir2, "node_modules", ...name.split("/"));
19940
+ const manifest = path26.join(pkgDir, "package.json");
18515
19941
  try {
18516
- const raw = fs25.readFileSync(manifest, "utf8");
19942
+ const raw = fs26.readFileSync(manifest, "utf8");
18517
19943
  const parsed = JSON.parse(raw);
18518
19944
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
18519
19945
  return { version: parsed.version, dir: pkgDir };
@@ -18521,7 +19947,7 @@ function resolveInstalled(name, cwd) {
18521
19947
  return null;
18522
19948
  } catch {
18523
19949
  }
18524
- const parent = path25.dirname(dir2);
19950
+ const parent = path26.dirname(dir2);
18525
19951
  if (parent === dir2) break;
18526
19952
  dir2 = parent;
18527
19953
  }
@@ -18643,7 +20069,7 @@ function checkInstalled(p, version, misses) {
18643
20069
  }
18644
20070
  async function checkOne(p, cfg, misses, networkCapped, cwd) {
18645
20071
  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;
20072
+ const localCwd = cwd && path27.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path27.resolve(dir2, step), cwd) : void 0;
18647
20073
  const installed = resolveInstalled(p.name, localCwd);
18648
20074
  if (installed && (!p.version || p.version === installed.version))
18649
20075
  return checkInstalled(p, installed.version, misses);
@@ -18686,8 +20112,61 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
18686
20112
  detail: `${label(p, version)} runs an install script`
18687
20113
  });
18688
20114
  }
20115
+ if (info && version && (cfg.newPackage !== "off" || cfg.installScript !== "off")) {
20116
+ return byHistory(p, version, info, findings, misses);
20117
+ }
18689
20118
  return findings;
18690
20119
  }
20120
+ var SOFT = /* @__PURE__ */ new Set(["new", "install-script"]);
20121
+ async function byHistory(p, version, info, findings, misses) {
20122
+ let out = findings;
20123
+ let h = info.history;
20124
+ if (!h && p.ecosystem === "PyPI" && out.some((f) => SOFT.has(f.kind))) {
20125
+ try {
20126
+ h = await pypiHistory(p.name, version) ?? void 0;
20127
+ } catch (err) {
20128
+ misses.push(`${label(p, version)}: release history ${err.message}`);
20129
+ }
20130
+ }
20131
+ if (!h || !h.previous) return out;
20132
+ const previous = h.previous;
20133
+ if (h.previousHasInstallScript === true) {
20134
+ out = out.filter((f) => f.kind !== "install-script");
20135
+ }
20136
+ const drop = () => ({
20137
+ pkg: p,
20138
+ version,
20139
+ kind: "provenance-drop",
20140
+ detail: `${label(p, version)} was published without the provenance its previous release (${previous}) had`
20141
+ });
20142
+ if (p.ecosystem === "npm") {
20143
+ if (h.previousProvenance && h.provenance === false) return [...out, drop()];
20144
+ if (!out.some((f) => SOFT.has(f.kind)) || !h.provenance || !h.previousProvenance) return out;
20145
+ const [now, before] = await Promise.all([
20146
+ npmSourceRepo(p.name, version),
20147
+ npmSourceRepo(p.name, previous)
20148
+ ]);
20149
+ return vouched(p, version, now, before, out, misses);
20150
+ }
20151
+ if (p.ecosystem === "PyPI") {
20152
+ if (!out.some((f) => SOFT.has(f.kind)) || !info.file || !h.previousFile) return out;
20153
+ const [now, before] = await Promise.all([
20154
+ pypiSourceRepo(p.name, version, info.file),
20155
+ pypiSourceRepo(p.name, previous, h.previousFile)
20156
+ ]);
20157
+ if (now === "none" && before !== "none" && before !== "unknown") return [...out, drop()];
20158
+ return vouched(p, version, now, before, out, misses);
20159
+ }
20160
+ return out;
20161
+ }
20162
+ function vouched(p, version, now, before, findings, misses) {
20163
+ if (now === "unknown" || before === "unknown") {
20164
+ misses.push(`${label(p, version)}: provenance unreadable, findings kept`);
20165
+ return findings;
20166
+ }
20167
+ if (now === "none" || before === "none" || now !== before) return findings;
20168
+ return findings.filter((f) => !SOFT.has(f.kind));
20169
+ }
18691
20170
  async function runPackageCheck(toolName, args, cfg, cwd) {
18692
20171
  if (!cfg.enabled) return ALLOW;
18693
20172
  try {
@@ -19070,7 +20549,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19070
20549
  fileAccess,
19071
20550
  config,
19072
20551
  skipIgnoredFastPath,
19073
- argumentScan: argScan
20552
+ argumentScan: argScan,
20553
+ // A server a person linked to an app is judged as that app (fw#390).
20554
+ mcpServer: resolveLinkedServer(meta?.mcpServer, {
20555
+ serverKey: meta?.serverKey,
20556
+ managedLinks: config.policy.appLinks,
20557
+ appliedShields: config.policy.appliedShields,
20558
+ workspaceGoverned: config.policySource === "workspace"
20559
+ })
19074
20560
  });
19075
20561
  if (!sessionScript || own.decision === "block") return own;
19076
20562
  const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
@@ -19286,7 +20772,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19286
20772
  const last = rec.sources[rec.sources.length - 1];
19287
20773
  const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
19288
20774
  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";
20775
+ const read2 = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
19290
20776
  if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
19291
20777
  const noun = (d) => {
19292
20778
  const k = destinationKind(d);
@@ -19295,14 +20781,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19295
20781
  const parts = held.map(
19296
20782
  (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
20783
  );
19298
- const home = os19.homedir();
20784
+ const home = os20.homedir();
19299
20785
  const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
19300
20786
  for (const w of writeHolds) {
19301
20787
  parts.push(
19302
20788
  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
20789
  );
19304
20790
  }
19305
- exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
20791
+ exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
19306
20792
  exposureFromEid = last?.eid;
19307
20793
  if (exposureFromEid) exposureMeta = { exposureFromEid };
19308
20794
  exposureKeys = [