@node9/proxy 2.37.1 → 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.js CHANGED
@@ -71,6 +71,93 @@ var init_hasher = __esm({
71
71
  });
72
72
 
73
73
  // packages/policy-engine/dist/index.mjs
74
+ function validateRegex(pattern) {
75
+ if (!pattern) return "Pattern is required";
76
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
77
+ try {
78
+ new RegExp(pattern);
79
+ } catch (e) {
80
+ return `Invalid regex syntax: ${e.message}`;
81
+ }
82
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
83
+ if (!(0, import_safe_regex2.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
84
+ return null;
85
+ }
86
+ function getCompiledRegex(pattern, flags = "") {
87
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
88
+ const key = `${pattern}\0${flags}`;
89
+ if (regexCache.has(key)) {
90
+ const cached = regexCache.get(key);
91
+ regexCache.delete(key);
92
+ regexCache.set(key, cached);
93
+ return cached;
94
+ }
95
+ if (validateRegex(pattern) !== null) return null;
96
+ try {
97
+ const re = new RegExp(pattern, flags);
98
+ if (regexCache.size >= REGEX_CACHE_MAX) {
99
+ const oldest = regexCache.keys().next().value;
100
+ if (oldest) regexCache.delete(oldest);
101
+ }
102
+ regexCache.set(key, re);
103
+ return re;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ function isToolRule(rule) {
109
+ return rule.tool.startsWith("{,mcp__*__}");
110
+ }
111
+ function actionPart(ruleName) {
112
+ const m = /^shield:[^:]+:(.+)$/.exec(ruleName ?? "");
113
+ return m ? m[1] : ruleName ?? "rule";
114
+ }
115
+ function ownReason(reason, owner) {
116
+ if (!reason) return reason;
117
+ return reason.replace(/\((?:Finance|Identity|Publishing) shield\)\.?$/, `(${owner}).`);
118
+ }
119
+ function buildAppShield(spec) {
120
+ const owner = spec.label;
121
+ const toolRules = ACTION_TEMPLATES[spec.template].filter(isToolRule).map((rule) => ({
122
+ ...rule,
123
+ name: `shield:${spec.name}:${actionPart(rule.name)}`,
124
+ server: spec.server,
125
+ reason: ownReason(rule.reason, owner)
126
+ }));
127
+ const ownTools = (spec.mcpTools ?? []).map((rule) => ({
128
+ ...rule,
129
+ server: spec.server
130
+ }));
131
+ return {
132
+ name: spec.name,
133
+ description: spec.description,
134
+ aliases: [],
135
+ smartRules: [...toolRules, ...ownTools, ...spec.doors],
136
+ dangerousWords: []
137
+ };
138
+ }
139
+ function buildGeneralRules() {
140
+ const out = [];
141
+ for (const name of Object.keys(ACTION_TEMPLATES)) {
142
+ const { slug: slug2, label: label2 } = ACTION_CHECKS[name];
143
+ for (const rule of ACTION_TEMPLATES[name].filter(isToolRule)) {
144
+ const part = actionPart(rule.name).replace(/^review-/, "");
145
+ out.push({
146
+ ...rule,
147
+ name: `review-action-${slug2}-${part}`,
148
+ notServer: APP_SERVER_GLOB,
149
+ reason: ownReason(rule.reason, label2)
150
+ });
151
+ }
152
+ }
153
+ return out;
154
+ }
155
+ function generalActionRuleNames(template) {
156
+ const { slug: slug2 } = ACTION_CHECKS[template];
157
+ return GENERAL_ACTION_RULES.filter((r) => r.name?.startsWith(`review-action-${slug2}-`)).map(
158
+ (r) => r.name
159
+ );
160
+ }
74
161
  function isShieldVerdict(v) {
75
162
  return v === "allow" || v === "review" || v === "block";
76
163
  }
@@ -108,16 +195,25 @@ function validateOverrides(raw) {
108
195
  return { overrides: result, warnings };
109
196
  }
110
197
  function assertBuiltinShieldRegexesAreSafe() {
111
- for (const shield of Object.values(BUILTIN_SHIELDS)) {
198
+ const sets = [
199
+ ...Object.values(BUILTIN_SHIELDS).map((sh) => ({
200
+ name: sh.name,
201
+ smartRules: sh.smartRules
202
+ })),
203
+ ...Object.entries(ACTION_TEMPLATES).map(([name, smartRules]) => ({ name, smartRules })),
204
+ { name: "general-action-checks", smartRules: GENERAL_ACTION_RULES }
205
+ ];
206
+ for (const shield of sets) {
112
207
  for (const rule of shield.smartRules) {
113
208
  const conditions = rule.conditions ?? [];
114
209
  for (const cond of conditions) {
115
210
  if (cond.op !== "matches" && cond.op !== "notMatches") continue;
116
211
  const pattern = cond.value;
117
212
  if (!pattern) continue;
118
- if (!(0, import_safe_regex2.default)(pattern)) {
213
+ const problem = validateRegex(pattern);
214
+ if (problem) {
119
215
  throw new Error(
120
- `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has unsafe regex: ${pattern}`
216
+ `[node9 engine] Shield '${shield.name}' rule '${rule.name ?? rule.tool}' has a regex the runtime refuses (${problem}): ${pattern}`
121
217
  );
122
218
  }
123
219
  }
@@ -403,7 +499,7 @@ function isLockedCheck(def) {
403
499
  function ssrfCheckId(tier) {
404
500
  return isStrictGatedTier(tier) ? "network.internal-addresses" : "network.metadata";
405
501
  }
406
- function unwrapRuleLabel(s) {
502
+ function ruleNameInLabel(s) {
407
503
  let rest = s;
408
504
  const override = rest.lastIndexOf(OVERRIDE_PREFIX);
409
505
  if (override >= 0) rest = rest.slice(override + OVERRIDE_PREFIX.length).trimStart();
@@ -419,7 +515,7 @@ function checkIdForRule(nameOrLabel) {
419
515
  const s = nameOrLabel.trim();
420
516
  const exact = LEGACY_TO_ID.get(s);
421
517
  if (exact) return exact;
422
- const unwrapped = unwrapRuleLabel(s);
518
+ const unwrapped = ruleNameInLabel(s);
423
519
  if (unwrapped !== void 0) return checkIdForRule(unwrapped);
424
520
  if (s.startsWith("shield:project-jail:")) {
425
521
  return PROJECT_JAIL_BLOCK_READ.test(s) ? "data.credential-files" : "data.credential-files-other";
@@ -507,9 +603,9 @@ function resolveCheck(settings, id) {
507
603
  const on = settings.appliedShields?.includes(def.pack) ?? false;
508
604
  return { id, value: on ? def.defaultValue : "off", source: "default" };
509
605
  }
510
- const read = LEGACY_READERS[id]?.(settings);
511
- if (read === void 0) return { id, value: def.defaultValue, source: "default" };
512
- const value = CHECK_VERDICT_RANK[read] < floor ? def.floor : read;
606
+ const read2 = LEGACY_READERS[id]?.(settings);
607
+ if (read2 === void 0) return { id, value: def.defaultValue, source: "default" };
608
+ const value = CHECK_VERDICT_RANK[read2] < floor ? def.floor : read2;
513
609
  return { id, value, source: value === def.defaultValue ? "default" : "configured" };
514
610
  }
515
611
  function checkValue(settings, id) {
@@ -577,6 +673,102 @@ function scanViews(text) {
577
673
  }
578
674
  return views;
579
675
  }
676
+ function isAgentInstructionPath(absPath, root) {
677
+ const r = root.replace(/\/$/, "");
678
+ if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
679
+ const rel = absPath.slice(r.length + 1);
680
+ if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
681
+ return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
682
+ }
683
+ function isInside(root, p) {
684
+ const r = root.replace(/\/$/, "") || "/";
685
+ return p === r || p.startsWith(r === "/" ? "/" : r + "/");
686
+ }
687
+ function isPasswordHashPath(p) {
688
+ return PASSWORD_HASH_FILES.includes(p);
689
+ }
690
+ function isLegacySystemPath(p) {
691
+ return LEGACY_SYSTEM_PATH_RE.test(p);
692
+ }
693
+ function normalizeFilePath(p, cwd = "", home = "~") {
694
+ if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
695
+ if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
696
+ const rooted = p.startsWith("/");
697
+ const parts = [];
698
+ for (const part of p.split("/")) {
699
+ if (!part || part === ".") continue;
700
+ if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
701
+ else if (part !== ".." || !rooted) parts.push(part);
702
+ }
703
+ return (rooted ? "/" : "") + parts.join("/");
704
+ }
705
+ function inlineLanguageOf(interp) {
706
+ const i = interp.toLowerCase();
707
+ if (/^python[\d.]*$/.test(i)) return "python";
708
+ if (/^(?:node|tsx|ts-node|deno|bun)$/.test(i)) return "node";
709
+ if (i === "perl") return "perl";
710
+ if (i === "ruby" || i === "irb") return "ruby";
711
+ if (i === "php") return "php";
712
+ return null;
713
+ }
714
+ function resolvePythonAliases(code) {
715
+ const aliases = [...code.matchAll(/\bimport\s+([\w.]+)\s+as\s+(\w+)/g)].slice(0, 32);
716
+ let out = code;
717
+ for (const [, mod, alias] of aliases) {
718
+ if (alias === mod) continue;
719
+ out = out.replace(new RegExp(String.raw`(?<![\w.])${alias}\b`, "g"), mod);
720
+ }
721
+ return out;
722
+ }
723
+ function protectedLiteral(code, isProtected) {
724
+ let n = 0;
725
+ for (const m of code.matchAll(STRING_LITERAL)) {
726
+ if (++n > 2048) break;
727
+ const value = m[2].trim();
728
+ if (value && isProtected(value)) return value;
729
+ }
730
+ return null;
731
+ }
732
+ function amplifies(language, code) {
733
+ 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*\)/;
734
+ const m = loop.exec(code);
735
+ if (!m) return null;
736
+ const body = code.slice(m.index);
737
+ 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(
738
+ body
739
+ ) ? m[0] : null;
740
+ }
741
+ function judgeInlineCode(language, code, isProtected) {
742
+ if (code.length > MAX_INLINE_CODE) {
743
+ return { outcome: "unreadable", what: "is too long for node9 to read" };
744
+ }
745
+ const text = language === "python" ? resolvePythonAliases(code) : code;
746
+ for (const [category, re] of TABLES[language]) {
747
+ const m = re.exec(text);
748
+ if (!m) continue;
749
+ const evidence = m[0].trim().replace(/\s+/g, " ").slice(0, 60);
750
+ return {
751
+ outcome: category === "builds-code" ? "unreadable" : "dangerous",
752
+ what: WHAT[category],
753
+ evidence,
754
+ category
755
+ };
756
+ }
757
+ const path28 = protectedLiteral(text, isProtected);
758
+ if (path28) {
759
+ return {
760
+ outcome: "dangerous",
761
+ what: WHAT["reads-protected"],
762
+ evidence: path28.slice(0, 60),
763
+ category: "reads-protected"
764
+ };
765
+ }
766
+ const loop = amplifies(language, text);
767
+ if (loop) {
768
+ return { outcome: "dangerous", what: WHAT.amplifies, evidence: loop, category: "amplifies" };
769
+ }
770
+ return null;
771
+ }
580
772
  function isCatHeredocOrLit(part) {
581
773
  if (!part) return false;
582
774
  const t = syntax.NodeType(part);
@@ -645,6 +837,55 @@ function cachedNormalize(command, compute) {
645
837
  function normalizeCommandForPolicy(command) {
646
838
  return commandReadingsImpl(command).posix;
647
839
  }
840
+ function fileAccessView(command) {
841
+ const hit = accessViewCache.get(command);
842
+ if (hit !== void 0) return hit;
843
+ const view = fileAccessViewImpl(command);
844
+ if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
845
+ const oldest = accessViewCache.keys().next().value;
846
+ if (oldest !== void 0) accessViewCache.delete(oldest);
847
+ }
848
+ accessViewCache.set(command, view);
849
+ return view;
850
+ }
851
+ function fileAccessViewImpl(command) {
852
+ const text = command.replace(INTERPRETER_LINE, "");
853
+ const f = parseShared(text);
854
+ if (f === PARSE_FAIL) return text;
855
+ const toCharIndex = byteOffsetToCharIndex(text);
856
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
857
+ const edits = [];
858
+ try {
859
+ syntax.Walk(f, (node) => {
860
+ if (!node) return false;
861
+ const n = node;
862
+ if (syntax.NodeType(n) !== "CallExpr") return true;
863
+ const args = n.Args || [];
864
+ const words = args.map((a) => resolveWordLiteral(a));
865
+ const head = unwrapCommandHead(words);
866
+ for (const i of /* @__PURE__ */ new Set([0, head])) {
867
+ const word = words[i];
868
+ if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
869
+ continue;
870
+ }
871
+ if (!PROGRAM_DIR.test(word)) continue;
872
+ const name = word.split(/[\\/]/).pop() ?? "";
873
+ if (!name) continue;
874
+ const s = at(args[i].Pos().Offset());
875
+ const e = at(args[i].End().Offset());
876
+ if (s < 0 || e < 0) continue;
877
+ edits.push([s, e, name]);
878
+ }
879
+ return true;
880
+ });
881
+ } catch {
882
+ return text;
883
+ }
884
+ edits.sort((a, b) => b[0] - a[0]);
885
+ let out = text;
886
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
887
+ return out;
888
+ }
648
889
  function commandReadings(command) {
649
890
  const r = commandReadingsImpl(command);
650
891
  const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
@@ -1100,53 +1341,119 @@ function unwrapCommandHead(words, stopAtFind = false, stopAtDynamic = false) {
1100
1341
  }
1101
1342
  return i;
1102
1343
  }
1103
- function inlineExecStmt(stmt, pipeFed) {
1344
+ function heredocText(word) {
1345
+ const parts = word?.Parts ?? [];
1346
+ let out = "";
1347
+ for (const p of parts) {
1348
+ if (syntax.NodeType(p) !== "Lit") return null;
1349
+ out += p.Value ?? "";
1350
+ }
1351
+ return out;
1352
+ }
1353
+ function stdinCodeOf(redirs) {
1354
+ const heredoc = /* @__PURE__ */ new Set([deriveRedirOp("cat <<X\nX"), deriveRedirOp("cat <<-X\nX")]);
1355
+ const herestring = deriveRedirOp("cat <<< x");
1356
+ for (const r of redirs) {
1357
+ if (!r || !redirStdinOps().has(r.Op)) continue;
1358
+ if (heredoc.has(r.Op)) {
1359
+ const text = heredocText(r.Hdoc);
1360
+ return text === null ? { code: null, source: "variable" } : { code: text };
1361
+ }
1362
+ if (r.Op === herestring) {
1363
+ const text = resolveWordLiteral(r.Word);
1364
+ return text === null ? { code: null, source: "variable" } : { code: text };
1365
+ }
1366
+ return { code: null, source: "file" };
1367
+ }
1368
+ return { code: null, source: "file" };
1369
+ }
1370
+ function attachedCode(interp, flag) {
1371
+ const eq = flag.indexOf("=");
1372
+ if (flag.startsWith("--")) return eq >= 0 ? flag.slice(eq + 1) : null;
1373
+ if (/^(pwsh|powershell)/.test(interp)) return null;
1374
+ const letters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1375
+ const body = flag.slice(1);
1376
+ const at = [...body].findIndex((ch) => letters.includes(ch.toLowerCase()));
1377
+ if (at < 0) return null;
1378
+ let rest = body.slice(at + 1);
1379
+ if (letters === "ep" && /^[ep]*$/i.test(rest)) rest = "";
1380
+ return rest === "" ? null : rest;
1381
+ }
1382
+ function inlineExecCall(stmt, pipeFed) {
1104
1383
  const cmd = stmt?.Cmd;
1105
- if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1384
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return null;
1106
1385
  const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1107
- if (words.length === 0) return false;
1386
+ if (words.length === 0) return null;
1108
1387
  const headIdx = unwrapCommandHead(words);
1109
1388
  const rawHead = words[headIdx];
1110
- if (rawHead == null) return false;
1389
+ if (rawHead == null) return null;
1111
1390
  const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1112
- if (!INLINE_INTERPRETER.test(interp)) return false;
1391
+ if (!INLINE_INTERPRETER.test(interp)) return null;
1113
1392
  let args = words.slice(headIdx + 1);
1114
- if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1393
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") {
1394
+ const code = args[1] ?? null;
1395
+ return { interp, code: [code], source: code === null ? "variable" : void 0 };
1396
+ }
1115
1397
  if (INTERP_LEADING_TARGET.has(interp)) {
1116
1398
  const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1117
1399
  args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1118
1400
  }
1401
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1402
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1403
+ const fromStdin = () => {
1404
+ if (stdinFed) {
1405
+ const { code, source } = stdinCodeOf(redirs);
1406
+ return { interp, code: [code], source };
1407
+ }
1408
+ return { interp, code: [null], source: "pipe" };
1409
+ };
1119
1410
  let positionals2 = 0;
1120
1411
  let selectedProgram = false;
1412
+ const codes = [];
1121
1413
  for (let i = 0; i < args.length; i++) {
1122
1414
  const a = args[i];
1123
1415
  if (a == null) {
1416
+ if (codes.length > 0) break;
1124
1417
  positionals2++;
1125
1418
  selectedProgram = true;
1126
1419
  continue;
1127
1420
  }
1128
1421
  if (!selectedProgram && isInlineCodeFlag(interp, a)) {
1129
- if (!/^(bash|sh|zsh|dash|ksh)$/.test(interp)) return true;
1130
- const payload = args[i + 1];
1131
- if (payload == null) return true;
1132
- return shellPayloadNeedsReview(payload);
1422
+ if (/^(bash|sh|zsh|dash|ksh)$/.test(interp)) {
1423
+ const payload = args[i + 1];
1424
+ if (payload == null) return { interp, code: [null], source: "variable" };
1425
+ return shellPayloadNeedsReview(payload) ? { interp, code: [null], source: "payload" } : null;
1426
+ }
1427
+ const attached = attachedCode(interp, a);
1428
+ if (attached !== null) {
1429
+ codes.push(attached);
1430
+ } else {
1431
+ codes.push(args[i + 1] ?? null);
1432
+ i++;
1433
+ }
1434
+ continue;
1435
+ }
1436
+ if (codes.length > 0) {
1437
+ if (a.startsWith("-")) continue;
1438
+ break;
1133
1439
  }
1134
1440
  if (a === "-m") {
1135
1441
  selectedProgram = true;
1136
1442
  continue;
1137
1443
  }
1138
- if (a === "-" && !selectedProgram) return true;
1444
+ if (a === "-" && !selectedProgram) return fromStdin();
1139
1445
  if (!a.startsWith("-")) {
1140
1446
  positionals2++;
1141
1447
  selectedProgram = true;
1142
1448
  }
1143
1449
  }
1144
- const redirs = stmt.Redirs || cmd.Redirs || [];
1145
- const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1450
+ if (codes.length > 0) {
1451
+ return { interp, code: codes, source: codes.includes(null) ? "variable" : void 0 };
1452
+ }
1146
1453
  if (positionals2 === 0 && (stdinFed || pipeFed)) {
1147
- if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1454
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return fromStdin();
1148
1455
  }
1149
- return false;
1456
+ return null;
1150
1457
  }
1151
1458
  function shellPayloadNeedsReview(payload) {
1152
1459
  if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
@@ -1179,7 +1486,7 @@ function shellPayloadNeedsReview(payload) {
1179
1486
  payload
1180
1487
  ))
1181
1488
  review = true;
1182
- if (!review && detectInlineExec(payload)) review = true;
1489
+ if (!review && inlineExecReview(payload) !== null) review = true;
1183
1490
  } catch {
1184
1491
  review = true;
1185
1492
  } finally {
@@ -1187,35 +1494,77 @@ function shellPayloadNeedsReview(payload) {
1187
1494
  }
1188
1495
  return review;
1189
1496
  }
1190
- function detectInlineExec(command) {
1497
+ function inlineExecCalls(command) {
1191
1498
  const f = parseShared(command);
1192
- if (f === PARSE_FAIL) {
1193
- 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(
1194
- command
1195
- );
1196
- }
1197
- let found = false;
1499
+ if (f === PARSE_FAIL) return PARSE_FAIL;
1500
+ const calls = [];
1198
1501
  try {
1199
1502
  syntax.Walk(f, (node) => {
1200
- if (!node || found) return false;
1503
+ if (!node || calls.length >= MAX_INLINE_CALLS) return false;
1201
1504
  const n = node;
1202
1505
  const t = syntax.NodeType(n);
1203
1506
  if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1204
- if (inlineExecStmt(n.Y, true)) {
1205
- found = true;
1206
- return false;
1207
- }
1507
+ const call = inlineExecCall(n.Y, true);
1508
+ if (call) calls.push(call);
1208
1509
  }
1209
- if (t === "Stmt" && inlineExecStmt(n, false)) {
1210
- found = true;
1211
- return false;
1510
+ if (t === "Stmt") {
1511
+ const call = inlineExecCall(n, false);
1512
+ if (call) calls.push(call);
1212
1513
  }
1213
1514
  return true;
1214
1515
  });
1215
1516
  } catch {
1216
- return found;
1217
1517
  }
1218
- return found;
1518
+ return calls;
1519
+ }
1520
+ function detectInlineExec(command) {
1521
+ const calls = inlineExecCalls(command);
1522
+ if (calls === PARSE_FAIL) return INLINE_EXEC_FALLBACK.test(command);
1523
+ return calls.length > 0;
1524
+ }
1525
+ function isProtectedCodePath(p) {
1526
+ if (matchSensitivePath(p) !== null) return true;
1527
+ return SYSTEM_AUTH_FILES.includes(p) || SYSTEM_AUTH_DIRS.some((d) => p === d || p.startsWith(d + "/"));
1528
+ }
1529
+ function inlineExecDescription(r) {
1530
+ 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.";
1531
+ return `${lead} Review the full script below before allowing it to execute.`;
1532
+ }
1533
+ function inlineExecReason(r) {
1534
+ const who = r.interp ? `This inline ${r.interp} code` : "This inline code";
1535
+ return r.evidence ? `${who} ${r.what} (${r.evidence})` : `${who} ${r.what}`;
1536
+ }
1537
+ function inlineExecReview(command) {
1538
+ const calls = inlineExecCalls(command);
1539
+ if (calls === PARSE_FAIL) {
1540
+ return INLINE_EXEC_FALLBACK.test(command) ? { outcome: "unreadable", interp: "", what: "could not be parsed" } : null;
1541
+ }
1542
+ let unreadable = calls.length >= MAX_INLINE_CALLS ? { outcome: "unreadable", interp: "", what: "runs more inline programs than node9 reads" } : null;
1543
+ for (const call of calls) {
1544
+ const language = inlineLanguageOf(call.interp);
1545
+ for (const code of call.code) {
1546
+ let finding = null;
1547
+ if (code === null) {
1548
+ finding = {
1549
+ outcome: "unreadable",
1550
+ interp: call.interp,
1551
+ what: UNREADABLE_SOURCE[call.source ?? "variable"]
1552
+ };
1553
+ } else if (language === null) {
1554
+ finding = {
1555
+ outcome: "unreadable",
1556
+ interp: call.interp,
1557
+ what: `is ${call.interp} code, which node9 does not read`
1558
+ };
1559
+ } else {
1560
+ const judged = judgeInlineCode(language, code, isProtectedCodePath);
1561
+ if (judged) finding = { ...judged, interp: call.interp };
1562
+ }
1563
+ if (finding?.outcome === "dangerous") return finding;
1564
+ if (finding && !unreadable) unreadable = finding;
1565
+ }
1566
+ }
1567
+ return unreadable;
1219
1568
  }
1220
1569
  function analyzeChmod777(command) {
1221
1570
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
@@ -2464,40 +2813,6 @@ function analyzeShellCommand(command) {
2464
2813
  }
2465
2814
  return { actions, paths, allTokens };
2466
2815
  }
2467
- function validateRegex(pattern) {
2468
- if (!pattern) return "Pattern is required";
2469
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
2470
- try {
2471
- new RegExp(pattern);
2472
- } catch (e) {
2473
- return `Invalid regex syntax: ${e.message}`;
2474
- }
2475
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
2476
- if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
2477
- return null;
2478
- }
2479
- function getCompiledRegex(pattern, flags = "") {
2480
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
2481
- const key = `${pattern}\0${flags}`;
2482
- if (regexCache.has(key)) {
2483
- const cached = regexCache.get(key);
2484
- regexCache.delete(key);
2485
- regexCache.set(key, cached);
2486
- return cached;
2487
- }
2488
- if (validateRegex(pattern) !== null) return null;
2489
- try {
2490
- const re = new RegExp(pattern, flags);
2491
- if (regexCache.size >= REGEX_CACHE_MAX) {
2492
- const oldest = regexCache.keys().next().value;
2493
- if (oldest) regexCache.delete(oldest);
2494
- }
2495
- regexCache.set(key, re);
2496
- return re;
2497
- } catch {
2498
- return null;
2499
- }
2500
- }
2501
2816
  function matchesPattern(text, patterns) {
2502
2817
  const p = Array.isArray(patterns) ? patterns : [patterns];
2503
2818
  if (p.length === 0) return false;
@@ -2508,9 +2823,21 @@ function matchesPattern(text, patterns) {
2508
2823
  const withoutDotSlash = text.replace(/^\.\//, "");
2509
2824
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2510
2825
  }
2511
- function getNestedValue(obj, path27) {
2826
+ function mcpServerFromToolName(toolName) {
2827
+ const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
2828
+ return match?.[1];
2829
+ }
2830
+ function ruleServerMatches(rule, serverLabel) {
2831
+ const label2 = typeof serverLabel === "string" ? serverLabel.trim().replace(/[\\/]/g, "-") || void 0 : void 0;
2832
+ if (rule.server) {
2833
+ if (!label2 || !matchesPattern(label2, rule.server)) return false;
2834
+ }
2835
+ if (rule.notServer && label2 && matchesPattern(label2, rule.notServer)) return false;
2836
+ return true;
2837
+ }
2838
+ function getNestedValue(obj, path28) {
2512
2839
  if (!obj || typeof obj !== "object") return null;
2513
- const segments2 = path27.split(".");
2840
+ const segments2 = path28.split(".");
2514
2841
  for (const seg of segments2) {
2515
2842
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2516
2843
  }
@@ -2530,23 +2857,26 @@ function evaluateSmartConditions(args, rule) {
2530
2857
  return val;
2531
2858
  };
2532
2859
  const readingsCache = /* @__PURE__ */ new Map();
2533
- const resolveFieldReadings = (field) => {
2534
- const cached = readingsCache.get(field);
2860
+ const resolveFieldReadings = (field, reading) => {
2861
+ const access = reading === "access" && field === "command";
2862
+ const key = access ? `${field}\0access` : field;
2863
+ const cached = readingsCache.get(key);
2535
2864
  if (cached) return cached;
2536
2865
  const primary = resolveField(field);
2537
2866
  if (primary === null) {
2538
- readingsCache.set(field, []);
2867
+ readingsCache.set(key, []);
2539
2868
  return [];
2540
2869
  }
2541
2870
  let out = [primary];
2542
2871
  if (field === "command") {
2543
2872
  const raw = getNestedValue(args, field);
2544
2873
  if (typeof raw === "string") {
2545
- const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
2546
- out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2874
+ const text = access ? fileAccessView(raw) : raw;
2875
+ const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
2876
+ out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2547
2877
  }
2548
2878
  }
2549
- readingsCache.set(field, out);
2879
+ readingsCache.set(key, out);
2550
2880
  return out;
2551
2881
  };
2552
2882
  const results = rule.conditions.map((cond) => {
@@ -2564,14 +2894,14 @@ function evaluateSmartConditions(args, rule) {
2564
2894
  if (val === null || !cond.value) return false;
2565
2895
  const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2566
2896
  if (!reM) return false;
2567
- return resolveFieldReadings(cond.field).some((v) => reM.test(v));
2897
+ return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
2568
2898
  }
2569
2899
  case "notMatches": {
2570
2900
  if (!cond.value) return false;
2571
2901
  if (val === null) return true;
2572
2902
  const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2573
2903
  if (!reN) return false;
2574
- return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
2904
+ return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
2575
2905
  }
2576
2906
  case "matchesGlob":
2577
2907
  return val !== null && cond.value ? import_picomatch2.default.isMatch(val, cond.value) : false;
@@ -3413,35 +3743,6 @@ function collectShellFileAccesses(command, context = {}) {
3413
3743
  }
3414
3744
  return out;
3415
3745
  }
3416
- function isAgentInstructionPath(absPath, root) {
3417
- const r = root.replace(/\/$/, "");
3418
- if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
3419
- const rel = absPath.slice(r.length + 1);
3420
- if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
3421
- return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
3422
- }
3423
- function isInside(root, p) {
3424
- const r = root.replace(/\/$/, "") || "/";
3425
- return p === r || p.startsWith(r === "/" ? "/" : r + "/");
3426
- }
3427
- function isPasswordHashPath(p) {
3428
- return PASSWORD_HASH_FILES.includes(p);
3429
- }
3430
- function isLegacySystemPath(p) {
3431
- return LEGACY_SYSTEM_PATH_RE.test(p);
3432
- }
3433
- function normalizeFilePath(p, cwd = "", home = "~") {
3434
- if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
3435
- if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
3436
- const rooted = p.startsWith("/");
3437
- const parts = [];
3438
- for (const part of p.split("/")) {
3439
- if (!part || part === ".") continue;
3440
- if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
3441
- else if (part !== ".." || !rooted) parts.push(part);
3442
- }
3443
- return (rooted ? "/" : "") + parts.join("/");
3444
- }
3445
3746
  function incompleteFileVerdict(operation = "write") {
3446
3747
  return {
3447
3748
  decision: "review",
@@ -3493,8 +3794,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
3493
3794
  }
3494
3795
  return out;
3495
3796
  }
3496
- function within(path27, root) {
3497
- return path27 === root || path27.startsWith(root === "/" ? "/" : root + "/");
3797
+ function within(path28, root) {
3798
+ return path28 === root || path28.startsWith(root === "/" ? "/" : root + "/");
3498
3799
  }
3499
3800
  function hits(access, paths, directories, context) {
3500
3801
  const home = context.homeDirs?.[0] ?? "~";
@@ -4302,14 +4603,14 @@ function sensitivePathMatch(originalPath) {
4302
4603
  }
4303
4604
  function assertBuiltinPatternsAreSafe() {
4304
4605
  for (const p of DLP_PATTERNS) {
4305
- if (!(0, import_safe_regex23.default)(p.regex.source)) {
4606
+ if (!(0, import_safe_regex22.default)(p.regex.source)) {
4306
4607
  throw new Error(
4307
4608
  `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
4308
4609
  );
4309
4610
  }
4310
4611
  }
4311
4612
  for (const re of SENSITIVE_PATH_PATTERNS) {
4312
- if (!(0, import_safe_regex23.default)(re.source)) {
4613
+ if (!(0, import_safe_regex22.default)(re.source)) {
4313
4614
  throw new Error(
4314
4615
  `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
4315
4616
  );
@@ -5433,9 +5734,9 @@ function bareToolName(toolName) {
5433
5734
  const parts = toolName.split("__");
5434
5735
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
5435
5736
  }
5436
- function valuesAt(args, path27) {
5737
+ function valuesAt(args, path28) {
5437
5738
  let cursors = [args];
5438
- for (const rawSegment of path27.split(".")) {
5739
+ for (const rawSegment of path28.split(".")) {
5439
5740
  const isArray = rawSegment.endsWith("[]");
5440
5741
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
5441
5742
  const next = [];
@@ -5466,8 +5767,8 @@ function extractToolDestinations(toolName, args) {
5466
5767
  try {
5467
5768
  const out = [];
5468
5769
  const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
5469
- for (const path27 of paths) {
5470
- for (const value of valuesAt(args, path27)) {
5770
+ for (const path28 of paths) {
5771
+ for (const value of valuesAt(args, path28)) {
5471
5772
  const host2 = hostOf(value);
5472
5773
  if (host2) out.push({ host: host2, binary: toolName, raw: value });
5473
5774
  }
@@ -5493,8 +5794,8 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5493
5794
  try {
5494
5795
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
5495
5796
  if (!paths) return null;
5496
- for (const path27 of paths) {
5497
- for (const value of valuesAt(args, path27)) {
5797
+ for (const path28 of paths) {
5798
+ for (const value of valuesAt(args, path28)) {
5498
5799
  const host2 = hostOf(value);
5499
5800
  if (!host2) continue;
5500
5801
  const m = classifySsrf(host2);
@@ -5503,7 +5804,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5503
5804
  const decision = ssrfFloorDecision(m, written, opts);
5504
5805
  if (decision === null) continue;
5505
5806
  const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
5506
- return { ...m, argPath: path27, host: host2, reason, decision };
5807
+ return { ...m, argPath: path28, host: host2, reason, decision };
5507
5808
  }
5508
5809
  }
5509
5810
  return null;
@@ -5883,11 +6184,15 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5883
6184
  };
5884
6185
  const ruleCheck = (rule) => {
5885
6186
  if (rule.verdict === "allow") return void 0;
5886
- if (!rule.builtin || rule.pinned) return void 0;
6187
+ if (!rule.builtin) return void 0;
5887
6188
  const id = checkIdForRule(rule.name);
5888
6189
  if (!id) return void 0;
5889
6190
  const def = getCheck(id);
5890
6191
  if (!def) return void 0;
6192
+ if (rule.pinned) {
6193
+ const explicit = config.policy.checks?.[id];
6194
+ return def.pack && config.policy.checkSources?.[id] === "workspace" && isVerdict(explicit) ? { id, value: explicit } : void 0;
6195
+ }
5891
6196
  if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
5892
6197
  if (def.pack) {
5893
6198
  const explicit = config.policy.checks?.[id];
@@ -5895,8 +6200,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5895
6200
  }
5896
6201
  return { id, value: cv(id) };
5897
6202
  };
6203
+ const serverLabel = context.mcpServer ?? mcpServerFromToolName(toolName);
5898
6204
  const matches = config.policy.smartRules.filter((rule) => {
5899
6205
  if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
6206
+ if (!ruleServerMatches(rule, serverLabel)) return false;
5900
6207
  if (astSuppressed(rule)) return false;
5901
6208
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
5902
6209
  if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
@@ -5973,11 +6280,13 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5973
6280
  if (v?.logged) noteLogged(v);
5974
6281
  else if (v) candidates.push(v);
5975
6282
  };
5976
- if (detectInlineExec(shellCommand)) {
6283
+ const inline = config.settings.mode === "strict" ? detectInlineExec(shellCommand) ? void 0 : null : inlineExecReview(shellCommand);
6284
+ if (inline !== null) {
5977
6285
  consider("commands.inline-exec", {
5978
6286
  decision: "review",
5979
6287
  blockedByLabel: "Node9 Standard (Inline Execution)",
5980
- ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
6288
+ ...inline ? { reason: inlineExecReason(inline) } : {},
6289
+ ruleDescription: inlineExecDescription(inline),
5981
6290
  tier: 3,
5982
6291
  checkId: "commands.inline-exec"
5983
6292
  });
@@ -7067,7 +7376,7 @@ function matchCanaryArgs(args, values) {
7067
7376
  return null;
7068
7377
  }
7069
7378
  }
7070
- var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, import_mvdan_sh4, import_crypto5, 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;
7379
+ var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_mvdan_sh2, import_safe_regex22, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, import_mvdan_sh4, import_crypto5, 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;
7071
7380
  var init_dist = __esm({
7072
7381
  "packages/policy-engine/dist/index.mjs"() {
7073
7382
  "use strict";
@@ -7075,15 +7384,17 @@ var init_dist = __esm({
7075
7384
  import_safe_regex2 = __toESM(require("safe-regex2"), 1);
7076
7385
  import_picomatch2 = __toESM(require("picomatch"), 1);
7077
7386
  import_mvdan_sh = __toESM(require("mvdan-sh"), 1);
7078
- import_safe_regex22 = __toESM(require("safe-regex2"), 1);
7079
7387
  import_mvdan_sh2 = __toESM(require("mvdan-sh"), 1);
7080
- import_safe_regex23 = __toESM(require("safe-regex2"), 1);
7388
+ import_safe_regex22 = __toESM(require("safe-regex2"), 1);
7081
7389
  import_crypto2 = require("crypto");
7082
7390
  import_crypto3 = require("crypto");
7083
7391
  import_mvdan_sh3 = __toESM(require("mvdan-sh"), 1);
7084
7392
  import_crypto4 = require("crypto");
7085
7393
  import_mvdan_sh4 = __toESM(require("mvdan-sh"), 1);
7086
7394
  import_crypto5 = __toESM(require("crypto"), 1);
7395
+ MAX_REGEX_LENGTH = 256;
7396
+ REGEX_CACHE_MAX = 500;
7397
+ regexCache = /* @__PURE__ */ new Map();
7087
7398
  aws_default = {
7088
7399
  name: "aws",
7089
7400
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -7385,81 +7696,10 @@ var init_dist = __esm({
7385
7696
  ],
7386
7697
  dangerousWords: ["wipefs"]
7387
7698
  };
7388
- finance_default = {
7389
- name: "finance",
7390
- 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.",
7391
- aliases: ["payments", "money"],
7392
- smartRules: [
7393
- {
7394
- name: "shield:finance:review-refund",
7395
- tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
7396
- conditions: [],
7397
- verdict: "review",
7398
- reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
7399
- },
7400
- {
7401
- name: "shield:finance:review-payout",
7402
- tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
7403
- conditions: [],
7404
- verdict: "review",
7405
- reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
7406
- },
7407
- {
7408
- name: "shield:finance:review-transfer",
7409
- 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}",
7410
- conditions: [],
7411
- verdict: "review",
7412
- reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
7413
- },
7414
- {
7415
- name: "shield:finance:review-charge",
7416
- 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}",
7417
- conditions: [],
7418
- verdict: "review",
7419
- reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
7420
- },
7421
- {
7422
- name: "shield:finance:review-payment-link",
7423
- tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
7424
- conditions: [],
7425
- verdict: "review",
7426
- reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
7427
- },
7428
- {
7429
- name: "shield:finance:review-subscription",
7430
- tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
7431
- conditions: [],
7432
- verdict: "review",
7433
- reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
7434
- },
7435
- {
7436
- name: "shield:finance:review-dispute",
7437
- tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
7438
- conditions: [],
7439
- verdict: "review",
7440
- reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
7441
- },
7442
- {
7443
- name: "shield:finance:review-card",
7444
- tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
7445
- conditions: [],
7446
- verdict: "review",
7447
- reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
7448
- },
7449
- {
7450
- name: "shield:finance:review-withdraw",
7451
- tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
7452
- conditions: [],
7453
- verdict: "review",
7454
- reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
7455
- }
7456
- ],
7457
- dangerousWords: []
7458
- };
7459
- github_default = {
7460
- name: "github",
7461
- description: "Protects GitHub repositories from destructive AI operations",
7462
- aliases: ["git"],
7699
+ github_default = {
7700
+ name: "github",
7701
+ description: "Protects GitHub repositories from destructive AI operations",
7702
+ aliases: ["git"],
7463
7703
  smartRules: [
7464
7704
  {
7465
7705
  name: "shield:github:review-delete-branch-remote",
@@ -7482,56 +7722,6 @@ var init_dist = __esm({
7482
7722
  ],
7483
7723
  dangerousWords: []
7484
7724
  };
7485
- identity_default = {
7486
- name: "identity",
7487
- 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.",
7488
- aliases: ["access", "iam"],
7489
- smartRules: [
7490
- {
7491
- name: "shield:identity:review-password",
7492
- tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
7493
- conditions: [],
7494
- verdict: "review",
7495
- reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
7496
- },
7497
- {
7498
- name: "shield:identity:review-key",
7499
- 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}",
7500
- conditions: [],
7501
- verdict: "review",
7502
- reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
7503
- },
7504
- {
7505
- name: "shield:identity:review-access",
7506
- 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}",
7507
- conditions: [],
7508
- verdict: "review",
7509
- reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
7510
- },
7511
- {
7512
- name: "shield:identity:review-membership",
7513
- 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}",
7514
- conditions: [],
7515
- verdict: "review",
7516
- reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
7517
- },
7518
- {
7519
- name: "shield:identity:review-user",
7520
- tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
7521
- conditions: [],
7522
- verdict: "review",
7523
- reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
7524
- },
7525
- {
7526
- name: "shield:identity:review-mfa",
7527
- tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
7528
- conditions: [],
7529
- verdict: "review",
7530
- reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
7531
- }
7532
- ],
7533
- dangerousWords: []
7534
- };
7535
7725
  k8s_default = {
7536
7726
  name: "k8s",
7537
7727
  description: "Protects Kubernetes clusters from destructive AI operations",
@@ -7784,7 +7974,7 @@ var init_dist = __esm({
7784
7974
  {
7785
7975
  field: "command",
7786
7976
  op: "matches",
7787
- 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|$|[;&|>)<])",
7977
+ 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;&|>)<]|$)",
7788
7978
  flags: "i"
7789
7979
  }
7790
7980
  ],
@@ -7798,7 +7988,7 @@ var init_dist = __esm({
7798
7988
  {
7799
7989
  field: "command",
7800
7990
  op: "matches",
7801
- 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)",
7991
+ 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)",
7802
7992
  flags: "i"
7803
7993
  }
7804
7994
  ],
@@ -7848,18 +8038,218 @@ var init_dist = __esm({
7848
8038
  reason: "Reading .env files is blocked by project-jail shield"
7849
8039
  },
7850
8040
  {
7851
- name: "shield:project-jail:review-read-credentials-any-tool",
7852
- tool: "*",
7853
- conditions: [
7854
- {
7855
- field: "file_path",
7856
- op: "matches",
7857
- value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
7858
- flags: "i"
7859
- }
7860
- ],
8041
+ name: "shield:project-jail:review-read-credentials-any-tool",
8042
+ tool: "*",
8043
+ conditions: [
8044
+ {
8045
+ field: "file_path",
8046
+ op: "matches",
8047
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
8048
+ flags: "i"
8049
+ }
8050
+ ],
8051
+ verdict: "review",
8052
+ reason: "Reading credential files requires approval (project-jail shield)"
8053
+ }
8054
+ ],
8055
+ dangerousWords: []
8056
+ };
8057
+ redis_default = {
8058
+ name: "redis",
8059
+ description: "Protects Redis instances from destructive AI operations",
8060
+ aliases: [],
8061
+ _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.",
8062
+ smartRules: [
8063
+ {
8064
+ name: "shield:redis:block-flushall",
8065
+ tool: "*",
8066
+ conditions: [
8067
+ {
8068
+ field: "command",
8069
+ op: "matches",
8070
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
8071
+ flags: "i"
8072
+ }
8073
+ ],
8074
+ verdict: "block",
8075
+ reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
8076
+ },
8077
+ {
8078
+ name: "shield:redis:block-flushdb",
8079
+ tool: "*",
8080
+ conditions: [
8081
+ {
8082
+ field: "command",
8083
+ op: "matches",
8084
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
8085
+ flags: "i"
8086
+ }
8087
+ ],
8088
+ verdict: "block",
8089
+ reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
8090
+ },
8091
+ {
8092
+ name: "shield:redis:block-config-resetstat",
8093
+ tool: "*",
8094
+ conditions: [
8095
+ {
8096
+ field: "command",
8097
+ op: "matches",
8098
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
8099
+ flags: "i"
8100
+ }
8101
+ ],
8102
+ verdict: "block",
8103
+ reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
8104
+ },
8105
+ {
8106
+ name: "shield:redis:review-config-set",
8107
+ tool: "*",
8108
+ conditions: [
8109
+ {
8110
+ field: "command",
8111
+ op: "matches",
8112
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
8113
+ flags: "i"
8114
+ }
8115
+ ],
8116
+ verdict: "review",
8117
+ reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
8118
+ },
8119
+ {
8120
+ name: "shield:redis:review-del-wildcard",
8121
+ tool: "*",
8122
+ conditions: [
8123
+ {
8124
+ field: "command",
8125
+ op: "matches",
8126
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
8127
+ flags: "i"
8128
+ }
8129
+ ],
8130
+ verdict: "review",
8131
+ reason: "Wildcard key deletion requires human approval (Redis shield)"
8132
+ }
8133
+ ],
8134
+ dangerousWords: ["FLUSHALL", "FLUSHDB"]
8135
+ };
8136
+ finance_default = {
8137
+ name: "finance",
8138
+ 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.",
8139
+ aliases: ["payments", "money"],
8140
+ smartRules: [
8141
+ {
8142
+ name: "shield:finance:review-refund",
8143
+ tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
8144
+ conditions: [],
8145
+ verdict: "review",
8146
+ reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
8147
+ },
8148
+ {
8149
+ name: "shield:finance:review-payout",
8150
+ tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
8151
+ conditions: [],
8152
+ verdict: "review",
8153
+ reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
8154
+ },
8155
+ {
8156
+ name: "shield:finance:review-transfer",
8157
+ tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
8158
+ conditions: [],
8159
+ verdict: "review",
8160
+ reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
8161
+ },
8162
+ {
8163
+ name: "shield:finance:review-charge",
8164
+ 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}",
8165
+ conditions: [],
8166
+ verdict: "review",
8167
+ reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
8168
+ },
8169
+ {
8170
+ name: "shield:finance:review-payment-link",
8171
+ tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
8172
+ conditions: [],
8173
+ verdict: "review",
8174
+ reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
8175
+ },
8176
+ {
8177
+ name: "shield:finance:review-subscription",
8178
+ tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
8179
+ conditions: [],
8180
+ verdict: "review",
8181
+ reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
8182
+ },
8183
+ {
8184
+ name: "shield:finance:review-dispute",
8185
+ tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
8186
+ conditions: [],
8187
+ verdict: "review",
8188
+ reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
8189
+ },
8190
+ {
8191
+ name: "shield:finance:review-card",
8192
+ tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
8193
+ conditions: [],
8194
+ verdict: "review",
8195
+ reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
8196
+ },
8197
+ {
8198
+ name: "shield:finance:review-withdraw",
8199
+ tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
8200
+ conditions: [],
8201
+ verdict: "review",
8202
+ reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
8203
+ }
8204
+ ],
8205
+ dangerousWords: []
8206
+ };
8207
+ identity_default = {
8208
+ name: "identity",
8209
+ 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.",
8210
+ aliases: ["access", "iam"],
8211
+ smartRules: [
8212
+ {
8213
+ name: "shield:identity:review-password",
8214
+ tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
8215
+ conditions: [],
8216
+ verdict: "review",
8217
+ reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
8218
+ },
8219
+ {
8220
+ name: "shield:identity:review-key",
8221
+ 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}",
8222
+ conditions: [],
8223
+ verdict: "review",
8224
+ reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
8225
+ },
8226
+ {
8227
+ name: "shield:identity:review-access",
8228
+ 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}",
8229
+ conditions: [],
8230
+ verdict: "review",
8231
+ reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
8232
+ },
8233
+ {
8234
+ name: "shield:identity:review-membership",
8235
+ 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}",
8236
+ conditions: [],
8237
+ verdict: "review",
8238
+ reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
8239
+ },
8240
+ {
8241
+ name: "shield:identity:review-user",
8242
+ 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}",
8243
+ conditions: [],
7861
8244
  verdict: "review",
7862
- reason: "Reading credential files requires approval (project-jail shield)"
8245
+ reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
8246
+ },
8247
+ {
8248
+ name: "shield:identity:review-mfa",
8249
+ tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
8250
+ conditions: [],
8251
+ verdict: "review",
8252
+ reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
7863
8253
  }
7864
8254
  ],
7865
8255
  dangerousWords: []
@@ -8033,103 +8423,510 @@ var init_dist = __esm({
8033
8423
  ],
8034
8424
  dangerousWords: []
8035
8425
  };
8036
- redis_default = {
8037
- name: "redis",
8038
- description: "Protects Redis instances from destructive AI operations",
8039
- aliases: [],
8040
- _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.",
8041
- smartRules: [
8042
- {
8043
- name: "shield:redis:block-flushall",
8044
- tool: "*",
8045
- conditions: [
8046
- {
8047
- field: "command",
8048
- op: "matches",
8049
- value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
8050
- flags: "i"
8051
- }
8052
- ],
8053
- verdict: "block",
8054
- reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
8055
- },
8056
- {
8057
- name: "shield:redis:block-flushdb",
8058
- tool: "*",
8059
- conditions: [
8060
- {
8061
- field: "command",
8062
- op: "matches",
8063
- value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
8064
- flags: "i"
8065
- }
8066
- ],
8067
- verdict: "block",
8068
- reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
8069
- },
8070
- {
8071
- name: "shield:redis:block-config-resetstat",
8072
- tool: "*",
8073
- conditions: [
8074
- {
8075
- field: "command",
8076
- op: "matches",
8077
- value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
8078
- flags: "i"
8079
- }
8080
- ],
8081
- verdict: "block",
8082
- reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
8083
- },
8084
- {
8085
- name: "shield:redis:review-config-set",
8086
- tool: "*",
8087
- conditions: [
8088
- {
8089
- field: "command",
8090
- op: "matches",
8091
- value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
8092
- flags: "i"
8093
- }
8094
- ],
8095
- verdict: "review",
8096
- reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
8097
- },
8098
- {
8099
- name: "shield:redis:review-del-wildcard",
8100
- tool: "*",
8101
- conditions: [
8102
- {
8103
- field: "command",
8104
- op: "matches",
8105
- value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
8106
- flags: "i"
8107
- }
8108
- ],
8109
- verdict: "review",
8110
- reason: "Wildcard key deletion requires human approval (Redis shield)"
8111
- }
8112
- ],
8113
- dangerousWords: ["FLUSHALL", "FLUSHDB"]
8426
+ ACTION_TEMPLATES = {
8427
+ finance: finance_default.smartRules,
8428
+ identity: identity_default.smartRules,
8429
+ publishing: publishing_default.smartRules
8430
+ };
8431
+ ACTION_CHECKS = {
8432
+ finance: { checkId: "actions.money", slug: "money", label: "Money check" },
8433
+ identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
8434
+ publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
8435
+ };
8436
+ GAP = "[^;&|]{0,200}?";
8437
+ prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
8438
+ httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
8439
+ httpWrite = (methods) => ({
8440
+ field: "command",
8441
+ op: "matches",
8442
+ 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)`
8443
+ });
8444
+ HTTP_WRITE = httpWrite("POST|DELETE");
8445
+ HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
8446
+ NOT_STRIPE_TEST = {
8447
+ field: "command",
8448
+ op: "notMatches",
8449
+ value: "\\b[sr]k_test_"
8114
8450
  };
8451
+ postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
8452
+ STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
8453
+ APP_SHIELD_SPECS = [
8454
+ {
8455
+ name: "stripe",
8456
+ label: "Stripe",
8457
+ 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.",
8458
+ template: "finance",
8459
+ server: "*stripe*",
8460
+ doors: [
8461
+ {
8462
+ name: "shield:stripe:review-cli",
8463
+ tool: "bash",
8464
+ conditions: [
8465
+ {
8466
+ field: "command",
8467
+ op: "matches",
8468
+ value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
8469
+ flags: "i"
8470
+ },
8471
+ {
8472
+ field: "command",
8473
+ op: "matches",
8474
+ // The stripe CLI works in test mode unless told otherwise.
8475
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8476
+ }
8477
+ ],
8478
+ conditionMode: "all",
8479
+ verdict: "review",
8480
+ reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
8481
+ },
8482
+ {
8483
+ name: "shield:stripe:review-cli-request",
8484
+ tool: "bash",
8485
+ conditions: [
8486
+ {
8487
+ field: "command",
8488
+ op: "matches",
8489
+ value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
8490
+ flags: "i"
8491
+ },
8492
+ {
8493
+ field: "command",
8494
+ op: "matches",
8495
+ // The stripe CLI works in test mode unless told otherwise.
8496
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8497
+ }
8498
+ ],
8499
+ conditionMode: "all",
8500
+ verdict: "review",
8501
+ reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
8502
+ },
8503
+ {
8504
+ name: "shield:stripe:review-api",
8505
+ tool: "bash",
8506
+ conditions: [
8507
+ {
8508
+ field: "command",
8509
+ op: "matches",
8510
+ value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
8511
+ flags: "i"
8512
+ },
8513
+ HTTP_WRITE,
8514
+ NOT_STRIPE_TEST
8515
+ ],
8516
+ conditionMode: "all",
8517
+ verdict: "review",
8518
+ reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
8519
+ },
8520
+ {
8521
+ name: "shield:stripe:review-code",
8522
+ tool: "bash",
8523
+ conditions: [
8524
+ {
8525
+ field: "command",
8526
+ op: "matches",
8527
+ // Python: stripe.Refund.create(...)
8528
+ value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
8529
+ },
8530
+ NOT_STRIPE_TEST
8531
+ ],
8532
+ conditionMode: "all",
8533
+ verdict: "review",
8534
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8535
+ },
8536
+ {
8537
+ name: "shield:stripe:review-code-node",
8538
+ tool: "bash",
8539
+ conditions: [
8540
+ {
8541
+ field: "command",
8542
+ op: "matches",
8543
+ // Node: stripe.refunds.create(...)
8544
+ value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
8545
+ },
8546
+ NOT_STRIPE_TEST
8547
+ ],
8548
+ conditionMode: "all",
8549
+ verdict: "review",
8550
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8551
+ },
8552
+ {
8553
+ name: "shield:stripe:review-code-http",
8554
+ tool: "bash",
8555
+ conditions: [
8556
+ {
8557
+ field: "command",
8558
+ op: "matches",
8559
+ // requests.post("https://api.stripe.com/v1/refunds", ...)
8560
+ value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
8561
+ },
8562
+ NOT_STRIPE_TEST
8563
+ ],
8564
+ conditionMode: "all",
8565
+ verdict: "review",
8566
+ reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
8567
+ }
8568
+ ]
8569
+ },
8570
+ {
8571
+ name: "paypal",
8572
+ label: "PayPal",
8573
+ 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.",
8574
+ template: "finance",
8575
+ server: "*paypal*",
8576
+ doors: [
8577
+ {
8578
+ name: "shield:paypal:review-api",
8579
+ tool: "bash",
8580
+ conditions: [
8581
+ {
8582
+ field: "command",
8583
+ op: "matches",
8584
+ // The live hosts only: api.paypal.com and api-m.paypal.com. The
8585
+ // sandbox (api-m.sandbox.paypal.com) moves no money.
8586
+ value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
8587
+ flags: "i"
8588
+ },
8589
+ {
8590
+ field: "command",
8591
+ op: "matches",
8592
+ // The endpoints that move money.
8593
+ value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
8594
+ flags: "i"
8595
+ },
8596
+ HTTP_WRITE
8597
+ ],
8598
+ conditionMode: "all",
8599
+ verdict: "review",
8600
+ reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
8601
+ },
8602
+ {
8603
+ name: "shield:paypal:review-code",
8604
+ tool: "bash",
8605
+ conditions: [
8606
+ {
8607
+ field: "command",
8608
+ op: "matches",
8609
+ // The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
8610
+ // Python paypalrestsdk.
8611
+ value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
8612
+ }
8613
+ ],
8614
+ verdict: "review",
8615
+ reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
8616
+ },
8617
+ {
8618
+ name: "shield:paypal:review-code-http",
8619
+ tool: "bash",
8620
+ conditions: [
8621
+ {
8622
+ field: "command",
8623
+ op: "matches",
8624
+ // The endpoints that move money; the token call every script
8625
+ // starts with (/v1/oauth2/token) is not one.
8626
+ value: postTo(
8627
+ "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)"
8628
+ )
8629
+ }
8630
+ ],
8631
+ verdict: "review",
8632
+ reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
8633
+ }
8634
+ ]
8635
+ },
8636
+ {
8637
+ name: "square",
8638
+ label: "Square",
8639
+ 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.",
8640
+ template: "finance",
8641
+ // Square's servers, never Squarespace (a website builder).
8642
+ server: "{*square,*square[-_]*,*squareup*}",
8643
+ mcpTools: [
8644
+ {
8645
+ // Square's official MCP server has one tool for every API call; the
8646
+ // service and method name what it does.
8647
+ name: "shield:square:review-mcp-request",
8648
+ tool: "{,mcp__*__}make_api_request",
8649
+ conditions: [
8650
+ {
8651
+ field: "service",
8652
+ op: "matches",
8653
+ value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
8654
+ flags: "i"
8655
+ },
8656
+ {
8657
+ field: "method",
8658
+ op: "notMatches",
8659
+ value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
8660
+ flags: "i"
8661
+ }
8662
+ ],
8663
+ conditionMode: "all",
8664
+ verdict: "review",
8665
+ reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
8666
+ }
8667
+ ],
8668
+ doors: [
8669
+ {
8670
+ name: "shield:square:review-api",
8671
+ tool: "bash",
8672
+ conditions: [
8673
+ {
8674
+ field: "command",
8675
+ op: "matches",
8676
+ // The live host only; the sandbox is connect.squareupsandbox.com.
8677
+ value: httpTo(
8678
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8679
+ ),
8680
+ flags: "i"
8681
+ },
8682
+ HTTP_WRITE_ANY
8683
+ ],
8684
+ conditionMode: "all",
8685
+ verdict: "review",
8686
+ reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
8687
+ },
8688
+ {
8689
+ name: "shield:square:review-code",
8690
+ tool: "bash",
8691
+ conditions: [
8692
+ {
8693
+ field: "command",
8694
+ op: "matches",
8695
+ // The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
8696
+ value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
8697
+ },
8698
+ {
8699
+ field: "command",
8700
+ op: "matches",
8701
+ 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*\\("
8702
+ }
8703
+ ],
8704
+ conditionMode: "all",
8705
+ verdict: "review",
8706
+ reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
8707
+ },
8708
+ {
8709
+ name: "shield:square:review-code-http",
8710
+ tool: "bash",
8711
+ conditions: [
8712
+ {
8713
+ field: "command",
8714
+ op: "matches",
8715
+ value: postTo(
8716
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8717
+ )
8718
+ }
8719
+ ],
8720
+ verdict: "review",
8721
+ reason: "This code posts to the Square API. node9 asks before money moves (Square)."
8722
+ }
8723
+ ]
8724
+ },
8725
+ {
8726
+ name: "plaid",
8727
+ label: "Plaid",
8728
+ 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.",
8729
+ template: "finance",
8730
+ server: "*plaid*",
8731
+ doors: [
8732
+ {
8733
+ name: "shield:plaid:review-api",
8734
+ tool: "bash",
8735
+ conditions: [
8736
+ {
8737
+ field: "command",
8738
+ op: "matches",
8739
+ // Every Plaid endpoint is a POST, so the path is what tells a
8740
+ // write; the live host only, never sandbox.plaid.com. A script
8741
+ // that posts to the URL meets this door too.
8742
+ value: httpTo(
8743
+ "production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
8744
+ ),
8745
+ flags: "i"
8746
+ }
8747
+ ],
8748
+ verdict: "review",
8749
+ reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
8750
+ },
8751
+ {
8752
+ name: "shield:plaid:review-code",
8753
+ tool: "bash",
8754
+ conditions: [
8755
+ { field: "command", op: "matches", value: "\\bplaid", flags: "i" },
8756
+ {
8757
+ field: "command",
8758
+ op: "matches",
8759
+ value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
8760
+ }
8761
+ ],
8762
+ conditionMode: "all",
8763
+ verdict: "review",
8764
+ reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
8765
+ }
8766
+ ]
8767
+ },
8768
+ {
8769
+ name: "okta",
8770
+ label: "Okta",
8771
+ 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.",
8772
+ template: "identity",
8773
+ server: "*okta*",
8774
+ doors: [
8775
+ {
8776
+ name: "shield:okta:review-api",
8777
+ tool: "bash",
8778
+ conditions: [
8779
+ {
8780
+ field: "command",
8781
+ op: "matches",
8782
+ // The management API only: sign-in (/api/v1/authn, /sessions,
8783
+ // /oauth2) is not under these paths.
8784
+ value: httpTo(
8785
+ "[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
8786
+ ),
8787
+ flags: "i"
8788
+ },
8789
+ HTTP_WRITE_ANY
8790
+ ],
8791
+ conditionMode: "all",
8792
+ verdict: "review",
8793
+ reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
8794
+ },
8795
+ {
8796
+ name: "shield:okta:review-code",
8797
+ tool: "bash",
8798
+ conditions: [
8799
+ {
8800
+ field: "command",
8801
+ op: "matches",
8802
+ // The Okta SDK in the script: a method named createUser alone is
8803
+ // in half the applications ever written.
8804
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8805
+ },
8806
+ {
8807
+ field: "command",
8808
+ op: "matches",
8809
+ value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
8810
+ }
8811
+ ],
8812
+ conditionMode: "all",
8813
+ verdict: "review",
8814
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8815
+ },
8816
+ {
8817
+ name: "shield:okta:review-code-py",
8818
+ tool: "bash",
8819
+ conditions: [
8820
+ {
8821
+ field: "command",
8822
+ op: "matches",
8823
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8824
+ },
8825
+ {
8826
+ field: "command",
8827
+ op: "matches",
8828
+ 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*\\("
8829
+ }
8830
+ ],
8831
+ conditionMode: "all",
8832
+ verdict: "review",
8833
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8834
+ }
8835
+ ]
8836
+ },
8837
+ {
8838
+ name: "auth0",
8839
+ label: "Auth0",
8840
+ 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.",
8841
+ template: "identity",
8842
+ server: "*auth0*",
8843
+ mcpTools: [
8844
+ {
8845
+ // Auth0's official MCP server: applications (their secrets and
8846
+ // callbacks), APIs and their scopes, and Actions (code that runs at
8847
+ // every sign-in).
8848
+ name: "shield:auth0:review-tenant-config",
8849
+ 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}",
8850
+ conditions: [],
8851
+ verdict: "review",
8852
+ reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
8853
+ }
8854
+ ],
8855
+ doors: [
8856
+ {
8857
+ name: "shield:auth0:review-cli",
8858
+ tool: "bash",
8859
+ conditions: [
8860
+ {
8861
+ field: "command",
8862
+ op: "matches",
8863
+ 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-])",
8864
+ flags: "i"
8865
+ }
8866
+ ],
8867
+ verdict: "review",
8868
+ reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
8869
+ },
8870
+ {
8871
+ name: "shield:auth0:review-api",
8872
+ tool: "bash",
8873
+ conditions: [
8874
+ {
8875
+ field: "command",
8876
+ op: "matches",
8877
+ // The Management API only; /oauth/token is signing in.
8878
+ value: httpTo(
8879
+ "\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
8880
+ ),
8881
+ flags: "i"
8882
+ },
8883
+ HTTP_WRITE_ANY
8884
+ ],
8885
+ conditionMode: "all",
8886
+ verdict: "review",
8887
+ reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
8888
+ },
8889
+ {
8890
+ name: "shield:auth0:review-code",
8891
+ tool: "bash",
8892
+ conditions: [
8893
+ {
8894
+ field: "command",
8895
+ op: "matches",
8896
+ // The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
8897
+ value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
8898
+ },
8899
+ {
8900
+ field: "command",
8901
+ op: "matches",
8902
+ 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*\\("
8903
+ }
8904
+ ],
8905
+ conditionMode: "all",
8906
+ verdict: "review",
8907
+ reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
8908
+ }
8909
+ ]
8910
+ }
8911
+ ];
8912
+ APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
8913
+ APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
8914
+ APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
8915
+ GENERAL_ACTION_RULES = buildGeneralRules();
8115
8916
  BUILTIN_SHIELDS = {
8116
8917
  [aws_default.name]: aws_default,
8117
8918
  [bash_safe_default.name]: bash_safe_default,
8118
8919
  [docker_default.name]: docker_default,
8119
8920
  [filesystem_default.name]: filesystem_default,
8120
- // Dangerous actions as category shields (node9Firewall#385): an MCP app's
8121
- // money, credential and publishing tools, asked about whatever the request
8122
- // said. One glob per rule covers the bare name the gateway sends and the
8123
- // mcp__<server>__ name the agent hook sends.
8124
- [finance_default.name]: finance_default,
8125
8921
  [github_default.name]: github_default,
8126
- [identity_default.name]: identity_default,
8127
8922
  [k8s_default.name]: k8s_default,
8128
8923
  [mongodb_default.name]: mongodb_default,
8129
8924
  [postgres_default.name]: postgres_default,
8130
8925
  [project_jail_default.name]: project_jail_default,
8131
- [publishing_default.name]: publishing_default,
8132
- [redis_default.name]: redis_default
8926
+ [redis_default.name]: redis_default,
8927
+ // App shields generated from the action templates (node9Firewall#390): one
8928
+ // per app, every door (MCP server, CLI, API, library), off by default.
8929
+ ...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
8133
8930
  };
8134
8931
  assertBuiltinShieldRegexesAreSafe();
8135
8932
  SSRF_MAX_HOST = 253;
@@ -8185,9 +8982,9 @@ var init_dist = __esm({
8185
8982
  },
8186
8983
  // ── Commands ──────────────────────────────────────────────────────────────
8187
8984
  "commands.inline-exec": {
8188
- plain: "The agent runs a small program written inside the command itself.",
8985
+ 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.",
8189
8986
  example: `node -e "require('fs').rmSync('dist', {recursive: true})"`,
8190
- 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."
8987
+ advice: "Keep Review. Strict mode reviews every inline call, harmless or not; Log or Off only if inline code never reaches anything that matters."
8191
8988
  },
8192
8989
  "commands.eval-dynamic": {
8193
8990
  plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
@@ -8219,6 +9016,21 @@ var init_dist = __esm({
8219
9016
  example: "chmod 777 deploy.sh",
8220
9017
  advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
8221
9018
  },
9019
+ "actions.money": {
9020
+ 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.",
9021
+ example: "refund_order on a server called acme-billing",
9022
+ 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."
9023
+ },
9024
+ "actions.access": {
9025
+ plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
9026
+ example: "invite_user or grant_role on a company admin server",
9027
+ advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
9028
+ },
9029
+ "actions.publish": {
9030
+ 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.",
9031
+ example: "create_release or deploy_to_production on a company MCP server",
9032
+ advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
9033
+ },
8222
9034
  "commands.sudo": {
8223
9035
  plain: "The agent runs a command as the administrator (root).",
8224
9036
  example: "sudo apt-get install -y nginx",
@@ -8379,7 +9191,7 @@ var init_dist = __esm({
8379
9191
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
8380
9192
  },
8381
9193
  "loading.malicious-package": {
8382
- 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.",
9194
+ 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.",
8383
9195
  example: "npm install of a package flagged as malware",
8384
9196
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
8385
9197
  }
@@ -8431,7 +9243,7 @@ var init_dist = __esm({
8431
9243
  id: "commands.inline-exec",
8432
9244
  group: "commands",
8433
9245
  title: "Inline code execution",
8434
- catches: "node -e, python -c, bash -c, a heredoc into an interpreter",
9246
+ 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",
8435
9247
  values: ALL,
8436
9248
  defaultValue: "review",
8437
9249
  legacy: ["Node9 Standard (Inline Execution)"]
@@ -8788,6 +9600,38 @@ var init_dist = __esm({
8788
9600
  values: ALL,
8789
9601
  defaultValue: "block",
8790
9602
  legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
9603
+ },
9604
+ // ── Actions in apps (node9Firewall#390) ──────────────────────────────────
9605
+ // The safety net under the app shields: an action that moves money, changes
9606
+ // access or publishes, on an MCP server no app shield claims (an internal
9607
+ // billing server, a vendor with no shield, Zapier). Review by default; the
9608
+ // rules come from the action templates (shields/apps.ts).
9609
+ {
9610
+ id: "actions.money",
9611
+ group: "actions",
9612
+ title: "Money moves",
9613
+ catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
9614
+ values: ALL,
9615
+ defaultValue: "review",
9616
+ legacy: generalActionRuleNames("finance")
9617
+ },
9618
+ {
9619
+ id: "actions.access",
9620
+ group: "actions",
9621
+ title: "Accounts and access change",
9622
+ catches: "a password, key, role, permission or user changed through an app with no protection of its own",
9623
+ values: ALL,
9624
+ defaultValue: "review",
9625
+ legacy: generalActionRuleNames("identity")
9626
+ },
9627
+ {
9628
+ id: "actions.publish",
9629
+ group: "actions",
9630
+ title: "Something is published",
9631
+ catches: "a release, package, deploy or public post through an app with no protection of its own",
9632
+ values: ALL,
9633
+ defaultValue: "review",
9634
+ legacy: generalActionRuleNames("publishing")
8791
9635
  }
8792
9636
  ];
8793
9637
  SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
@@ -8910,12 +9754,223 @@ var init_dist = __esm({
8910
9754
  \u03A5: "Y",
8911
9755
  \u03A7: "X"
8912
9756
  };
8913
- HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
8914
- MAX_BLOBS = 32;
8915
- MIN_DECODED = 16;
8916
- BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
8917
- HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
8918
- HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
9757
+ HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
9758
+ MAX_BLOBS = 32;
9759
+ MIN_DECODED = 16;
9760
+ BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
9761
+ HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
9762
+ HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
9763
+ SYSTEM_AUTH_FILES = [
9764
+ "/etc/passwd",
9765
+ "/etc/shadow",
9766
+ "/etc/group",
9767
+ "/etc/gshadow",
9768
+ "/etc/sudoers"
9769
+ ];
9770
+ SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9771
+ PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9772
+ STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9773
+ SYSTEM_DIRS = [
9774
+ "/usr",
9775
+ "/etc",
9776
+ "/boot",
9777
+ "/bin",
9778
+ "/sbin",
9779
+ "/lib",
9780
+ "/lib64",
9781
+ "/var/log"
9782
+ ];
9783
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9784
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9785
+ AGENT_INSTRUCTION_FILES = [
9786
+ ".gitlab-ci.yml",
9787
+ "CLAUDE.md",
9788
+ "AGENTS.md",
9789
+ ".cursorrules",
9790
+ ".mcp.json",
9791
+ ".vscode/settings.json",
9792
+ "package.json"
9793
+ ];
9794
+ AGENT_INSTRUCTION_DIRS = [
9795
+ ".github/workflows",
9796
+ ".claude",
9797
+ ".cursor",
9798
+ ".git/hooks",
9799
+ ".husky"
9800
+ ];
9801
+ LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9802
+ MAX_INLINE_CODE = 64 * 1024;
9803
+ WHAT = {
9804
+ deletes: "deletes files",
9805
+ "writes-outside": "writes a file outside the project",
9806
+ sends: "sends data over the network",
9807
+ listens: "opens a listening socket or a shell",
9808
+ "runs-commands": "runs other commands or processes",
9809
+ "reads-protected": "reads a protected file",
9810
+ "builds-code": "runs code it builds at run time",
9811
+ permissions: "changes permissions or ownership",
9812
+ monitors: "captures the screen, the clipboard or the keyboard",
9813
+ amplifies: "writes or sends in an endless loop"
9814
+ };
9815
+ OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
9816
+ pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
9817
+ either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
9818
+ PYTHON = [
9819
+ [
9820
+ "deletes",
9821
+ /\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/
9822
+ ],
9823
+ [
9824
+ "writes-outside",
9825
+ new RegExp(
9826
+ 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}`
9827
+ )
9828
+ ],
9829
+ [
9830
+ "sends",
9831
+ either(
9832
+ pyImport(
9833
+ String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
9834
+ ),
9835
+ /\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
9836
+ )
9837
+ ],
9838
+ [
9839
+ "listens",
9840
+ either(
9841
+ pyImport(String.raw`pty|socketserver|http\.server`),
9842
+ /\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
9843
+ )
9844
+ ],
9845
+ [
9846
+ "runs-commands",
9847
+ either(
9848
+ pyImport("subprocess|pexpect|commands|psutil"),
9849
+ /\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+\*/
9850
+ )
9851
+ ],
9852
+ [
9853
+ "builds-code",
9854
+ /(?<![\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*\[/
9855
+ ],
9856
+ [
9857
+ // Spyware shapes: a screenshot, the clipboard, a key logger.
9858
+ "monitors",
9859
+ either(
9860
+ pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
9861
+ /\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
9862
+ )
9863
+ ],
9864
+ [
9865
+ "permissions",
9866
+ /\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
9867
+ ]
9868
+ ];
9869
+ NODE = [
9870
+ [
9871
+ "deletes",
9872
+ /\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
9873
+ ],
9874
+ [
9875
+ "writes-outside",
9876
+ new RegExp(
9877
+ 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}`
9878
+ )
9879
+ ],
9880
+ [
9881
+ "sends",
9882
+ /\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*\(/
9883
+ ],
9884
+ ["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
9885
+ [
9886
+ "runs-commands",
9887
+ /\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
9888
+ ],
9889
+ [
9890
+ "builds-code",
9891
+ /(?<![\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)]/
9892
+ ],
9893
+ [
9894
+ "permissions",
9895
+ /\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
9896
+ ]
9897
+ ];
9898
+ PERL = [
9899
+ ["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
9900
+ [
9901
+ "writes-outside",
9902
+ new RegExp(
9903
+ String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
9904
+ )
9905
+ ],
9906
+ [
9907
+ "sends",
9908
+ /\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
9909
+ ],
9910
+ [
9911
+ "runs-commands",
9912
+ /\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
9913
+ ],
9914
+ ["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
9915
+ ["permissions", /\b(?:chmod|chown)\b/]
9916
+ ];
9917
+ RUBY = [
9918
+ [
9919
+ "deletes",
9920
+ /\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
9921
+ ],
9922
+ [
9923
+ "writes-outside",
9924
+ new RegExp(
9925
+ 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}`
9926
+ )
9927
+ ],
9928
+ [
9929
+ "sends",
9930
+ /\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)['"]/
9931
+ ],
9932
+ ["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
9933
+ [
9934
+ "runs-commands",
9935
+ /(?<![\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/
9936
+ ],
9937
+ [
9938
+ "builds-code",
9939
+ /(?<![\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/
9940
+ ],
9941
+ ["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
9942
+ ];
9943
+ PHP = [
9944
+ ["deletes", /\b(?:unlink|rmdir)\s*\(/],
9945
+ [
9946
+ "writes-outside",
9947
+ new RegExp(
9948
+ String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
9949
+ )
9950
+ ],
9951
+ [
9952
+ "sends",
9953
+ /\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):/
9954
+ ],
9955
+ ["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
9956
+ [
9957
+ "runs-commands",
9958
+ /\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
9959
+ ],
9960
+ [
9961
+ "builds-code",
9962
+ /\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
9963
+ ],
9964
+ ["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
9965
+ ];
9966
+ TABLES = {
9967
+ python: PYTHON,
9968
+ node: NODE,
9969
+ perl: PERL,
9970
+ ruby: RUBY,
9971
+ php: PHP
9972
+ };
9973
+ STRING_LITERAL = /(['"`])((?:\\.|(?!\1)[^\\\n]){1,4096})\1/g;
8919
9974
  ({ syntax } = import_mvdan_sh.default);
8920
9975
  sharedParser = syntax.NewParser();
8921
9976
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -8953,6 +10008,10 @@ var init_dist = __esm({
8953
10008
  AST_CACHE_MAX = 5e3;
8954
10009
  astCache = /* @__PURE__ */ new Map();
8955
10010
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
10011
+ INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
10012
+ PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
10013
+ ACCESS_VIEW_CACHE_MAX = 1e3;
10014
+ accessViewCache = /* @__PURE__ */ new Map();
8956
10015
  MAX_READING_PAYLOADS = 8;
8957
10016
  defaultExpansionOps = null;
8958
10017
  VARIABLE_READERS = /* @__PURE__ */ new Set([
@@ -9662,6 +10721,14 @@ var init_dist = __esm({
9662
10721
  INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
9663
10722
  MAX_PAYLOAD_NESTING = 8;
9664
10723
  payloadNesting = 0;
10724
+ MAX_INLINE_CALLS = 16;
10725
+ 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;
10726
+ UNREADABLE_SOURCE = {
10727
+ pipe: "reads its code from another program through a pipe",
10728
+ file: "reads its code from a file on stdin",
10729
+ variable: "runs code the shell fills in at run time",
10730
+ payload: "runs a shell payload that evaluates, sources or runs further code"
10731
+ };
9665
10732
  SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
9666
10733
  SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
9667
10734
  MAX_SCRIPT_OPERANDS = 8;
@@ -9861,9 +10928,6 @@ var init_dist = __esm({
9861
10928
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
9862
10929
  NONE = { kind: "none" };
9863
10930
  UNKNOWN = { kind: "unknown" };
9864
- MAX_REGEX_LENGTH = 256;
9865
- REGEX_CACHE_MAX = 500;
9866
- regexCache = /* @__PURE__ */ new Map();
9867
10931
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
9868
10932
  BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
9869
10933
  "bash",
@@ -9978,45 +11042,6 @@ var init_dist = __esm({
9978
11042
  (s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
9979
11043
  )
9980
11044
  );
9981
- SYSTEM_AUTH_FILES = [
9982
- "/etc/passwd",
9983
- "/etc/shadow",
9984
- "/etc/group",
9985
- "/etc/gshadow",
9986
- "/etc/sudoers"
9987
- ];
9988
- SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9989
- PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9990
- STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9991
- SYSTEM_DIRS = [
9992
- "/usr",
9993
- "/etc",
9994
- "/boot",
9995
- "/bin",
9996
- "/sbin",
9997
- "/lib",
9998
- "/lib64",
9999
- "/var/log"
10000
- ];
10001
- SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
10002
- UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
10003
- AGENT_INSTRUCTION_FILES = [
10004
- ".gitlab-ci.yml",
10005
- "CLAUDE.md",
10006
- "AGENTS.md",
10007
- ".cursorrules",
10008
- ".mcp.json",
10009
- ".vscode/settings.json",
10010
- "package.json"
10011
- ];
10012
- AGENT_INSTRUCTION_DIRS = [
10013
- ".github/workflows",
10014
- ".claude",
10015
- ".cursor",
10016
- ".git/hooks",
10017
- ".husky"
10018
- ];
10019
- LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
10020
11045
  exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
10021
11046
  RANK = { allow: 0, review: 1, block: 2 };
10022
11047
  BIP39_ENGLISH = `abandon
@@ -13509,7 +14534,11 @@ zoo`.split("\n");
13509
14534
  "commands.disk-destroy",
13510
14535
  "commands.dangerous-word",
13511
14536
  "commands.node9-disarm",
13512
- "data.pipe-chain"
14537
+ "data.pipe-chain",
14538
+ // The general action checks (node9Firewall#390): smart rules the map governs.
14539
+ "actions.money",
14540
+ "actions.access",
14541
+ "actions.publish"
13513
14542
  ]);
13514
14543
  KNOB_GOVERNED = {
13515
14544
  "data.secrets": ["off", "block"],
@@ -14224,7 +15253,8 @@ var SmartConditionSchema = import_zod.z.object({
14224
15253
  }
14225
15254
  ),
14226
15255
  value: import_zod.z.string().optional(),
14227
- flags: import_zod.z.string().optional()
15256
+ flags: import_zod.z.string().optional(),
15257
+ reading: import_zod.z.enum(["access"]).optional()
14228
15258
  }).refine(
14229
15259
  (c) => {
14230
15260
  if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
@@ -14235,6 +15265,9 @@ var SmartConditionSchema = import_zod.z.object({
14235
15265
  var SmartRuleSchema = import_zod.z.object({
14236
15266
  name: import_zod.z.string().optional(),
14237
15267
  tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
15268
+ // Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
15269
+ server: import_zod.z.string().min(1).optional(),
15270
+ notServer: import_zod.z.string().min(1).optional(),
14238
15271
  conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
14239
15272
  conditionMode: import_zod.z.enum(["all", "any"]).optional(),
14240
15273
  verdict: import_zod.z.enum(["allow", "review", "block"], {
@@ -14384,8 +15417,8 @@ var ConfigFileSchema = import_zod.z.object({
14384
15417
  }).strict();
14385
15418
  function formatIssues(issues) {
14386
15419
  const lines = issues.map((issue) => {
14387
- const path27 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
14388
- return ` \u2022 ${path27}: ${issue.message}`;
15420
+ const path28 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
15421
+ return ` \u2022 ${path28}: ${issue.message}`;
14389
15422
  });
14390
15423
  return `Invalid config:
14391
15424
  ${lines.join("\n")}`;
@@ -14398,9 +15431,9 @@ function prunePaths(root, paths) {
14398
15431
  const yi = y[y.length - 1];
14399
15432
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
14400
15433
  });
14401
- for (const path27 of ordered) {
15434
+ for (const path28 of ordered) {
14402
15435
  let cur = root;
14403
- for (const key of path27.slice(0, -1)) {
15436
+ for (const key of path28.slice(0, -1)) {
14404
15437
  if (cur === null || typeof cur !== "object") {
14405
15438
  cur = void 0;
14406
15439
  break;
@@ -14408,7 +15441,7 @@ function prunePaths(root, paths) {
14408
15441
  cur = cur[key];
14409
15442
  }
14410
15443
  if (cur === null || typeof cur !== "object") continue;
14411
- const last = path27[path27.length - 1];
15444
+ const last = path28[path28.length - 1];
14412
15445
  if (Array.isArray(cur)) {
14413
15446
  const i = Number(last);
14414
15447
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -14444,7 +15477,7 @@ function sanitizeConfig(raw) {
14444
15477
  return issue.keys.map((k) => [...at, k]);
14445
15478
  }
14446
15479
  return [at.slice(0, -level || void 0)];
14447
- }).filter((path27) => path27.length > 0);
15480
+ }).filter((path28) => path28.length > 0);
14448
15481
  if (paths.length === 0 || !prunePaths(working, paths)) break;
14449
15482
  }
14450
15483
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -14470,6 +15503,85 @@ var import_fs2 = __toESM(require("fs"));
14470
15503
  var import_path2 = __toESM(require("path"));
14471
15504
  var import_os2 = __toESM(require("os"));
14472
15505
  init_dist();
15506
+
15507
+ // src/shields/build.ts
15508
+ init_dist();
15509
+ function escapeRegex(s) {
15510
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15511
+ }
15512
+ function slug(s) {
15513
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
15514
+ }
15515
+ var B = "[\\s/\\\\]";
15516
+ var SEP = "[/\\\\]";
15517
+ function pathToRegexFragment(rawPath) {
15518
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
15519
+ const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
15520
+ if (segments2.length === 0) return "";
15521
+ return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
15522
+ }
15523
+ function pathMatchesFragment(candidate, rawPath) {
15524
+ const value = pathToRegexFragment(rawPath);
15525
+ if (!value || !candidate) return false;
15526
+ const re = getCompiledRegex(value);
15527
+ if (!re) return false;
15528
+ return re.test(candidate);
15529
+ }
15530
+ function pathRules(rawPath, verdict, reason) {
15531
+ const value = pathToRegexFragment(rawPath);
15532
+ if (!value) return [];
15533
+ const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
15534
+ const s = slug(rawPath);
15535
+ return [
15536
+ // `reading: 'access'`: a script's interpreter line and the program a
15537
+ // command runs are not reads of the jailed path (node9Firewall#385).
15538
+ {
15539
+ name: `${verdict}-path-${s}-bash`,
15540
+ tool: "bash",
15541
+ conditions: [{ field: "command", op: "matches", value, reading: "access" }],
15542
+ verdict,
15543
+ reason: why
15544
+ },
15545
+ // Keep the historical `-anytool` name for the file_path rule: the
15546
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
15547
+ {
15548
+ name: `${verdict}-path-${s}-anytool`,
15549
+ tool: "*",
15550
+ conditions: [{ field: "file_path", op: "matches", value }],
15551
+ verdict,
15552
+ reason: why
15553
+ },
15554
+ {
15555
+ name: `${verdict}-path-${s}-anytool-path`,
15556
+ tool: "*",
15557
+ conditions: [{ field: "path", op: "matches", value }],
15558
+ verdict,
15559
+ reason: why
15560
+ },
15561
+ {
15562
+ name: `${verdict}-path-${s}-anytool-pattern`,
15563
+ tool: "*",
15564
+ conditions: [{ field: "pattern", op: "matches", value }],
15565
+ verdict,
15566
+ reason: why
15567
+ }
15568
+ ];
15569
+ }
15570
+ var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
15571
+ function withAccessReading(def) {
15572
+ let changed = false;
15573
+ const smartRules = def.smartRules.map((r) => {
15574
+ const [c, ...rest] = r.conditions ?? [];
15575
+ 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) {
15576
+ return r;
15577
+ }
15578
+ changed = true;
15579
+ return { ...r, conditions: [{ ...c, reading: "access" }] };
15580
+ });
15581
+ return changed ? { ...def, smartRules } : def;
15582
+ }
15583
+
15584
+ // src/shields.ts
14473
15585
  init_dist();
14474
15586
  var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
14475
15587
  function loadUserShields() {
@@ -14496,7 +15608,7 @@ function loadUserShields() {
14496
15608
  `);
14497
15609
  continue;
14498
15610
  }
14499
- result[v.ok.name] = v.ok;
15611
+ result[v.ok.name] = withAccessReading(v.ok);
14500
15612
  } catch (err) {
14501
15613
  process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
14502
15614
  `);
@@ -14648,90 +15760,28 @@ var CLASS_B_COMMAND_CHECKS = /* @__PURE__ */ new Set([
14648
15760
  "evalDynamic",
14649
15761
  "pipeChainHigh"
14650
15762
  ]);
14651
- function applyManagedCommandChecks(local, managed, locked) {
14652
- const next = { ...local };
14653
- for (const key of COMMAND_CHECK_KEYS) {
14654
- const m = managed[key];
14655
- if (typeof m !== "string") continue;
14656
- const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
14657
- const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
14658
- locked: locked.includes(lockKey)
14659
- });
14660
- if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
14661
- next[key] = resolved;
14662
- }
14663
- return next;
14664
- }
14665
- function applyManagedApprovers(local, managed) {
14666
- return {
14667
- ...local,
14668
- native: typeof managed.native === "boolean" ? managed.native : local.native,
14669
- browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
14670
- cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
14671
- terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
14672
- };
14673
- }
14674
-
14675
- // src/shields/build.ts
14676
- init_dist();
14677
- function escapeRegex(s) {
14678
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14679
- }
14680
- function slug(s) {
14681
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
14682
- }
14683
- var B = "[\\s/\\\\]";
14684
- var SEP = "[/\\\\]";
14685
- function pathToRegexFragment(rawPath) {
14686
- const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
14687
- const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
14688
- if (segments2.length === 0) return "";
14689
- return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
14690
- }
14691
- function pathMatchesFragment(candidate, rawPath) {
14692
- const value = pathToRegexFragment(rawPath);
14693
- if (!value || !candidate) return false;
14694
- const re = getCompiledRegex(value);
14695
- if (!re) return false;
14696
- return re.test(candidate);
14697
- }
14698
- function pathRules(rawPath, verdict, reason) {
14699
- const value = pathToRegexFragment(rawPath);
14700
- if (!value) return [];
14701
- const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
14702
- const s = slug(rawPath);
14703
- return [
14704
- {
14705
- name: `${verdict}-path-${s}-bash`,
14706
- tool: "bash",
14707
- conditions: [{ field: "command", op: "matches", value }],
14708
- verdict,
14709
- reason: why
14710
- },
14711
- // Keep the historical `-anytool` name for the file_path rule: the
14712
- // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
14713
- {
14714
- name: `${verdict}-path-${s}-anytool`,
14715
- tool: "*",
14716
- conditions: [{ field: "file_path", op: "matches", value }],
14717
- verdict,
14718
- reason: why
14719
- },
14720
- {
14721
- name: `${verdict}-path-${s}-anytool-path`,
14722
- tool: "*",
14723
- conditions: [{ field: "path", op: "matches", value }],
14724
- verdict,
14725
- reason: why
14726
- },
14727
- {
14728
- name: `${verdict}-path-${s}-anytool-pattern`,
14729
- tool: "*",
14730
- conditions: [{ field: "pattern", op: "matches", value }],
14731
- verdict,
14732
- reason: why
14733
- }
14734
- ];
15763
+ function applyManagedCommandChecks(local, managed, locked) {
15764
+ const next = { ...local };
15765
+ for (const key of COMMAND_CHECK_KEYS) {
15766
+ const m = managed[key];
15767
+ if (typeof m !== "string") continue;
15768
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
15769
+ const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
15770
+ locked: locked.includes(lockKey)
15771
+ });
15772
+ if (CLASS_B_COMMAND_CHECKS.has(key) && resolved === "off") continue;
15773
+ next[key] = resolved;
15774
+ }
15775
+ return next;
15776
+ }
15777
+ function applyManagedApprovers(local, managed) {
15778
+ return {
15779
+ ...local,
15780
+ native: typeof managed.native === "boolean" ? managed.native : local.native,
15781
+ browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
15782
+ cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
15783
+ terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
15784
+ };
14735
15785
  }
14736
15786
 
14737
15787
  // src/auth/trusted-hosts.ts
@@ -14792,6 +15842,7 @@ function isTrustedHost(host2) {
14792
15842
 
14793
15843
  // src/config/index.ts
14794
15844
  init_dist();
15845
+ init_dist();
14795
15846
 
14796
15847
  // src/config/v2.ts
14797
15848
  init_dist();
@@ -15104,7 +16155,9 @@ var DEFAULT_CONFIG = {
15104
16155
  op: "matches",
15105
16156
  // Single spaces on purpose: a nested `\\s+` inside the alternation
15106
16157
  // fails the engine's ReDoS check and the rule would be dropped.
15107
- value: "(^|&&|\\|\\||;)\\s*(npx )?node9 (session-(exposure|taint) clear|trust (add|recipient add)|pause|shield (disable|set)|egress allow|approver set)\\b",
16158
+ // `apps`: turning an app off, allowing one of its actions, or
16159
+ // linking a server to an app (the general checks leave it).
16160
+ 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",
15108
16161
  flags: "i"
15109
16162
  }
15110
16163
  ],
@@ -15190,7 +16243,12 @@ var DEFAULT_CONFIG = {
15190
16243
  verdict: "block",
15191
16244
  reason: "Piping remote script into a shell is a supply-chain attack vector",
15192
16245
  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."
15193
- }
16246
+ },
16247
+ // ── Actions in apps (node9Firewall#390) ───────────────────────────────
16248
+ // actions.money / actions.access / actions.publish: the action templates
16249
+ // on every MCP server no app shield claims, plus the shell publishing
16250
+ // rules. Review by default; the check value governs them.
16251
+ ...GENERAL_ACTION_RULES
15194
16252
  ],
15195
16253
  dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
15196
16254
  egress: {
@@ -15227,6 +16285,7 @@ var DEFAULT_CONFIG = {
15227
16285
  trustedHosts: [],
15228
16286
  trustedHostsManaged: false,
15229
16287
  appPermissions: {},
16288
+ appLinks: {},
15230
16289
  managedJailPaths: []
15231
16290
  },
15232
16291
  environments: {}
@@ -15461,6 +16520,7 @@ function getConfig(cwd) {
15461
16520
  trustedHosts: [],
15462
16521
  trustedHostsManaged: false,
15463
16522
  appPermissions: {},
16523
+ appLinks: {},
15464
16524
  managedJailPaths: []
15465
16525
  };
15466
16526
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
@@ -15845,19 +16905,26 @@ function getConfig(cwd) {
15845
16905
  }
15846
16906
  if (Array.isArray(mc.jailPaths)) {
15847
16907
  for (const jp of mc.jailPaths) {
15848
- const path27 = typeof jp?.path === "string" ? jp.path.trim() : "";
15849
- if (!path27) continue;
16908
+ const path28 = typeof jp?.path === "string" ? jp.path.trim() : "";
16909
+ if (!path28) continue;
15850
16910
  const verdict = jp?.verdict === "review" ? "review" : "block";
15851
- for (const r of pathRules(path27, verdict, "org-managed jail")) {
16911
+ for (const r of pathRules(path28, verdict, "org-managed jail")) {
15852
16912
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
15853
16913
  }
15854
- mergedPolicy.managedJailPaths.push({ path: path27, verdict });
16914
+ mergedPolicy.managedJailPaths.push({ path: path28, verdict });
15855
16915
  }
15856
16916
  }
15857
16917
  if (Array.isArray(mc.trustedHosts)) {
15858
16918
  mergedPolicy.trustedHostsManaged = true;
15859
16919
  mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
15860
16920
  }
16921
+ if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
16922
+ const links = {};
16923
+ for (const [key, slug2] of Object.entries(mc.appLinks)) {
16924
+ if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
16925
+ }
16926
+ mergedPolicy.appLinks = links;
16927
+ }
15861
16928
  if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
15862
16929
  const coerced = {};
15863
16930
  for (const [srv, tools] of Object.entries(mc.appPermissions)) {
@@ -16403,7 +17470,8 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
16403
17470
  cwd: prepared.cwd,
16404
17471
  activeEnvironment,
16405
17472
  skipIgnoredFastPath: opts?.skipIgnoredFastPath,
16406
- argumentScan: opts?.argumentScan
17473
+ argumentScan: opts?.argumentScan,
17474
+ mcpServer: opts?.mcpServer
16407
17475
  },
16408
17476
  {
16409
17477
  checkProvenance,
@@ -17071,14 +18139,272 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
17071
18139
 
17072
18140
  // src/auth/orchestrator.ts
17073
18141
  var import_crypto10 = require("crypto");
17074
- var import_os19 = __toESM(require("os"));
18142
+
18143
+ // src/apps/state.ts
18144
+ var import_fs12 = __toESM(require("fs"));
18145
+ var import_os10 = __toESM(require("os"));
18146
+ var import_path12 = __toESM(require("path"));
18147
+
18148
+ // src/apps/catalog.ts
18149
+ var APP_CATALOG = [
18150
+ {
18151
+ slug: "redis",
18152
+ label: "Redis",
18153
+ shield: "redis",
18154
+ servers: ["redis", "valkey"],
18155
+ commands: ["redis-cli", "valkey-cli"],
18156
+ packages: ["redis", "ioredis", "valkey"],
18157
+ envPrefixes: ["REDIS_"]
18158
+ },
18159
+ {
18160
+ slug: "postgres",
18161
+ label: "PostgreSQL",
18162
+ shield: "postgres",
18163
+ servers: ["postgres", "pgsql"],
18164
+ commands: ["psql", "pg_dump", "pg_restore", "pgcli"],
18165
+ packages: ["pg", "postgres", "psycopg", "psycopg2", "psycopg2-binary", "asyncpg"],
18166
+ envPrefixes: ["PGHOST", "PGDATABASE", "POSTGRES_"]
18167
+ },
18168
+ {
18169
+ slug: "mongodb",
18170
+ label: "MongoDB",
18171
+ shield: "mongodb",
18172
+ servers: ["mongo"],
18173
+ commands: ["mongosh", "mongo", "mongodump", "mongorestore"],
18174
+ packages: ["mongodb", "mongoose", "pymongo", "motor"],
18175
+ envPrefixes: ["MONGO"]
18176
+ },
18177
+ {
18178
+ slug: "github",
18179
+ label: "GitHub",
18180
+ shield: "github",
18181
+ servers: ["github"],
18182
+ commands: ["gh"],
18183
+ packages: ["@octokit/", "octokit", "pygithub"],
18184
+ envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"]
18185
+ },
18186
+ {
18187
+ slug: "aws",
18188
+ label: "AWS",
18189
+ shield: "aws",
18190
+ servers: ["aws"],
18191
+ commands: ["aws", "sam", "cdk"],
18192
+ packages: ["aws-sdk", "@aws-sdk/", "boto3", "aws-cdk-lib"],
18193
+ envPrefixes: ["AWS_ACCESS_KEY_ID", "AWS_PROFILE"]
18194
+ },
18195
+ {
18196
+ slug: "k8s",
18197
+ label: "Kubernetes",
18198
+ shield: "k8s",
18199
+ servers: ["kubernetes", "k8s", "kubectl"],
18200
+ commands: ["kubectl", "helm", "k9s"],
18201
+ packages: ["@kubernetes/client-node", "kubernetes"],
18202
+ envPrefixes: ["KUBECONFIG"]
18203
+ },
18204
+ {
18205
+ slug: "docker",
18206
+ label: "Docker",
18207
+ shield: "docker",
18208
+ servers: ["docker"],
18209
+ commands: ["docker", "docker-compose", "podman"],
18210
+ packages: ["dockerode", "docker"],
18211
+ envPrefixes: ["DOCKER_HOST"]
18212
+ },
18213
+ {
18214
+ slug: "stripe",
18215
+ label: "Stripe",
18216
+ shield: "stripe",
18217
+ servers: ["stripe"],
18218
+ commands: ["stripe"],
18219
+ packages: ["stripe", "@stripe/"],
18220
+ envPrefixes: ["STRIPE_"]
18221
+ },
18222
+ {
18223
+ slug: "paypal",
18224
+ label: "PayPal",
18225
+ shield: "paypal",
18226
+ servers: ["paypal"],
18227
+ commands: [],
18228
+ packages: ["@paypal/", "paypalrestsdk", "paypal-checkout-serversdk", "paypal-server-sdk"],
18229
+ envPrefixes: ["PAYPAL_"]
18230
+ },
18231
+ {
18232
+ slug: "square",
18233
+ label: "Square",
18234
+ shield: "square",
18235
+ servers: ["square"],
18236
+ // Squarespace is a website builder. SQUARE_ already misses SQUARESPACE_.
18237
+ notServers: ["squarespace"],
18238
+ commands: [],
18239
+ packages: ["square", "squareup"],
18240
+ envPrefixes: ["SQUARE_"]
18241
+ },
18242
+ {
18243
+ slug: "plaid",
18244
+ label: "Plaid",
18245
+ shield: "plaid",
18246
+ servers: ["plaid"],
18247
+ commands: [],
18248
+ packages: ["plaid", "plaid-python"],
18249
+ envPrefixes: ["PLAID_"]
18250
+ },
18251
+ {
18252
+ slug: "okta",
18253
+ label: "Okta",
18254
+ shield: "okta",
18255
+ servers: ["okta"],
18256
+ commands: ["okta"],
18257
+ packages: ["@okta/", "okta"],
18258
+ envPrefixes: ["OKTA_"]
18259
+ },
18260
+ {
18261
+ slug: "auth0",
18262
+ label: "Auth0",
18263
+ shield: "auth0",
18264
+ servers: ["auth0"],
18265
+ commands: ["auth0"],
18266
+ packages: ["auth0", "@auth0/"],
18267
+ envPrefixes: ["AUTH0_"]
18268
+ }
18269
+ ];
18270
+ var APP_BY_SLUG = new Map(
18271
+ APP_CATALOG.map((a) => [a.slug, a])
18272
+ );
18273
+
18274
+ // src/apps/state.ts
18275
+ init_audit();
18276
+
18277
+ // src/daemon/mcp-tools.ts
18278
+ var import_fs11 = __toESM(require("fs"));
18279
+ var import_path11 = __toESM(require("path"));
18280
+ var import_os9 = __toESM(require("os"));
18281
+ function getMcpToolsFile() {
18282
+ return import_path11.default.join(import_os9.default.homedir(), ".node9", "mcp-tools.json");
18283
+ }
18284
+ function readMcpToolsConfig() {
18285
+ try {
18286
+ const file2 = getMcpToolsFile();
18287
+ if (!import_fs11.default.existsSync(file2)) return {};
18288
+ const raw = import_fs11.default.readFileSync(file2, "utf-8");
18289
+ return JSON.parse(raw);
18290
+ } catch {
18291
+ return {};
18292
+ }
18293
+ }
18294
+
18295
+ // src/apps/state.ts
18296
+ function appsFilePath(home = import_os10.default.homedir()) {
18297
+ return import_path12.default.join(home, ".node9", "apps.json");
18298
+ }
18299
+ var SIGNAL_KINDS = /* @__PURE__ */ new Set(["mcp-server", "command", "package", "env-name"]);
18300
+ function emptyFile() {
18301
+ return { version: 1, apps: /* @__PURE__ */ Object.create(null), links: /* @__PURE__ */ Object.create(null) };
18302
+ }
18303
+ function ownString(map, key) {
18304
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return void 0;
18305
+ const v = map[key];
18306
+ return typeof v === "string" ? v : void 0;
18307
+ }
18308
+ function readSignals(raw) {
18309
+ if (!Array.isArray(raw)) return [];
18310
+ const out = [];
18311
+ for (const x of raw) {
18312
+ if (!x || typeof x.kind !== "string" || typeof x.value !== "string") continue;
18313
+ if (!SIGNAL_KINDS.has(x.kind)) continue;
18314
+ out.push({
18315
+ kind: x.kind,
18316
+ value: x.value.slice(0, 256),
18317
+ ...typeof x.project === "string" ? { project: x.project } : {}
18318
+ });
18319
+ }
18320
+ return out;
18321
+ }
18322
+ function parseAppsFile(text) {
18323
+ const raw = JSON.parse(text);
18324
+ const file2 = emptyFile();
18325
+ for (const [slug2, r] of Object.entries(raw.apps ?? {})) {
18326
+ if (!APP_BY_SLUG.has(slug2) || !r || r.state !== "on" && r.state !== "off") continue;
18327
+ file2.apps[slug2] = {
18328
+ state: r.state,
18329
+ by: r.by === "user" || r.by === "workspace" ? r.by : "detected",
18330
+ at: typeof r.at === "string" ? r.at : "",
18331
+ signals: readSignals(r.signals)
18332
+ };
18333
+ }
18334
+ for (const [server, slug2] of Object.entries(raw.links ?? {})) {
18335
+ if (typeof slug2 === "string" && APP_BY_SLUG.has(slug2)) file2.links[server.toLowerCase()] = slug2;
18336
+ }
18337
+ return file2;
18338
+ }
18339
+ function readAppsFile(home) {
18340
+ return readAppsFileStrict(home) ?? emptyFile();
18341
+ }
18342
+ function readAppsFileStrict(home) {
18343
+ let text;
18344
+ try {
18345
+ text = import_fs12.default.readFileSync(appsFilePath(home), "utf8");
18346
+ } catch (e) {
18347
+ return e.code === "ENOENT" ? emptyFile() : null;
18348
+ }
18349
+ try {
18350
+ return parseAppsFile(text);
18351
+ } catch {
18352
+ return null;
18353
+ }
18354
+ }
18355
+ var linkCache = null;
18356
+ var NO_LINKS = /* @__PURE__ */ Object.create(null);
18357
+ function localLinks() {
18358
+ const file2 = appsFilePath();
18359
+ try {
18360
+ const mtimeMs = import_fs12.default.statSync(file2).mtimeMs;
18361
+ if (!linkCache || linkCache.file !== file2 || linkCache.mtimeMs !== mtimeMs) {
18362
+ linkCache = { mtimeMs, file: file2, links: readAppsFile().links };
18363
+ }
18364
+ return linkCache.links;
18365
+ } catch {
18366
+ return NO_LINKS;
18367
+ }
18368
+ }
18369
+ function resolveLinkedServer(label2, opts = {}) {
18370
+ if (typeof label2 !== "string" || !label2) return label2;
18371
+ const applied = new Set(opts.appliedShields ?? []);
18372
+ const live = (slug2) => {
18373
+ const app = slug2 ? APP_BY_SLUG.get(slug2) : void 0;
18374
+ return app && applied.has(app.shield) ? slug2 : void 0;
18375
+ };
18376
+ const lower2 = label2.toLowerCase();
18377
+ if (!opts.workspaceGoverned) {
18378
+ const local = live(ownString(localLinks(), lower2));
18379
+ if (local) return local;
18380
+ }
18381
+ const managed = opts.managedLinks ?? {};
18382
+ if (opts.serverKey) {
18383
+ const byKey = live(ownString(managed, opts.serverKey));
18384
+ if (byKey) return byKey;
18385
+ }
18386
+ const keys = Object.keys(managed);
18387
+ if (keys.length === 0) return label2;
18388
+ const tools = readMcpToolsConfig();
18389
+ for (const key of keys) {
18390
+ const name = key.startsWith("cfg:") ? key.split(":").slice(2).join(":") : Object.prototype.hasOwnProperty.call(tools, key) ? tools[key]?.name : void 0;
18391
+ if (typeof name === "string" && name.toLowerCase() === lower2) {
18392
+ const linked = live(ownString(managed, key));
18393
+ if (linked) return linked;
18394
+ }
18395
+ }
18396
+ return label2;
18397
+ }
18398
+
18399
+ // src/auth/orchestrator.ts
18400
+ var import_os20 = __toESM(require("os"));
17075
18401
 
17076
18402
  // src/ui/native.ts
17077
18403
  var import_child_process = require("child_process");
17078
- var import_path12 = __toESM(require("path"));
18404
+ var import_path14 = __toESM(require("path"));
17079
18405
 
17080
18406
  // src/context-sniper.ts
17081
- var import_path11 = __toESM(require("path"));
18407
+ var import_path13 = __toESM(require("path"));
17082
18408
  function smartTruncate(str, maxLen = 500) {
17083
18409
  if (str.length <= maxLen) return str;
17084
18410
  const edge = Math.floor(maxLen / 2) - 3;
@@ -17146,7 +18472,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
17146
18472
  intent = "EDIT";
17147
18473
  if (obj.file_path) {
17148
18474
  editFilePath = String(obj.file_path);
17149
- editFileName = import_path11.default.basename(editFilePath);
18475
+ editFileName = import_path13.default.basename(editFilePath);
17150
18476
  }
17151
18477
  const result = extractContext(String(obj.new_string), matchedWord);
17152
18478
  contextSnippet = result.snippet;
@@ -17214,7 +18540,7 @@ function formatArgs(args, matchedField, matchedWord) {
17214
18540
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
17215
18541
  const obj = parsed;
17216
18542
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
17217
- const file2 = obj.file_path ? import_path12.default.basename(String(obj.file_path)) : "file";
18543
+ const file2 = obj.file_path ? import_path14.default.basename(String(obj.file_path)) : "file";
17218
18544
  const oldPreview = smartTruncate(String(obj.old_string), 120);
17219
18545
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
17220
18546
  return {
@@ -17417,24 +18743,24 @@ end run`;
17417
18743
  }
17418
18744
 
17419
18745
  // src/canary/registry.ts
17420
- var import_fs12 = __toESM(require("fs"));
17421
- var import_os10 = __toESM(require("os"));
17422
- var import_path14 = __toESM(require("path"));
18746
+ var import_fs14 = __toESM(require("fs"));
18747
+ var import_os12 = __toESM(require("os"));
18748
+ var import_path16 = __toESM(require("path"));
17423
18749
  init_dist();
17424
18750
 
17425
18751
  // src/shields/jail.ts
17426
- var import_fs11 = __toESM(require("fs"));
17427
- var import_os9 = __toESM(require("os"));
17428
- var import_path13 = __toESM(require("path"));
18752
+ var import_fs13 = __toESM(require("fs"));
18753
+ var import_os11 = __toESM(require("os"));
18754
+ var import_path15 = __toESM(require("path"));
17429
18755
  init_dist();
17430
18756
  var USER_JAIL_SHIELD = "user-jail";
17431
18757
  function jailStorePath() {
17432
- return import_path13.default.join(import_os9.default.homedir(), ".node9", "jail-paths.json");
18758
+ return import_path15.default.join(import_os11.default.homedir(), ".node9", "jail-paths.json");
17433
18759
  }
17434
18760
  function readJailPaths() {
17435
18761
  let text;
17436
18762
  try {
17437
- text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
18763
+ text = import_fs13.default.readFileSync(jailStorePath(), "utf8");
17438
18764
  } catch (err) {
17439
18765
  if (err.code === "ENOENT") return [];
17440
18766
  throw err;
@@ -17463,7 +18789,7 @@ function findJailedPathIn(candidate, paths) {
17463
18789
 
17464
18790
  // src/canary/registry.ts
17465
18791
  function canaryStorePath() {
17466
- return import_path14.default.join(import_os10.default.homedir(), ".node9", "canaries.json");
18792
+ return import_path16.default.join(import_os12.default.homedir(), ".node9", "canaries.json");
17467
18793
  }
17468
18794
  function isRecord(x) {
17469
18795
  if (!x || typeof x !== "object") return false;
@@ -17474,7 +18800,7 @@ function loadCanaries(opts) {
17474
18800
  const p = canaryStorePath();
17475
18801
  let raw;
17476
18802
  try {
17477
- raw = import_fs12.default.readFileSync(p, "utf-8");
18803
+ raw = import_fs14.default.readFileSync(p, "utf-8");
17478
18804
  } catch (e) {
17479
18805
  if (e.code === "ENOENT") return [];
17480
18806
  throw e;
@@ -17500,22 +18826,22 @@ function canaryValues() {
17500
18826
  init_dist();
17501
18827
 
17502
18828
  // src/auth/session-script.ts
17503
- var import_fs13 = __toESM(require("fs"));
17504
- var import_os11 = __toESM(require("os"));
17505
- var import_path15 = __toESM(require("path"));
18829
+ var import_fs15 = __toESM(require("fs"));
18830
+ var import_os13 = __toESM(require("os"));
18831
+ var import_path17 = __toESM(require("path"));
17506
18832
  init_dist();
17507
18833
  var MAX_SCRIPT_BYTES = 64 * 1024;
17508
18834
  var MAX_RECORDED_WRITES = 32;
17509
18835
  function readScriptFile(resolved) {
17510
18836
  let fd;
17511
18837
  try {
17512
- fd = import_fs13.default.openSync(resolved, import_fs13.default.constants.O_RDONLY | (import_fs13.default.constants.O_NONBLOCK ?? 0));
17513
- const st = import_fs13.default.fstatSync(fd);
18838
+ fd = import_fs15.default.openSync(resolved, import_fs15.default.constants.O_RDONLY | (import_fs15.default.constants.O_NONBLOCK ?? 0));
18839
+ const st = import_fs15.default.fstatSync(fd);
17514
18840
  if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
17515
18841
  const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
17516
18842
  let n = 0;
17517
18843
  while (n < buf.length) {
17518
- const got = import_fs13.default.readSync(fd, buf, n, buf.length - n, n);
18844
+ const got = import_fs15.default.readSync(fd, buf, n, buf.length - n, n);
17519
18845
  if (got === 0) break;
17520
18846
  n += got;
17521
18847
  }
@@ -17526,7 +18852,7 @@ function readScriptFile(resolved) {
17526
18852
  } finally {
17527
18853
  if (fd !== void 0) {
17528
18854
  try {
17529
- import_fs13.default.closeSync(fd);
18855
+ import_fs15.default.closeSync(fd);
17530
18856
  } catch {
17531
18857
  }
17532
18858
  }
@@ -17534,9 +18860,9 @@ function readScriptFile(resolved) {
17534
18860
  }
17535
18861
  function absolute(p, cwd, home) {
17536
18862
  const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
17537
- return import_path15.default.resolve(cwd, spelled);
18863
+ return import_path17.default.resolve(cwd, spelled);
17538
18864
  }
17539
- async function resolveSessionScript(toolName, args, cwd, layers, deps, home = import_os11.default.homedir()) {
18865
+ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = import_os13.default.homedir()) {
17540
18866
  try {
17541
18867
  const d = deps ?? { checkWritten, readScript: readScriptFile };
17542
18868
  const shell = resolveShellInput(toolName, args, layers);
@@ -17565,7 +18891,7 @@ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = im
17565
18891
  return null;
17566
18892
  }
17567
18893
  }
17568
- function sessionWriteTargets(toolName, args, fileAccess, cwd, home = import_os11.default.homedir()) {
18894
+ function sessionWriteTargets(toolName, args, fileAccess, cwd, home = import_os13.default.homedir()) {
17569
18895
  const base2 = cwd ?? process.cwd();
17570
18896
  const out = /* @__PURE__ */ new Set();
17571
18897
  const argsObj = args && typeof args === "object" ? args : {};
@@ -17595,26 +18921,26 @@ init_audit();
17595
18921
  init_dist();
17596
18922
 
17597
18923
  // src/exposure/known-hosts.ts
17598
- var import_fs15 = __toESM(require("fs"));
17599
- var import_path17 = __toESM(require("path"));
18924
+ var import_fs17 = __toESM(require("fs"));
18925
+ var import_path19 = __toESM(require("path"));
17600
18926
 
17601
18927
  // src/utils/git-remotes.ts
17602
- var import_fs14 = __toESM(require("fs"));
17603
- var import_path16 = __toESM(require("path"));
18928
+ var import_fs16 = __toESM(require("fs"));
18929
+ var import_path18 = __toESM(require("path"));
17604
18930
  init_dist();
17605
18931
  function readSmallFile(p, max) {
17606
18932
  let fd;
17607
18933
  try {
17608
- fd = import_fs14.default.openSync(p, import_fs14.default.constants.O_RDONLY | (import_fs14.default.constants.O_NOFOLLOW ?? 0));
17609
- const st = import_fs14.default.fstatSync(fd);
18934
+ fd = import_fs16.default.openSync(p, import_fs16.default.constants.O_RDONLY | (import_fs16.default.constants.O_NOFOLLOW ?? 0));
18935
+ const st = import_fs16.default.fstatSync(fd);
17610
18936
  if (!st.isFile() || st.size > max) return null;
17611
18937
  const buf = Buffer.alloc(st.size);
17612
- const n = import_fs14.default.readSync(fd, buf, 0, st.size, 0);
18938
+ const n = import_fs16.default.readSync(fd, buf, 0, st.size, 0);
17613
18939
  return buf.subarray(0, n).toString("utf8");
17614
18940
  } catch {
17615
18941
  return null;
17616
18942
  } finally {
17617
- if (fd !== void 0) import_fs14.default.closeSync(fd);
18943
+ if (fd !== void 0) import_fs16.default.closeSync(fd);
17618
18944
  }
17619
18945
  }
17620
18946
  function gitRemoteHost(url) {
@@ -17633,20 +18959,20 @@ function gitRemoteHost(url) {
17633
18959
  return null;
17634
18960
  }
17635
18961
  function findGitConfig(dir2, walkUp = true) {
17636
- let cur = import_path16.default.resolve(dir2);
18962
+ let cur = import_path18.default.resolve(dir2);
17637
18963
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
17638
- const dotGit = import_path16.default.join(cur, ".git");
17639
- const direct = readSmallFile(import_path16.default.join(dotGit, "config"), 1e6);
18964
+ const dotGit = import_path18.default.join(cur, ".git");
18965
+ const direct = readSmallFile(import_path18.default.join(dotGit, "config"), 1e6);
17640
18966
  if (direct !== null) return direct;
17641
18967
  const pointer = readSmallFile(dotGit, 4096);
17642
18968
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
17643
18969
  if (m) {
17644
- let gitDir = import_path16.default.resolve(cur, m[1]);
17645
- const common = readSmallFile(import_path16.default.join(gitDir, "commondir"), 4096);
17646
- if (common !== null) gitDir = import_path16.default.resolve(gitDir, common.trim());
17647
- return readSmallFile(import_path16.default.join(gitDir, "config"), 1e6);
18970
+ let gitDir = import_path18.default.resolve(cur, m[1]);
18971
+ const common = readSmallFile(import_path18.default.join(gitDir, "commondir"), 4096);
18972
+ if (common !== null) gitDir = import_path18.default.resolve(gitDir, common.trim());
18973
+ return readSmallFile(import_path18.default.join(gitDir, "config"), 1e6);
17648
18974
  }
17649
- const parent = import_path16.default.dirname(cur);
18975
+ const parent = import_path18.default.dirname(cur);
17650
18976
  if (parent === cur) return null;
17651
18977
  cur = parent;
17652
18978
  }
@@ -17656,12 +18982,12 @@ var cache = /* @__PURE__ */ new Map();
17656
18982
  function readGitRemotes(dir2, opts = {}) {
17657
18983
  if (!dir2) return [];
17658
18984
  const walkUp = opts.walkUp !== false;
17659
- const key = `${walkUp ? "up" : "at"}:${import_path16.default.resolve(dir2)}`;
18985
+ const key = `${walkUp ? "up" : "at"}:${import_path18.default.resolve(dir2)}`;
17660
18986
  const hit = cache.get(key);
17661
18987
  if (hit) return hit;
17662
18988
  const out = [];
17663
18989
  try {
17664
- const cfg = findGitConfig(import_path16.default.resolve(dir2), walkUp);
18990
+ const cfg = findGitConfig(import_path18.default.resolve(dir2), walkUp);
17665
18991
  if (cfg !== null) {
17666
18992
  let current = null;
17667
18993
  for (const line of cfg.split(/\r?\n/)) {
@@ -17697,15 +19023,15 @@ var ECOSYSTEMS = [
17697
19023
  ];
17698
19024
  var cache2 = /* @__PURE__ */ new Map();
17699
19025
  function manifestRoot(dir2) {
17700
- let cur = import_path17.default.resolve(dir2);
19026
+ let cur = import_path19.default.resolve(dir2);
17701
19027
  for (let i = 0; i < 64; i++) {
17702
19028
  for (const [m] of ECOSYSTEMS) {
17703
19029
  try {
17704
- if (import_fs15.default.existsSync(import_path17.default.join(cur, m))) return cur;
19030
+ if (import_fs17.default.existsSync(import_path19.default.join(cur, m))) return cur;
17705
19031
  } catch {
17706
19032
  }
17707
19033
  }
17708
- const parent = import_path17.default.dirname(cur);
19034
+ const parent = import_path19.default.dirname(cur);
17709
19035
  if (parent === cur) return null;
17710
19036
  cur = parent;
17711
19037
  }
@@ -17714,7 +19040,7 @@ function manifestRoot(dir2) {
17714
19040
  function knownProjectHosts(dir2) {
17715
19041
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
17716
19042
  if (!dir2) return empty;
17717
- const key = import_path17.default.resolve(dir2);
19043
+ const key = import_path19.default.resolve(dir2);
17718
19044
  const hit = cache2.get(key);
17719
19045
  if (hit) return hit;
17720
19046
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
@@ -17727,10 +19053,10 @@ function knownProjectHosts(dir2) {
17727
19053
  const root = manifestRoot(key);
17728
19054
  if (root !== null) {
17729
19055
  for (const [m, registry] of ECOSYSTEMS) {
17730
- if (import_fs15.default.existsSync(import_path17.default.join(root, m))) out.hosts.add(registry);
19056
+ if (import_fs17.default.existsSync(import_path19.default.join(root, m))) out.hosts.add(registry);
17731
19057
  }
17732
19058
  try {
17733
- if (import_fs15.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
19059
+ if (import_fs17.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17734
19060
  out.hosts.add("host:rubygems.org");
17735
19061
  } catch {
17736
19062
  }
@@ -17742,9 +19068,9 @@ function knownProjectHosts(dir2) {
17742
19068
  }
17743
19069
 
17744
19070
  // src/exposure/writes.ts
17745
- var import_fs16 = __toESM(require("fs"));
17746
- var import_os12 = __toESM(require("os"));
17747
- var import_path18 = __toESM(require("path"));
19071
+ var import_fs18 = __toESM(require("fs"));
19072
+ var import_os14 = __toESM(require("os"));
19073
+ var import_path20 = __toESM(require("path"));
17748
19074
  init_dist();
17749
19075
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
17750
19076
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
@@ -17774,7 +19100,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17774
19100
  if (typeof a.content === "string") {
17775
19101
  try {
17776
19102
  const next = JSON.parse(a.content);
17777
- const prev = JSON.parse(import_fs16.default.readFileSync(absPath, "utf8"));
19103
+ const prev = JSON.parse(import_fs18.default.readFileSync(absPath, "utf8"));
17778
19104
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
17779
19105
  } catch {
17780
19106
  return LIFECYCLE_RE.test(a.content);
@@ -17784,8 +19110,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17784
19110
  }
17785
19111
  function judgeWritesAfterExposure(opts) {
17786
19112
  const holds = [];
17787
- const home = opts.home ?? import_os12.default.homedir();
17788
- const tmp = normalizeFilePath(import_os12.default.tmpdir());
19113
+ const home = opts.home ?? import_os14.default.homedir();
19114
+ const tmp = normalizeFilePath(import_os14.default.tmpdir());
17789
19115
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
17790
19116
  const seen = /* @__PURE__ */ new Set();
17791
19117
  for (const access of opts.accesses) {
@@ -17795,7 +19121,7 @@ function judgeWritesAfterExposure(opts) {
17795
19121
  seen.add(p);
17796
19122
  if (p === "/dev" || p.startsWith("/dev/")) continue;
17797
19123
  if (root !== null && isInside(root, p)) {
17798
- if (isAgentInstructionPath(p, root) && (import_path18.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
19124
+ if (isAgentInstructionPath(p, root) && (import_path20.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17799
19125
  holds.push({ path: p, why: "agent-instruction" });
17800
19126
  continue;
17801
19127
  }
@@ -17817,25 +19143,25 @@ function judgeWritesAfterExposure(opts) {
17817
19143
 
17818
19144
  // src/exposure/marker.ts
17819
19145
  var import_crypto8 = __toESM(require("crypto"));
17820
- var import_fs17 = __toESM(require("fs"));
17821
- var import_os13 = __toESM(require("os"));
17822
- var import_path19 = __toESM(require("path"));
19146
+ var import_fs19 = __toESM(require("fs"));
19147
+ var import_os15 = __toESM(require("os"));
19148
+ var import_path21 = __toESM(require("path"));
17823
19149
 
17824
19150
  // src/daemon/exposure-store.ts
17825
19151
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
17826
19152
 
17827
19153
  // src/exposure/marker.ts
17828
19154
  function dir() {
17829
- return import_path19.default.join(import_os13.default.homedir(), ".node9", "exposure-sessions");
19155
+ return import_path21.default.join(import_os15.default.homedir(), ".node9", "exposure-sessions");
17830
19156
  }
17831
19157
  function file(sessionId) {
17832
19158
  const id = import_crypto8.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
17833
- return import_path19.default.join(dir(), id);
19159
+ return import_path21.default.join(dir(), id);
17834
19160
  }
17835
19161
  function maybeSessionExposed(sessionId) {
17836
19162
  if (!sessionId) return false;
17837
19163
  try {
17838
- const st = import_fs17.default.statSync(file(sessionId));
19164
+ const st = import_fs19.default.statSync(file(sessionId));
17839
19165
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
17840
19166
  } catch (e) {
17841
19167
  return e.code !== "ENOENT";
@@ -17843,27 +19169,7 @@ function maybeSessionExposed(sessionId) {
17843
19169
  }
17844
19170
 
17845
19171
  // src/exposure/roles.ts
17846
- var import_fs19 = __toESM(require("fs"));
17847
-
17848
- // src/daemon/mcp-tools.ts
17849
- var import_fs18 = __toESM(require("fs"));
17850
- var import_path20 = __toESM(require("path"));
17851
- var import_os14 = __toESM(require("os"));
17852
- function getMcpToolsFile() {
17853
- return import_path20.default.join(import_os14.default.homedir(), ".node9", "mcp-tools.json");
17854
- }
17855
- function readMcpToolsConfig() {
17856
- try {
17857
- const file2 = getMcpToolsFile();
17858
- if (!import_fs18.default.existsSync(file2)) return {};
17859
- const raw = import_fs18.default.readFileSync(file2, "utf-8");
17860
- return JSON.parse(raw);
17861
- } catch {
17862
- return {};
17863
- }
17864
- }
17865
-
17866
- // src/exposure/roles.ts
19172
+ var import_fs20 = __toESM(require("fs"));
17867
19173
  var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
17868
19174
  var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
17869
19175
  function foldServerName(name) {
@@ -17873,7 +19179,7 @@ var inventoryCache = null;
17873
19179
  var inventoryStamp = null;
17874
19180
  function inventoryFileStamp() {
17875
19181
  try {
17876
- const st = import_fs19.default.statSync(getMcpToolsFile());
19182
+ const st = import_fs20.default.statSync(getMcpToolsFile());
17877
19183
  return `${st.mtimeMs}:${st.size}`;
17878
19184
  } catch {
17879
19185
  return "";
@@ -17957,16 +19263,16 @@ function sourceWord(toolName) {
17957
19263
  }
17958
19264
 
17959
19265
  // src/auth/trusted-recipients.ts
17960
- var import_fs20 = __toESM(require("fs"));
17961
- var import_path21 = __toESM(require("path"));
17962
- var import_os15 = __toESM(require("os"));
19266
+ var import_fs21 = __toESM(require("fs"));
19267
+ var import_path22 = __toESM(require("path"));
19268
+ var import_os16 = __toESM(require("os"));
17963
19269
  init_dist();
17964
19270
  function getTrustedRecipientsPath() {
17965
- return import_path21.default.join(import_os15.default.homedir(), ".node9", "trusted-recipients.json");
19271
+ return import_path22.default.join(import_os16.default.homedir(), ".node9", "trusted-recipients.json");
17966
19272
  }
17967
19273
  function readTrustedRecipients() {
17968
19274
  try {
17969
- const raw = import_fs20.default.readFileSync(getTrustedRecipientsPath(), "utf8");
19275
+ const raw = import_fs21.default.readFileSync(getTrustedRecipientsPath(), "utf8");
17970
19276
  const parsed = JSON.parse(raw);
17971
19277
  return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
17972
19278
  } catch {
@@ -17975,10 +19281,10 @@ function readTrustedRecipients() {
17975
19281
  }
17976
19282
  function writeTrustedRecipients(entries) {
17977
19283
  const filePath = getTrustedRecipientsPath();
17978
- import_fs20.default.mkdirSync(import_path21.default.dirname(filePath), { recursive: true });
19284
+ import_fs21.default.mkdirSync(import_path22.default.dirname(filePath), { recursive: true });
17979
19285
  const tmp = filePath + ".node9-tmp";
17980
- import_fs20.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
17981
- import_fs20.default.renameSync(tmp, filePath);
19286
+ import_fs21.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
19287
+ import_fs21.default.renameSync(tmp, filePath);
17982
19288
  }
17983
19289
  function addTrustedRecipient(raw, addedBy = "user") {
17984
19290
  const value = normalizeRecipient(raw);
@@ -17993,7 +19299,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
17993
19299
  var cache3 = null;
17994
19300
  function mtimeOf() {
17995
19301
  try {
17996
- return import_fs20.default.statSync(getTrustedRecipientsPath()).mtimeMs;
19302
+ return import_fs21.default.statSync(getTrustedRecipientsPath()).mtimeMs;
17997
19303
  } catch {
17998
19304
  return 0;
17999
19305
  }
@@ -18007,41 +19313,41 @@ function isTrustedRecipient(value) {
18007
19313
  }
18008
19314
 
18009
19315
  // src/auth/cloud.ts
18010
- var import_fs22 = __toESM(require("fs"));
18011
- var import_os16 = __toESM(require("os"));
18012
- var import_path22 = __toESM(require("path"));
19316
+ var import_fs23 = __toESM(require("fs"));
19317
+ var import_os17 = __toESM(require("os"));
19318
+ var import_path23 = __toESM(require("path"));
18013
19319
  init_audit();
18014
19320
 
18015
19321
  // src/utils/read-capped.ts
18016
- var import_fs21 = __toESM(require("fs"));
19322
+ var import_fs22 = __toESM(require("fs"));
18017
19323
  function readCapped(file2, maxBytes) {
18018
19324
  if (maxBytes < 0) return null;
18019
19325
  let fd;
18020
19326
  try {
18021
- fd = import_fs21.default.openSync(file2, "r");
19327
+ fd = import_fs22.default.openSync(file2, "r");
18022
19328
  } catch {
18023
19329
  return null;
18024
19330
  }
18025
19331
  try {
18026
- const st = import_fs21.default.fstatSync(fd);
19332
+ const st = import_fs22.default.fstatSync(fd);
18027
19333
  if (!st.isFile()) return null;
18028
19334
  const want = Math.min(st.size, maxBytes + 1);
18029
19335
  const buf = Buffer.alloc(want);
18030
- let read = 0;
18031
- while (read < want) {
18032
- const n = import_fs21.default.readSync(fd, buf, read, want - read, read);
19336
+ let read2 = 0;
19337
+ while (read2 < want) {
19338
+ const n = import_fs22.default.readSync(fd, buf, read2, want - read2, read2);
18033
19339
  if (n <= 0) break;
18034
- read += n;
19340
+ read2 += n;
18035
19341
  }
18036
19342
  return {
18037
- bytes: buf.subarray(0, Math.min(read, maxBytes)),
18038
- truncated: read > maxBytes
19343
+ bytes: buf.subarray(0, Math.min(read2, maxBytes)),
19344
+ truncated: read2 > maxBytes
18039
19345
  };
18040
19346
  } catch {
18041
19347
  return null;
18042
19348
  } finally {
18043
19349
  try {
18044
- import_fs21.default.closeSync(fd);
19350
+ import_fs22.default.closeSync(fd);
18045
19351
  } catch {
18046
19352
  }
18047
19353
  }
@@ -18086,7 +19392,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18086
19392
  let ciContext;
18087
19393
  if (process.env.CI) {
18088
19394
  try {
18089
- const ciContextPath = import_path22.default.join(import_os16.default.homedir(), ".node9", "ci-context.json");
19395
+ const ciContextPath = import_path23.default.join(import_os17.default.homedir(), ".node9", "ci-context.json");
18090
19396
  const ci = readCappedText(ciContextPath, 1e4);
18091
19397
  if (!ci) throw new Error("ci-context.json unreadable");
18092
19398
  if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
@@ -18128,9 +19434,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18128
19434
  context: {
18129
19435
  agent: meta?.agent,
18130
19436
  mcpServer: meta?.mcpServer,
18131
- hostname: import_os16.default.hostname(),
19437
+ hostname: import_os17.default.hostname(),
18132
19438
  cwd: process.cwd(),
18133
- platform: import_os16.default.platform()
19439
+ platform: import_os17.default.platform()
18134
19440
  },
18135
19441
  // The SaaS validates riskMetadata strictly and would drop it whole on
18136
19442
  // an unknown key, so only fields its schema declares go on the wire.
@@ -18204,14 +19510,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18204
19510
  });
18205
19511
  clearTimeout(timer);
18206
19512
  if (!res.ok) {
18207
- import_fs22.default.appendFileSync(
19513
+ import_fs23.default.appendFileSync(
18208
19514
  HOOK_DEBUG_LOG,
18209
19515
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
18210
19516
  `
18211
19517
  );
18212
19518
  }
18213
19519
  } catch (err) {
18214
- import_fs22.default.appendFileSync(
19520
+ import_fs23.default.appendFileSync(
18215
19521
  HOOK_DEBUG_LOG,
18216
19522
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
18217
19523
  `
@@ -18220,18 +19526,18 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18220
19526
  }
18221
19527
 
18222
19528
  // src/loop-detector.ts
18223
- var import_fs23 = __toESM(require("fs"));
18224
- var import_path23 = __toESM(require("path"));
18225
- var import_os17 = __toESM(require("os"));
19529
+ var import_fs24 = __toESM(require("fs"));
19530
+ var import_path24 = __toESM(require("path"));
19531
+ var import_os18 = __toESM(require("os"));
18226
19532
  init_dist();
18227
19533
  init_dist();
18228
19534
  function loopStateFile() {
18229
- return import_path23.default.join(import_os17.default.homedir(), ".node9", "loop-state.json");
19535
+ return import_path24.default.join(import_os18.default.homedir(), ".node9", "loop-state.json");
18230
19536
  }
18231
19537
  function readState() {
18232
19538
  try {
18233
- if (!import_fs23.default.existsSync(loopStateFile())) return [];
18234
- const raw = import_fs23.default.readFileSync(loopStateFile(), "utf-8");
19539
+ if (!import_fs24.default.existsSync(loopStateFile())) return [];
19540
+ const raw = import_fs24.default.readFileSync(loopStateFile(), "utf-8");
18235
19541
  const parsed = JSON.parse(raw);
18236
19542
  if (!Array.isArray(parsed)) return [];
18237
19543
  return parsed;
@@ -18240,11 +19546,11 @@ function readState() {
18240
19546
  }
18241
19547
  }
18242
19548
  function writeState(records) {
18243
- const dir2 = import_path23.default.dirname(loopStateFile());
18244
- if (!import_fs23.default.existsSync(dir2)) import_fs23.default.mkdirSync(dir2, { recursive: true });
18245
- const tmpPath = `${loopStateFile()}.${import_os17.default.hostname()}.${process.pid}.tmp`;
18246
- import_fs23.default.writeFileSync(tmpPath, JSON.stringify(records));
18247
- import_fs23.default.renameSync(tmpPath, loopStateFile());
19549
+ const dir2 = import_path24.default.dirname(loopStateFile());
19550
+ if (!import_fs24.default.existsSync(dir2)) import_fs24.default.mkdirSync(dir2, { recursive: true });
19551
+ const tmpPath = `${loopStateFile()}.${import_os18.default.hostname()}.${process.pid}.tmp`;
19552
+ import_fs24.default.writeFileSync(tmpPath, JSON.stringify(records));
19553
+ import_fs24.default.renameSync(tmpPath, loopStateFile());
18248
19554
  }
18249
19555
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18250
19556
  try {
@@ -18258,21 +19564,21 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18258
19564
 
18259
19565
  // src/supply-chain/check.ts
18260
19566
  var import_picomatch3 = __toESM(require("picomatch"));
18261
- var import_path26 = __toESM(require("path"));
19567
+ var import_path27 = __toESM(require("path"));
18262
19568
  init_dist();
18263
19569
 
18264
19570
  // src/supply-chain/osv-index.ts
18265
- var import_fs24 = __toESM(require("fs"));
18266
- var import_os18 = __toESM(require("os"));
18267
- var import_path24 = __toESM(require("path"));
19571
+ var import_fs25 = __toESM(require("fs"));
19572
+ var import_os19 = __toESM(require("os"));
19573
+ var import_path25 = __toESM(require("path"));
18268
19574
  var import_crypto9 = require("crypto");
18269
19575
  init_dist();
18270
19576
  var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
18271
19577
  function osvRoot() {
18272
- return import_path24.default.join(import_os18.default.homedir(), ".node9", "osv");
19578
+ return import_path25.default.join(import_os19.default.homedir(), ".node9", "osv");
18273
19579
  }
18274
19580
  function ecoDir(eco) {
18275
- return import_path24.default.join(osvRoot(), eco);
19581
+ return import_path25.default.join(osvRoot(), eco);
18276
19582
  }
18277
19583
  function indexKey(eco, name) {
18278
19584
  switch (eco) {
@@ -18291,11 +19597,11 @@ function shardOf(key) {
18291
19597
  return (0, import_crypto9.createHash)("sha1").update(key).digest("hex").slice(0, 2);
18292
19598
  }
18293
19599
  function shardPath(eco, shard) {
18294
- return import_path24.default.join(ecoDir(eco), "shards", `${shard}.json`);
19600
+ return import_path25.default.join(ecoDir(eco), "shards", `${shard}.json`);
18295
19601
  }
18296
19602
  function readMeta(eco) {
18297
19603
  try {
18298
- const m = JSON.parse(import_fs24.default.readFileSync(import_path24.default.join(ecoDir(eco), "meta.json"), "utf8"));
19604
+ const m = JSON.parse(import_fs25.default.readFileSync(import_path25.default.join(ecoDir(eco), "meta.json"), "utf8"));
18299
19605
  if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
18300
19606
  return m;
18301
19607
  } catch {
@@ -18304,7 +19610,7 @@ function readMeta(eco) {
18304
19610
  }
18305
19611
  function readShard(eco, shard) {
18306
19612
  try {
18307
- const s = JSON.parse(import_fs24.default.readFileSync(shardPath(eco, shard), "utf8"));
19613
+ const s = JSON.parse(import_fs25.default.readFileSync(shardPath(eco, shard), "utf8"));
18308
19614
  return s && typeof s === "object" && !Array.isArray(s) ? s : {};
18309
19615
  } catch {
18310
19616
  return {};
@@ -18430,14 +19736,102 @@ async function queryOsvMalicious(queries) {
18430
19736
  }
18431
19737
 
18432
19738
  // src/supply-chain/registry.ts
19739
+ var import_semver2 = require("semver");
19740
+
19741
+ // src/supply-chain/provenance.ts
18433
19742
  var import_semver = require("semver");
18434
19743
  var TIMEOUT_MS2 = 1500;
18435
- var MAX_BYTES = 2 * 1024 * 1024;
19744
+ var MAX_BYTES = 256 * 1024;
19745
+ function normalizeRepo(raw, host2 = "github.com") {
19746
+ let s = raw.trim().toLowerCase();
19747
+ s = s.replace(/^git\+/, "").replace(/^[a-z]+:\/\//, "");
19748
+ s = s.replace(/@refs\/.*$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
19749
+ if (!s.includes(".") || !s.split("/")[0].includes(".")) s = `${host2}/${s}`;
19750
+ return /^[a-z0-9.-]+\/[\w.-]+\/[\w.-]+/.test(s) ? s : null;
19751
+ }
19752
+ function npmRepoOf(doc) {
19753
+ const list = doc?.attestations;
19754
+ if (!Array.isArray(list)) return null;
19755
+ for (const a of list) {
19756
+ const att = a;
19757
+ if (!att.predicateType?.startsWith("https://slsa.dev/provenance/")) continue;
19758
+ const payload = att.bundle?.dsseEnvelope?.payload;
19759
+ if (typeof payload !== "string") continue;
19760
+ try {
19761
+ const statement = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
19762
+ const p = statement.predicate;
19763
+ const repo = p?.buildDefinition?.externalParameters?.workflow?.repository ?? p?.invocation?.configSource?.uri;
19764
+ if (typeof repo === "string") return normalizeRepo(repo);
19765
+ } catch {
19766
+ }
19767
+ }
19768
+ return null;
19769
+ }
19770
+ function pypiRepoOf(doc) {
19771
+ const bundles = doc?.attestation_bundles;
19772
+ if (!Array.isArray(bundles)) return null;
19773
+ for (const b of bundles) {
19774
+ const pub = b.publisher;
19775
+ if (typeof pub?.repository === "string") {
19776
+ const host2 = /gitlab/i.test(pub.kind ?? "") ? "gitlab.com" : "github.com";
19777
+ return normalizeRepo(pub.repository, host2);
19778
+ }
19779
+ if (/^google$/i.test(pub?.kind ?? "") && typeof pub?.email === "string") {
19780
+ return `google:${pub.email.trim().toLowerCase()}`;
19781
+ }
19782
+ }
19783
+ return null;
19784
+ }
19785
+ async function npmSourceRepo(name, version) {
19786
+ const base2 = endpoints().npmRegistry;
19787
+ if (!base2) return "unknown";
19788
+ const url = `${base2}/-/npm/v1/attestations/${name.replaceAll("/", "%2F")}@${encodeURIComponent(version)}`;
19789
+ return read(url, npmRepoOf);
19790
+ }
19791
+ async function read(url, repoOf) {
19792
+ try {
19793
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
19794
+ if (doc === null) return "none";
19795
+ return repoOf(doc) ?? "unknown";
19796
+ } catch {
19797
+ return "unknown";
19798
+ }
19799
+ }
19800
+ async function pypiSourceRepo(name, version, filename) {
19801
+ const base2 = endpoints().pypi;
19802
+ if (!base2) return "unknown";
19803
+ const url = `${base2}/integrity/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}/provenance`;
19804
+ return read(url, pypiRepoOf);
19805
+ }
19806
+ var PYPI_FINAL = /^\d+(?:\.\d+)*$/;
19807
+ function comparePypi(a, b) {
19808
+ const x = a.split(".").map(Number);
19809
+ const y = b.split(".").map(Number);
19810
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
19811
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
19812
+ if (d !== 0) return d;
19813
+ }
19814
+ return 0;
19815
+ }
19816
+ function previousRelease(eco, versions, version) {
19817
+ if (eco === "npm") {
19818
+ if (!(0, import_semver.valid)(version)) return void 0;
19819
+ const below2 = versions.filter((v) => (0, import_semver.valid)(v) && !(0, import_semver.prerelease)(v) && (0, import_semver.rcompare)(v, version) > 0);
19820
+ return below2.sort(import_semver.rcompare)[0] ?? null;
19821
+ }
19822
+ if (!PYPI_FINAL.test(version)) return void 0;
19823
+ const below = versions.filter((v) => PYPI_FINAL.test(v) && comparePypi(v, version) < 0);
19824
+ return below.sort((a, b) => comparePypi(b, a))[0] ?? null;
19825
+ }
19826
+
19827
+ // src/supply-chain/registry.ts
19828
+ var TIMEOUT_MS3 = 1500;
19829
+ var MAX_BYTES2 = 2 * 1024 * 1024;
18436
19830
  var VERSION_DOC_MAX_BYTES = 256 * 1024;
18437
19831
  var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
18438
19832
  async function npmVersionDocument(url, selector) {
18439
19833
  const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
18440
- timeoutMs: TIMEOUT_MS2,
19834
+ timeoutMs: TIMEOUT_MS3,
18441
19835
  maxBytes: VERSION_DOC_MAX_BYTES
18442
19836
  });
18443
19837
  if (!doc?.version) return null;
@@ -18461,16 +19855,16 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18461
19855
  let doc;
18462
19856
  try {
18463
19857
  doc = await fetchJson(url, {
18464
- timeoutMs: TIMEOUT_MS2,
18465
- maxBytes: MAX_BYTES,
19858
+ timeoutMs: TIMEOUT_MS3,
19859
+ maxBytes: MAX_BYTES2,
18466
19860
  headers: { Accept: "application/vnd.npm.install-v1+json" }
18467
19861
  });
18468
19862
  } catch (err) {
18469
19863
  if (!/too large/.test(err.message)) throw err;
18470
- const range = !version && spec && (0, import_semver.validRange)(spec) ? spec : null;
19864
+ const range = !version && spec && (0, import_semver2.validRange)(spec) ? spec : null;
18471
19865
  const selector = version ?? (spec && !range ? spec : "latest");
18472
19866
  const info2 = await npmVersionDocument(url, selector);
18473
- if (info2 && range && !(0, import_semver.satisfies)(info2.version, range)) return null;
19867
+ if (info2 && range && !(0, import_semver2.satisfies)(info2.version, range)) return null;
18474
19868
  return info2;
18475
19869
  }
18476
19870
  if (!doc) return null;
@@ -18479,8 +19873,8 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18479
19873
  if (!resolved) {
18480
19874
  if (!spec) resolved = latest;
18481
19875
  else if (doc["dist-tags"]?.[spec]) resolved = doc["dist-tags"][spec];
18482
- else if ((0, import_semver.validRange)(spec)) {
18483
- resolved = latest && (0, import_semver.satisfies)(latest, spec) ? latest : (0, import_semver.maxSatisfying)(Object.keys(doc.versions ?? {}), spec) ?? void 0;
19876
+ else if ((0, import_semver2.validRange)(spec)) {
19877
+ resolved = latest && (0, import_semver2.satisfies)(latest, spec) ? latest : (0, import_semver2.maxSatisfying)(Object.keys(doc.versions ?? {}), spec) ?? void 0;
18484
19878
  }
18485
19879
  }
18486
19880
  if (!resolved) return null;
@@ -18490,13 +19884,23 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18490
19884
  version: resolved,
18491
19885
  hasInstallScript: v?.hasInstallScript === true
18492
19886
  };
19887
+ const previous = previousRelease("npm", Object.keys(doc.versions ?? {}), resolved);
19888
+ if (v && previous !== void 0) {
19889
+ const pv = previous ? doc.versions?.[previous] : void 0;
19890
+ info.history = {
19891
+ previous,
19892
+ previousHasInstallScript: pv ? pv.hasInstallScript === true : void 0,
19893
+ provenance: !!v.dist?.attestations?.provenance,
19894
+ previousProvenance: pv ? !!pv.dist?.attestations?.provenance : void 0
19895
+ };
19896
+ }
18493
19897
  if (!Number.isFinite(modifiedMs)) return info;
18494
19898
  if (Date.now() - modifiedMs >= freshWindowMs) {
18495
19899
  info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
18496
19900
  return info;
18497
19901
  }
18498
19902
  try {
18499
- const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
19903
+ const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18500
19904
  const t = full?.time?.[resolved];
18501
19905
  const tMs = t ? Date.parse(t) : NaN;
18502
19906
  if (Number.isFinite(tMs)) {
@@ -18514,15 +19918,37 @@ async function pypiInfo(name, version) {
18514
19918
  const base2 = endpoints().pypi;
18515
19919
  if (!base2) return null;
18516
19920
  const url = version ? `${base2}/pypi/${encodeURIComponent(name)}/${encodeURIComponent(version)}/json` : `${base2}/pypi/${encodeURIComponent(name)}/json`;
18517
- const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
19921
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18518
19922
  if (!doc?.info?.version) return null;
18519
19923
  const times = (doc.urls ?? []).map((u) => u.upload_time_iso_8601 ? Date.parse(u.upload_time_iso_8601) : NaN).filter((t) => Number.isFinite(t));
18520
- return {
18521
- version: version ?? doc.info.version,
19924
+ const resolved = version ?? doc.info.version;
19925
+ const info = {
19926
+ version: resolved,
18522
19927
  // The earliest file of the release is when it became installable.
18523
- publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
19928
+ publishedAtMs: times.length > 0 ? Math.min(...times) : void 0,
19929
+ file: doc.urls?.find((u) => typeof u.filename === "string")?.filename
19930
+ };
19931
+ if (doc.releases) info.history = pypiHistoryOf(doc.releases, resolved) ?? void 0;
19932
+ return info;
19933
+ }
19934
+ function pypiHistoryOf(releases, version) {
19935
+ const versions = Object.keys(releases).filter((v) => (releases[v] ?? []).length > 0);
19936
+ const previous = previousRelease("PyPI", versions, version);
19937
+ if (previous === void 0) return null;
19938
+ return {
19939
+ previous,
19940
+ previousFile: previous ? releases[previous]?.find((f) => typeof f.filename === "string")?.filename : void 0
18524
19941
  };
18525
19942
  }
19943
+ async function pypiHistory(name, version) {
19944
+ const base2 = endpoints().pypi;
19945
+ if (!base2) return null;
19946
+ const doc = await fetchJson(`${base2}/pypi/${encodeURIComponent(name)}/json`, {
19947
+ timeoutMs: TIMEOUT_MS3,
19948
+ maxBytes: MAX_BYTES2
19949
+ });
19950
+ return doc?.releases ? pypiHistoryOf(doc.releases, version) : null;
19951
+ }
18526
19952
  var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
18527
19953
  async function registryInfo(eco, name, version, freshWindowMs, raw) {
18528
19954
  if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
@@ -18531,19 +19957,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
18531
19957
  }
18532
19958
 
18533
19959
  // src/supply-chain/local-resolve.ts
18534
- var import_fs25 = __toESM(require("fs"));
18535
- var import_path25 = __toESM(require("path"));
19960
+ var import_fs26 = __toESM(require("fs"));
19961
+ var import_path26 = __toESM(require("path"));
18536
19962
  var MAX_LEVELS = 32;
18537
19963
  var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
18538
19964
  function resolveInstalled(name, cwd) {
18539
- if (!cwd || !import_path25.default.isAbsolute(cwd)) return null;
19965
+ if (!cwd || !import_path26.default.isAbsolute(cwd)) return null;
18540
19966
  if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
18541
- let dir2 = import_path25.default.resolve(cwd);
19967
+ let dir2 = import_path26.default.resolve(cwd);
18542
19968
  for (let i = 0; i < MAX_LEVELS; i++) {
18543
- const pkgDir = import_path25.default.join(dir2, "node_modules", ...name.split("/"));
18544
- const manifest = import_path25.default.join(pkgDir, "package.json");
19969
+ const pkgDir = import_path26.default.join(dir2, "node_modules", ...name.split("/"));
19970
+ const manifest = import_path26.default.join(pkgDir, "package.json");
18545
19971
  try {
18546
- const raw = import_fs25.default.readFileSync(manifest, "utf8");
19972
+ const raw = import_fs26.default.readFileSync(manifest, "utf8");
18547
19973
  const parsed = JSON.parse(raw);
18548
19974
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
18549
19975
  return { version: parsed.version, dir: pkgDir };
@@ -18551,7 +19977,7 @@ function resolveInstalled(name, cwd) {
18551
19977
  return null;
18552
19978
  } catch {
18553
19979
  }
18554
- const parent = import_path25.default.dirname(dir2);
19980
+ const parent = import_path26.default.dirname(dir2);
18555
19981
  if (parent === dir2) break;
18556
19982
  dir2 = parent;
18557
19983
  }
@@ -18673,7 +20099,7 @@ function checkInstalled(p, version, misses) {
18673
20099
  }
18674
20100
  async function checkOne(p, cfg, misses, networkCapped, cwd) {
18675
20101
  if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
18676
- const localCwd = cwd && import_path26.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path26.default.resolve(dir2, step), cwd) : void 0;
20102
+ const localCwd = cwd && import_path27.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path27.default.resolve(dir2, step), cwd) : void 0;
18677
20103
  const installed = resolveInstalled(p.name, localCwd);
18678
20104
  if (installed && (!p.version || p.version === installed.version))
18679
20105
  return checkInstalled(p, installed.version, misses);
@@ -18716,8 +20142,61 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
18716
20142
  detail: `${label(p, version)} runs an install script`
18717
20143
  });
18718
20144
  }
20145
+ if (info && version && (cfg.newPackage !== "off" || cfg.installScript !== "off")) {
20146
+ return byHistory(p, version, info, findings, misses);
20147
+ }
18719
20148
  return findings;
18720
20149
  }
20150
+ var SOFT = /* @__PURE__ */ new Set(["new", "install-script"]);
20151
+ async function byHistory(p, version, info, findings, misses) {
20152
+ let out = findings;
20153
+ let h = info.history;
20154
+ if (!h && p.ecosystem === "PyPI" && out.some((f) => SOFT.has(f.kind))) {
20155
+ try {
20156
+ h = await pypiHistory(p.name, version) ?? void 0;
20157
+ } catch (err) {
20158
+ misses.push(`${label(p, version)}: release history ${err.message}`);
20159
+ }
20160
+ }
20161
+ if (!h || !h.previous) return out;
20162
+ const previous = h.previous;
20163
+ if (h.previousHasInstallScript === true) {
20164
+ out = out.filter((f) => f.kind !== "install-script");
20165
+ }
20166
+ const drop = () => ({
20167
+ pkg: p,
20168
+ version,
20169
+ kind: "provenance-drop",
20170
+ detail: `${label(p, version)} was published without the provenance its previous release (${previous}) had`
20171
+ });
20172
+ if (p.ecosystem === "npm") {
20173
+ if (h.previousProvenance && h.provenance === false) return [...out, drop()];
20174
+ if (!out.some((f) => SOFT.has(f.kind)) || !h.provenance || !h.previousProvenance) return out;
20175
+ const [now, before] = await Promise.all([
20176
+ npmSourceRepo(p.name, version),
20177
+ npmSourceRepo(p.name, previous)
20178
+ ]);
20179
+ return vouched(p, version, now, before, out, misses);
20180
+ }
20181
+ if (p.ecosystem === "PyPI") {
20182
+ if (!out.some((f) => SOFT.has(f.kind)) || !info.file || !h.previousFile) return out;
20183
+ const [now, before] = await Promise.all([
20184
+ pypiSourceRepo(p.name, version, info.file),
20185
+ pypiSourceRepo(p.name, previous, h.previousFile)
20186
+ ]);
20187
+ if (now === "none" && before !== "none" && before !== "unknown") return [...out, drop()];
20188
+ return vouched(p, version, now, before, out, misses);
20189
+ }
20190
+ return out;
20191
+ }
20192
+ function vouched(p, version, now, before, findings, misses) {
20193
+ if (now === "unknown" || before === "unknown") {
20194
+ misses.push(`${label(p, version)}: provenance unreadable, findings kept`);
20195
+ return findings;
20196
+ }
20197
+ if (now === "none" || before === "none" || now !== before) return findings;
20198
+ return findings.filter((f) => !SOFT.has(f.kind));
20199
+ }
18721
20200
  async function runPackageCheck(toolName, args, cfg, cwd) {
18722
20201
  if (!cfg.enabled) return ALLOW;
18723
20202
  try {
@@ -19100,7 +20579,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19100
20579
  fileAccess,
19101
20580
  config,
19102
20581
  skipIgnoredFastPath,
19103
- argumentScan: argScan
20582
+ argumentScan: argScan,
20583
+ // A server a person linked to an app is judged as that app (fw#390).
20584
+ mcpServer: resolveLinkedServer(meta?.mcpServer, {
20585
+ serverKey: meta?.serverKey,
20586
+ managedLinks: config.policy.appLinks,
20587
+ appliedShields: config.policy.appliedShields,
20588
+ workspaceGoverned: config.policySource === "workspace"
20589
+ })
19104
20590
  });
19105
20591
  if (!sessionScript || own.decision === "block") return own;
19106
20592
  const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
@@ -19316,7 +20802,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19316
20802
  const last = rec.sources[rec.sources.length - 1];
19317
20803
  const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
19318
20804
  const when = last ? new Date(last.at).toTimeString().slice(0, 5) : "";
19319
- const read = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
20805
+ const read2 = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
19320
20806
  if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
19321
20807
  const noun = (d) => {
19322
20808
  const k = destinationKind(d);
@@ -19325,14 +20811,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19325
20811
  const parts = held.map(
19326
20812
  (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`
19327
20813
  );
19328
- const home = import_os19.default.homedir();
20814
+ const home = import_os20.default.homedir();
19329
20815
  const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
19330
20816
  for (const w of writeHolds) {
19331
20817
  parts.push(
19332
20818
  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}`
19333
20819
  );
19334
20820
  }
19335
- exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
20821
+ exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
19336
20822
  exposureFromEid = last?.eid;
19337
20823
  if (exposureFromEid) exposureMeta = { exposureFromEid };
19338
20824
  exposureKeys = [