@node9/proxy 2.37.1 → 2.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
  }
@@ -353,6 +449,32 @@ function ssrfFloorDecision(m, asWritten, opts) {
353
449
  }
354
450
  return "block";
355
451
  }
452
+ function isModeValue(v) {
453
+ return typeof v === "string" && MODE_VALUES.includes(v);
454
+ }
455
+ function levelOf(mode) {
456
+ if (mode === "observe") return "audit";
457
+ return LEVELS.includes(mode ?? "") ? mode : "standard";
458
+ }
459
+ function isRecordOnly(mode) {
460
+ return levelOf(mode) === "audit";
461
+ }
462
+ function reviewsEveryInline(mode) {
463
+ const level = levelOf(mode);
464
+ return level === "strict" || level === "block-all";
465
+ }
466
+ function isBlockAll(mode) {
467
+ return levelOf(mode) === "block-all";
468
+ }
469
+ function levelCheckValue(mode, id) {
470
+ return LEVEL_CHECKS[levelOf(mode)][id];
471
+ }
472
+ function levelChecks(mode) {
473
+ return LEVEL_CHECKS[levelOf(mode)];
474
+ }
475
+ function levelPackageSignals(mode) {
476
+ return levelOf(mode) === "soft" ? { newPackage: "off", installScript: "off" } : void 0;
477
+ }
356
478
  function isVerdict(v) {
357
479
  return v === "off" || v === "log" || v === "review" || v === "block";
358
480
  }
@@ -403,7 +525,7 @@ function isLockedCheck(def) {
403
525
  function ssrfCheckId(tier) {
404
526
  return isStrictGatedTier(tier) ? "network.internal-addresses" : "network.metadata";
405
527
  }
406
- function unwrapRuleLabel(s) {
528
+ function ruleNameInLabel(s) {
407
529
  let rest = s;
408
530
  const override = rest.lastIndexOf(OVERRIDE_PREFIX);
409
531
  if (override >= 0) rest = rest.slice(override + OVERRIDE_PREFIX.length).trimStart();
@@ -419,7 +541,7 @@ function checkIdForRule(nameOrLabel) {
419
541
  const s = nameOrLabel.trim();
420
542
  const exact = LEGACY_TO_ID.get(s);
421
543
  if (exact) return exact;
422
- const unwrapped = unwrapRuleLabel(s);
544
+ const unwrapped = ruleNameInLabel(s);
423
545
  if (unwrapped !== void 0) return checkIdForRule(unwrapped);
424
546
  if (s.startsWith("shield:project-jail:")) {
425
547
  return PROJECT_JAIL_BLOCK_READ.test(s) ? "data.credential-files" : "data.credential-files-other";
@@ -498,19 +620,21 @@ function resolveCheck(settings, id) {
498
620
  if (!def) return void 0;
499
621
  if (isLockedCheck(def)) return { id, value: "block", source: "locked" };
500
622
  const floor = def.floor ? CHECK_VERDICT_RANK[def.floor] : 0;
623
+ const levelDefault = levelCheckValue(settings.mode, id) ?? def.defaultValue;
624
+ const sourceOf = (value2) => value2 === levelDefault ? "default" : "configured";
501
625
  const explicit = settings.checks?.[id];
502
626
  if (isVerdict(explicit)) {
503
627
  const value2 = CHECK_VERDICT_RANK[explicit] < floor ? def.floor : explicit;
504
- return { id, value: value2, source: value2 === def.defaultValue ? "default" : "configured" };
628
+ return { id, value: value2, source: sourceOf(value2) };
505
629
  }
506
630
  if (def.pack) {
507
631
  const on = settings.appliedShields?.includes(def.pack) ?? false;
508
632
  return { id, value: on ? def.defaultValue : "off", source: "default" };
509
633
  }
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;
513
- return { id, value, source: value === def.defaultValue ? "default" : "configured" };
634
+ const read2 = LEGACY_READERS[id]?.(settings);
635
+ if (read2 === void 0) return { id, value: levelDefault, source: "default" };
636
+ const value = CHECK_VERDICT_RANK[read2] < floor ? def.floor : read2;
637
+ return { id, value, source: sourceOf(value) };
514
638
  }
515
639
  function checkValue(settings, id) {
516
640
  return resolveCheck(settings, id)?.value ?? "off";
@@ -577,6 +701,102 @@ function scanViews(text) {
577
701
  }
578
702
  return views;
579
703
  }
704
+ function isAgentInstructionPath(absPath, root) {
705
+ const r = root.replace(/\/$/, "");
706
+ if (!(absPath === r || absPath.startsWith(r + "/"))) return false;
707
+ const rel = absPath.slice(r.length + 1);
708
+ if (AGENT_INSTRUCTION_FILES.includes(rel)) return true;
709
+ return AGENT_INSTRUCTION_DIRS.some((d) => rel === d || rel.startsWith(d + "/"));
710
+ }
711
+ function isInside(root, p) {
712
+ const r = root.replace(/\/$/, "") || "/";
713
+ return p === r || p.startsWith(r === "/" ? "/" : r + "/");
714
+ }
715
+ function isPasswordHashPath(p) {
716
+ return PASSWORD_HASH_FILES.includes(p);
717
+ }
718
+ function isLegacySystemPath(p) {
719
+ return LEGACY_SYSTEM_PATH_RE.test(p);
720
+ }
721
+ function normalizeFilePath(p, cwd = "", home = "~") {
722
+ if (!p.startsWith("/") && !p.startsWith("~") && cwd) p = `${cwd}/${p}`;
723
+ if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
724
+ const rooted = p.startsWith("/");
725
+ const parts = [];
726
+ for (const part of p.split("/")) {
727
+ if (!part || part === ".") continue;
728
+ if (part === ".." && parts.length && parts.at(-1) !== "..") parts.pop();
729
+ else if (part !== ".." || !rooted) parts.push(part);
730
+ }
731
+ return (rooted ? "/" : "") + parts.join("/");
732
+ }
733
+ function inlineLanguageOf(interp) {
734
+ const i = interp.toLowerCase();
735
+ if (/^python[\d.]*$/.test(i)) return "python";
736
+ if (/^(?:node|tsx|ts-node|deno|bun)$/.test(i)) return "node";
737
+ if (i === "perl") return "perl";
738
+ if (i === "ruby" || i === "irb") return "ruby";
739
+ if (i === "php") return "php";
740
+ return null;
741
+ }
742
+ function resolvePythonAliases(code) {
743
+ const aliases = [...code.matchAll(/\bimport\s+([\w.]+)\s+as\s+(\w+)/g)].slice(0, 32);
744
+ let out = code;
745
+ for (const [, mod, alias] of aliases) {
746
+ if (alias === mod) continue;
747
+ out = out.replace(new RegExp(String.raw`(?<![\w.])${alias}\b`, "g"), mod);
748
+ }
749
+ return out;
750
+ }
751
+ function protectedLiteral(code, isProtected) {
752
+ let n = 0;
753
+ for (const m of code.matchAll(STRING_LITERAL)) {
754
+ if (++n > 2048) break;
755
+ const value = m[2].trim();
756
+ if (value && isProtected(value)) return value;
757
+ }
758
+ return null;
759
+ }
760
+ function amplifies(language, code) {
761
+ 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*\)/;
762
+ const m = loop.exec(code);
763
+ if (!m) return null;
764
+ const body = code.slice(m.index);
765
+ 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(
766
+ body
767
+ ) ? m[0] : null;
768
+ }
769
+ function judgeInlineCode(language, code, isProtected) {
770
+ if (code.length > MAX_INLINE_CODE) {
771
+ return { outcome: "unreadable", what: "is too long for node9 to read" };
772
+ }
773
+ const text = language === "python" ? resolvePythonAliases(code) : code;
774
+ for (const [category, re] of TABLES[language]) {
775
+ const m = re.exec(text);
776
+ if (!m) continue;
777
+ const evidence = m[0].trim().replace(/\s+/g, " ").slice(0, 60);
778
+ return {
779
+ outcome: category === "builds-code" ? "unreadable" : "dangerous",
780
+ what: WHAT[category],
781
+ evidence,
782
+ category
783
+ };
784
+ }
785
+ const path28 = protectedLiteral(text, isProtected);
786
+ if (path28) {
787
+ return {
788
+ outcome: "dangerous",
789
+ what: WHAT["reads-protected"],
790
+ evidence: path28.slice(0, 60),
791
+ category: "reads-protected"
792
+ };
793
+ }
794
+ const loop = amplifies(language, text);
795
+ if (loop) {
796
+ return { outcome: "dangerous", what: WHAT.amplifies, evidence: loop, category: "amplifies" };
797
+ }
798
+ return null;
799
+ }
580
800
  function isCatHeredocOrLit(part) {
581
801
  if (!part) return false;
582
802
  const t = syntax.NodeType(part);
@@ -645,6 +865,55 @@ function cachedNormalize(command, compute) {
645
865
  function normalizeCommandForPolicy(command) {
646
866
  return commandReadingsImpl(command).posix;
647
867
  }
868
+ function fileAccessView(command) {
869
+ const hit = accessViewCache.get(command);
870
+ if (hit !== void 0) return hit;
871
+ const view = fileAccessViewImpl(command);
872
+ if (accessViewCache.size >= ACCESS_VIEW_CACHE_MAX) {
873
+ const oldest = accessViewCache.keys().next().value;
874
+ if (oldest !== void 0) accessViewCache.delete(oldest);
875
+ }
876
+ accessViewCache.set(command, view);
877
+ return view;
878
+ }
879
+ function fileAccessViewImpl(command) {
880
+ const text = command.replace(INTERPRETER_LINE, "");
881
+ const f = parseShared(text);
882
+ if (f === PARSE_FAIL) return text;
883
+ const toCharIndex = byteOffsetToCharIndex(text);
884
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
885
+ const edits = [];
886
+ try {
887
+ syntax.Walk(f, (node) => {
888
+ if (!node) return false;
889
+ const n = node;
890
+ if (syntax.NodeType(n) !== "CallExpr") return true;
891
+ const args = n.Args || [];
892
+ const words = args.map((a) => resolveWordLiteral(a));
893
+ const head = unwrapCommandHead(words);
894
+ for (const i of /* @__PURE__ */ new Set([0, head])) {
895
+ const word = words[i];
896
+ if (i >= args.length || word === null || word === void 0 || !/[\\/]/.test(word)) {
897
+ continue;
898
+ }
899
+ if (!PROGRAM_DIR.test(word)) continue;
900
+ const name = word.split(/[\\/]/).pop() ?? "";
901
+ if (!name) continue;
902
+ const s = at(args[i].Pos().Offset());
903
+ const e = at(args[i].End().Offset());
904
+ if (s < 0 || e < 0) continue;
905
+ edits.push([s, e, name]);
906
+ }
907
+ return true;
908
+ });
909
+ } catch {
910
+ return text;
911
+ }
912
+ edits.sort((a, b) => b[0] - a[0]);
913
+ let out = text;
914
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
915
+ return out;
916
+ }
648
917
  function commandReadings(command) {
649
918
  const r = commandReadingsImpl(command);
650
919
  const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
@@ -1100,53 +1369,119 @@ function unwrapCommandHead(words, stopAtFind = false, stopAtDynamic = false) {
1100
1369
  }
1101
1370
  return i;
1102
1371
  }
1103
- function inlineExecStmt(stmt, pipeFed) {
1372
+ function heredocText(word) {
1373
+ const parts = word?.Parts ?? [];
1374
+ let out = "";
1375
+ for (const p of parts) {
1376
+ if (syntax.NodeType(p) !== "Lit") return null;
1377
+ out += p.Value ?? "";
1378
+ }
1379
+ return out;
1380
+ }
1381
+ function stdinCodeOf(redirs) {
1382
+ const heredoc = /* @__PURE__ */ new Set([deriveRedirOp("cat <<X\nX"), deriveRedirOp("cat <<-X\nX")]);
1383
+ const herestring = deriveRedirOp("cat <<< x");
1384
+ for (const r of redirs) {
1385
+ if (!r || !redirStdinOps().has(r.Op)) continue;
1386
+ if (heredoc.has(r.Op)) {
1387
+ const text = heredocText(r.Hdoc);
1388
+ return text === null ? { code: null, source: "variable" } : { code: text };
1389
+ }
1390
+ if (r.Op === herestring) {
1391
+ const text = resolveWordLiteral(r.Word);
1392
+ return text === null ? { code: null, source: "variable" } : { code: text };
1393
+ }
1394
+ return { code: null, source: "file" };
1395
+ }
1396
+ return { code: null, source: "file" };
1397
+ }
1398
+ function attachedCode(interp, flag) {
1399
+ const eq = flag.indexOf("=");
1400
+ if (flag.startsWith("--")) return eq >= 0 ? flag.slice(eq + 1) : null;
1401
+ if (/^(pwsh|powershell)/.test(interp)) return null;
1402
+ const letters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1403
+ const body = flag.slice(1);
1404
+ const at = [...body].findIndex((ch) => letters.includes(ch.toLowerCase()));
1405
+ if (at < 0) return null;
1406
+ let rest = body.slice(at + 1);
1407
+ if (letters === "ep" && /^[ep]*$/i.test(rest)) rest = "";
1408
+ return rest === "" ? null : rest;
1409
+ }
1410
+ function inlineExecCall(stmt, pipeFed) {
1104
1411
  const cmd = stmt?.Cmd;
1105
- if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1412
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return null;
1106
1413
  const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1107
- if (words.length === 0) return false;
1414
+ if (words.length === 0) return null;
1108
1415
  const headIdx = unwrapCommandHead(words);
1109
1416
  const rawHead = words[headIdx];
1110
- if (rawHead == null) return false;
1417
+ if (rawHead == null) return null;
1111
1418
  const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1112
- if (!INLINE_INTERPRETER.test(interp)) return false;
1419
+ if (!INLINE_INTERPRETER.test(interp)) return null;
1113
1420
  let args = words.slice(headIdx + 1);
1114
- if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1421
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") {
1422
+ const code = args[1] ?? null;
1423
+ return { interp, code: [code], source: code === null ? "variable" : void 0 };
1424
+ }
1115
1425
  if (INTERP_LEADING_TARGET.has(interp)) {
1116
1426
  const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1117
1427
  args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1118
1428
  }
1429
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1430
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1431
+ const fromStdin = () => {
1432
+ if (stdinFed) {
1433
+ const { code, source } = stdinCodeOf(redirs);
1434
+ return { interp, code: [code], source };
1435
+ }
1436
+ return { interp, code: [null], source: "pipe" };
1437
+ };
1119
1438
  let positionals2 = 0;
1120
1439
  let selectedProgram = false;
1440
+ const codes = [];
1121
1441
  for (let i = 0; i < args.length; i++) {
1122
1442
  const a = args[i];
1123
1443
  if (a == null) {
1444
+ if (codes.length > 0) break;
1124
1445
  positionals2++;
1125
1446
  selectedProgram = true;
1126
1447
  continue;
1127
1448
  }
1128
1449
  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);
1450
+ if (/^(bash|sh|zsh|dash|ksh)$/.test(interp)) {
1451
+ const payload = args[i + 1];
1452
+ if (payload == null) return { interp, code: [null], source: "variable" };
1453
+ return shellPayloadNeedsReview(payload) ? { interp, code: [null], source: "payload" } : null;
1454
+ }
1455
+ const attached = attachedCode(interp, a);
1456
+ if (attached !== null) {
1457
+ codes.push(attached);
1458
+ } else {
1459
+ codes.push(args[i + 1] ?? null);
1460
+ i++;
1461
+ }
1462
+ continue;
1463
+ }
1464
+ if (codes.length > 0) {
1465
+ if (a.startsWith("-")) continue;
1466
+ break;
1133
1467
  }
1134
1468
  if (a === "-m") {
1135
1469
  selectedProgram = true;
1136
1470
  continue;
1137
1471
  }
1138
- if (a === "-" && !selectedProgram) return true;
1472
+ if (a === "-" && !selectedProgram) return fromStdin();
1139
1473
  if (!a.startsWith("-")) {
1140
1474
  positionals2++;
1141
1475
  selectedProgram = true;
1142
1476
  }
1143
1477
  }
1144
- const redirs = stmt.Redirs || cmd.Redirs || [];
1145
- const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1478
+ if (codes.length > 0) {
1479
+ return { interp, code: codes, source: codes.includes(null) ? "variable" : void 0 };
1480
+ }
1146
1481
  if (positionals2 === 0 && (stdinFed || pipeFed)) {
1147
- if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1482
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return fromStdin();
1148
1483
  }
1149
- return false;
1484
+ return null;
1150
1485
  }
1151
1486
  function shellPayloadNeedsReview(payload) {
1152
1487
  if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
@@ -1179,7 +1514,7 @@ function shellPayloadNeedsReview(payload) {
1179
1514
  payload
1180
1515
  ))
1181
1516
  review = true;
1182
- if (!review && detectInlineExec(payload)) review = true;
1517
+ if (!review && inlineExecReview(payload) !== null) review = true;
1183
1518
  } catch {
1184
1519
  review = true;
1185
1520
  } finally {
@@ -1187,35 +1522,77 @@ function shellPayloadNeedsReview(payload) {
1187
1522
  }
1188
1523
  return review;
1189
1524
  }
1190
- function detectInlineExec(command) {
1525
+ function inlineExecCalls(command) {
1191
1526
  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;
1527
+ if (f === PARSE_FAIL) return PARSE_FAIL;
1528
+ const calls = [];
1198
1529
  try {
1199
1530
  syntax.Walk(f, (node) => {
1200
- if (!node || found) return false;
1531
+ if (!node || calls.length >= MAX_INLINE_CALLS) return false;
1201
1532
  const n = node;
1202
1533
  const t = syntax.NodeType(n);
1203
1534
  if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1204
- if (inlineExecStmt(n.Y, true)) {
1205
- found = true;
1206
- return false;
1207
- }
1535
+ const call = inlineExecCall(n.Y, true);
1536
+ if (call) calls.push(call);
1208
1537
  }
1209
- if (t === "Stmt" && inlineExecStmt(n, false)) {
1210
- found = true;
1211
- return false;
1538
+ if (t === "Stmt") {
1539
+ const call = inlineExecCall(n, false);
1540
+ if (call) calls.push(call);
1212
1541
  }
1213
1542
  return true;
1214
1543
  });
1215
1544
  } catch {
1216
- return found;
1217
1545
  }
1218
- return found;
1546
+ return calls;
1547
+ }
1548
+ function detectInlineExec(command) {
1549
+ const calls = inlineExecCalls(command);
1550
+ if (calls === PARSE_FAIL) return INLINE_EXEC_FALLBACK.test(command);
1551
+ return calls.length > 0;
1552
+ }
1553
+ function isProtectedCodePath(p) {
1554
+ if (matchSensitivePath(p) !== null) return true;
1555
+ return SYSTEM_AUTH_FILES.includes(p) || SYSTEM_AUTH_DIRS.some((d) => p === d || p.startsWith(d + "/"));
1556
+ }
1557
+ function inlineExecDescription(r) {
1558
+ 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.";
1559
+ return `${lead} Review the full script below before allowing it to execute.`;
1560
+ }
1561
+ function inlineExecReason(r) {
1562
+ const who = r.interp ? `This inline ${r.interp} code` : "This inline code";
1563
+ return r.evidence ? `${who} ${r.what} (${r.evidence})` : `${who} ${r.what}`;
1564
+ }
1565
+ function inlineExecReview(command) {
1566
+ const calls = inlineExecCalls(command);
1567
+ if (calls === PARSE_FAIL) {
1568
+ return INLINE_EXEC_FALLBACK.test(command) ? { outcome: "unreadable", interp: "", what: "could not be parsed" } : null;
1569
+ }
1570
+ let unreadable = calls.length >= MAX_INLINE_CALLS ? { outcome: "unreadable", interp: "", what: "runs more inline programs than node9 reads" } : null;
1571
+ for (const call of calls) {
1572
+ const language = inlineLanguageOf(call.interp);
1573
+ for (const code of call.code) {
1574
+ let finding = null;
1575
+ if (code === null) {
1576
+ finding = {
1577
+ outcome: "unreadable",
1578
+ interp: call.interp,
1579
+ what: UNREADABLE_SOURCE[call.source ?? "variable"]
1580
+ };
1581
+ } else if (language === null) {
1582
+ finding = {
1583
+ outcome: "unreadable",
1584
+ interp: call.interp,
1585
+ what: `is ${call.interp} code, which node9 does not read`
1586
+ };
1587
+ } else {
1588
+ const judged = judgeInlineCode(language, code, isProtectedCodePath);
1589
+ if (judged) finding = { ...judged, interp: call.interp };
1590
+ }
1591
+ if (finding?.outcome === "dangerous") return finding;
1592
+ if (finding && !unreadable) unreadable = finding;
1593
+ }
1594
+ }
1595
+ return unreadable;
1219
1596
  }
1220
1597
  function analyzeChmod777(command) {
1221
1598
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
@@ -2464,40 +2841,6 @@ function analyzeShellCommand(command) {
2464
2841
  }
2465
2842
  return { actions, paths, allTokens };
2466
2843
  }
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
2844
  function matchesPattern(text, patterns) {
2502
2845
  const p = Array.isArray(patterns) ? patterns : [patterns];
2503
2846
  if (p.length === 0) return false;
@@ -2508,9 +2851,21 @@ function matchesPattern(text, patterns) {
2508
2851
  const withoutDotSlash = text.replace(/^\.\//, "");
2509
2852
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2510
2853
  }
2511
- function getNestedValue(obj, path27) {
2854
+ function mcpServerFromToolName(toolName) {
2855
+ const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
2856
+ return match?.[1];
2857
+ }
2858
+ function ruleServerMatches(rule, serverLabel) {
2859
+ const label2 = typeof serverLabel === "string" ? serverLabel.trim().replace(/[\\/]/g, "-") || void 0 : void 0;
2860
+ if (rule.server) {
2861
+ if (!label2 || !matchesPattern(label2, rule.server)) return false;
2862
+ }
2863
+ if (rule.notServer && label2 && matchesPattern(label2, rule.notServer)) return false;
2864
+ return true;
2865
+ }
2866
+ function getNestedValue(obj, path28) {
2512
2867
  if (!obj || typeof obj !== "object") return null;
2513
- const segments2 = path27.split(".");
2868
+ const segments2 = path28.split(".");
2514
2869
  for (const seg of segments2) {
2515
2870
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2516
2871
  }
@@ -2530,23 +2885,26 @@ function evaluateSmartConditions(args, rule) {
2530
2885
  return val;
2531
2886
  };
2532
2887
  const readingsCache = /* @__PURE__ */ new Map();
2533
- const resolveFieldReadings = (field) => {
2534
- const cached = readingsCache.get(field);
2888
+ const resolveFieldReadings = (field, reading) => {
2889
+ const access = reading === "access" && field === "command";
2890
+ const key = access ? `${field}\0access` : field;
2891
+ const cached = readingsCache.get(key);
2535
2892
  if (cached) return cached;
2536
2893
  const primary = resolveField(field);
2537
2894
  if (primary === null) {
2538
- readingsCache.set(field, []);
2895
+ readingsCache.set(key, []);
2539
2896
  return [];
2540
2897
  }
2541
2898
  let out = [primary];
2542
2899
  if (field === "command") {
2543
2900
  const raw = getNestedValue(args, field);
2544
2901
  if (typeof raw === "string") {
2545
- const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
2546
- out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2902
+ const text = access ? fileAccessView(raw) : raw;
2903
+ const collapsed = commandReadings(text).map((r) => r.replace(/\s+/g, " ").trim());
2904
+ out = access ? [...new Set(collapsed)] : [.../* @__PURE__ */ new Set([primary, ...collapsed])];
2547
2905
  }
2548
2906
  }
2549
- readingsCache.set(field, out);
2907
+ readingsCache.set(key, out);
2550
2908
  return out;
2551
2909
  };
2552
2910
  const results = rule.conditions.map((cond) => {
@@ -2564,14 +2922,14 @@ function evaluateSmartConditions(args, rule) {
2564
2922
  if (val === null || !cond.value) return false;
2565
2923
  const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2566
2924
  if (!reM) return false;
2567
- return resolveFieldReadings(cond.field).some((v) => reM.test(v));
2925
+ return resolveFieldReadings(cond.field, cond.reading).some((v) => reM.test(v));
2568
2926
  }
2569
2927
  case "notMatches": {
2570
2928
  if (!cond.value) return false;
2571
2929
  if (val === null) return true;
2572
2930
  const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2573
2931
  if (!reN) return false;
2574
- return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
2932
+ return !resolveFieldReadings(cond.field, cond.reading).some((v) => reN.test(v));
2575
2933
  }
2576
2934
  case "matchesGlob":
2577
2935
  return val !== null && cond.value ? import_picomatch2.default.isMatch(val, cond.value) : false;
@@ -3413,35 +3771,6 @@ function collectShellFileAccesses(command, context = {}) {
3413
3771
  }
3414
3772
  return out;
3415
3773
  }
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
3774
  function incompleteFileVerdict(operation = "write") {
3446
3775
  return {
3447
3776
  decision: "review",
@@ -3493,8 +3822,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
3493
3822
  }
3494
3823
  return out;
3495
3824
  }
3496
- function within(path27, root) {
3497
- return path27 === root || path27.startsWith(root === "/" ? "/" : root + "/");
3825
+ function within(path28, root) {
3826
+ return path28 === root || path28.startsWith(root === "/" ? "/" : root + "/");
3498
3827
  }
3499
3828
  function hits(access, paths, directories, context) {
3500
3829
  const home = context.homeDirs?.[0] ?? "~";
@@ -4302,14 +4631,14 @@ function sensitivePathMatch(originalPath) {
4302
4631
  }
4303
4632
  function assertBuiltinPatternsAreSafe() {
4304
4633
  for (const p of DLP_PATTERNS) {
4305
- if (!(0, import_safe_regex23.default)(p.regex.source)) {
4634
+ if (!(0, import_safe_regex22.default)(p.regex.source)) {
4306
4635
  throw new Error(
4307
4636
  `[node9 engine] Builtin DLP pattern '${p.name}' is vulnerable to ReDoS: ${p.regex.source}`
4308
4637
  );
4309
4638
  }
4310
4639
  }
4311
4640
  for (const re of SENSITIVE_PATH_PATTERNS) {
4312
- if (!(0, import_safe_regex23.default)(re.source)) {
4641
+ if (!(0, import_safe_regex22.default)(re.source)) {
4313
4642
  throw new Error(
4314
4643
  `[node9 engine] Builtin sensitive-path pattern is vulnerable to ReDoS: ${re.source}`
4315
4644
  );
@@ -5055,6 +5384,20 @@ function extractPackageInstalls(command) {
5055
5384
  return true;
5056
5385
  });
5057
5386
  }
5387
+ function tuningFromLegacyPolicy(policy) {
5388
+ const out = {};
5389
+ const p = policy;
5390
+ if (policy.packageCheck?.registrySignals === false) {
5391
+ out["loading.malicious-package"] = { newPackage: "off", installScript: "off" };
5392
+ }
5393
+ for (const f of TUNING_FIELDS) {
5394
+ const [block, key] = f.legacy;
5395
+ const value = p[block]?.[key];
5396
+ if (value === void 0) continue;
5397
+ (out[f.checkId] ??= {})[f.name] = value;
5398
+ }
5399
+ return out;
5400
+ }
5058
5401
  function tuningToLegacyPolicy(policy, tuning) {
5059
5402
  const unknown = [];
5060
5403
  for (const [id, fields] of Object.entries(tuning)) {
@@ -5169,6 +5512,25 @@ function checkToLegacyPolicy(policy, id, v) {
5169
5512
  return false;
5170
5513
  }
5171
5514
  }
5515
+ function knobStatedChecks(policy) {
5516
+ const out = /* @__PURE__ */ new Set();
5517
+ const has = (o, key) => !!o && typeof o === "object" && o[key] !== void 0;
5518
+ const { dlp, egress, loopDetection, injectionScan, skillPinning, packageCheck } = policy;
5519
+ const uc = policy.untrustedContent;
5520
+ if (has(dlp, "enabled")) out.add("data.secrets");
5521
+ if (has(dlp, "reviewAction") || dlp?.enabled === false) out.add("data.secrets-weak");
5522
+ if (has(dlp, "pii")) out.add("data.pii");
5523
+ if (has(egress, "enabled") || has(egress, "mode")) out.add("network.unknown-host");
5524
+ if (has(egress, "ssrfStrict")) out.add("network.internal-addresses");
5525
+ if (has(loopDetection, "enabled")) out.add("behavior.loops");
5526
+ if (has(injectionScan, "enabled")) out.add("behavior.prompt-injection");
5527
+ if (has(skillPinning, "enabled") || has(skillPinning, "mode")) out.add("loading.skill-tamper");
5528
+ if (has(packageCheck, "enabled") || has(packageCheck, "onMalicious"))
5529
+ out.add("loading.malicious-package");
5530
+ if (uc && typeof uc === "object" && (uc.enabled === false || has(uc, "afterExposure")))
5531
+ out.add("behavior.untrusted-content");
5532
+ return out;
5533
+ }
5172
5534
  function isKnobGoverned(id) {
5173
5535
  return id in KNOB_GOVERNED;
5174
5536
  }
@@ -5433,9 +5795,9 @@ function bareToolName(toolName) {
5433
5795
  const parts = toolName.split("__");
5434
5796
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
5435
5797
  }
5436
- function valuesAt(args, path27) {
5798
+ function valuesAt(args, path28) {
5437
5799
  let cursors = [args];
5438
- for (const rawSegment of path27.split(".")) {
5800
+ for (const rawSegment of path28.split(".")) {
5439
5801
  const isArray = rawSegment.endsWith("[]");
5440
5802
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
5441
5803
  const next = [];
@@ -5466,8 +5828,8 @@ function extractToolDestinations(toolName, args) {
5466
5828
  try {
5467
5829
  const out = [];
5468
5830
  const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
5469
- for (const path27 of paths) {
5470
- for (const value of valuesAt(args, path27)) {
5831
+ for (const path28 of paths) {
5832
+ for (const value of valuesAt(args, path28)) {
5471
5833
  const host2 = hostOf(value);
5472
5834
  if (host2) out.push({ host: host2, binary: toolName, raw: value });
5473
5835
  }
@@ -5493,8 +5855,8 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5493
5855
  try {
5494
5856
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
5495
5857
  if (!paths) return null;
5496
- for (const path27 of paths) {
5497
- for (const value of valuesAt(args, path27)) {
5858
+ for (const path28 of paths) {
5859
+ for (const value of valuesAt(args, path28)) {
5498
5860
  const host2 = hostOf(value);
5499
5861
  if (!host2) continue;
5500
5862
  const m = classifySsrf(host2);
@@ -5503,7 +5865,7 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
5503
5865
  const decision = ssrfFloorDecision(m, written, opts);
5504
5866
  if (decision === null) continue;
5505
5867
  const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
5506
- return { ...m, argPath: path27, host: host2, reason, decision };
5868
+ return { ...m, argPath: path28, host: host2, reason, decision };
5507
5869
  }
5508
5870
  }
5509
5871
  return null;
@@ -5883,11 +6245,15 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5883
6245
  };
5884
6246
  const ruleCheck = (rule) => {
5885
6247
  if (rule.verdict === "allow") return void 0;
5886
- if (!rule.builtin || rule.pinned) return void 0;
6248
+ if (!rule.builtin) return void 0;
5887
6249
  const id = checkIdForRule(rule.name);
5888
6250
  if (!id) return void 0;
5889
6251
  const def = getCheck(id);
5890
6252
  if (!def) return void 0;
6253
+ if (rule.pinned) {
6254
+ const explicit = config.policy.checks?.[id];
6255
+ return def.pack && config.policy.checkSources?.[id] === "workspace" && isVerdict(explicit) ? { id, value: explicit } : void 0;
6256
+ }
5891
6257
  if (rule.name?.startsWith("shield:") && !def.pack) return void 0;
5892
6258
  if (def.pack) {
5893
6259
  const explicit = config.policy.checks?.[id];
@@ -5895,8 +6261,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5895
6261
  }
5896
6262
  return { id, value: cv(id) };
5897
6263
  };
6264
+ const serverLabel = context.mcpServer ?? mcpServerFromToolName(toolName);
5898
6265
  const matches = config.policy.smartRules.filter((rule) => {
5899
6266
  if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
6267
+ if (!ruleServerMatches(rule, serverLabel)) return false;
5900
6268
  if (astSuppressed(rule)) return false;
5901
6269
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
5902
6270
  if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
@@ -5973,11 +6341,13 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5973
6341
  if (v?.logged) noteLogged(v);
5974
6342
  else if (v) candidates.push(v);
5975
6343
  };
5976
- if (detectInlineExec(shellCommand)) {
6344
+ const inline = reviewsEveryInline(config.settings.mode) ? detectInlineExec(shellCommand) ? void 0 : null : inlineExecReview(shellCommand);
6345
+ if (inline !== null) {
5977
6346
  consider("commands.inline-exec", {
5978
6347
  decision: "review",
5979
6348
  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.",
6349
+ ...inline ? { reason: inlineExecReason(inline) } : {},
6350
+ ruleDescription: inlineExecDescription(inline),
5981
6351
  tier: 3,
5982
6352
  checkId: "commands.inline-exec"
5983
6353
  });
@@ -6031,21 +6401,17 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
6031
6401
  if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
6032
6402
  const prov = checkProvenance2(firstToken, cwd);
6033
6403
  if (prov.trustLevel === "suspect") {
6034
- const value = cv("commands.temp-binary");
6035
- const v = applyCheckValue(
6036
- value === "review" && config.settings.mode === "strict" ? "block" : value,
6037
- {
6038
- decision: "review",
6039
- blockedByLabel: "Node9: Suspect Binary",
6040
- reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
6041
- tier: 3,
6042
- checkId: "commands.temp-binary"
6043
- }
6044
- );
6404
+ const v = applyCheckValue(cv("commands.temp-binary"), {
6405
+ decision: "review",
6406
+ blockedByLabel: "Node9: Suspect Binary",
6407
+ reason: `Binary "${firstToken}" resolved to ${prov.resolvedPath} \u2014 ${prov.reason}`,
6408
+ tier: 3,
6409
+ checkId: "commands.temp-binary"
6410
+ });
6045
6411
  if (v?.logged) noteLogged(v);
6046
6412
  else if (v) return v;
6047
6413
  }
6048
- if (prov.trustLevel === "unknown" && config.settings.mode === "strict") {
6414
+ if (prov.trustLevel === "unknown" && isBlockAll(config.settings.mode)) {
6049
6415
  return {
6050
6416
  decision: "review",
6051
6417
  blockedByLabel: "Node9: Unknown Binary (strict mode)",
@@ -6136,7 +6502,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
6136
6502
  if (v?.logged) noteLogged(v);
6137
6503
  else if (v) return v;
6138
6504
  }
6139
- if (config.settings.mode === "strict") {
6505
+ if (isBlockAll(config.settings.mode)) {
6140
6506
  if (activeEnvironment?.requireApproval === false) return allowed();
6141
6507
  return {
6142
6508
  decision: "review",
@@ -7067,7 +7433,7 @@ function matchCanaryArgs(args, values) {
7067
7433
  return null;
7068
7434
  }
7069
7435
  }
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;
7436
+ 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, LEVELS, MODE_VALUES, SOFT, STRICT, BLOCK_ALL, LEVEL_CHECKS, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, MAX_INLINE_CODE, WHAT, OUTSIDE, pyImport, either, PYTHON, NODE, PERL, RUBY, PHP, TABLES, STRING_LITERAL, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, INTERPRETER_LINE, PROGRAM_DIR, ACCESS_VIEW_CACHE_MAX, accessViewCache, MAX_READING_PAYLOADS, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, CURL_SOURCE_FLAGS, CURL_VALUE_LETTERS, WGET_VALUE_LETTERS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, MAX_PAYLOAD_NESTING, payloadNesting, MAX_INLINE_CALLS, INLINE_EXEC_FALLBACK, UNREADABLE_SOURCE, SCRIPT_RUNNERS, SCRIPT_RUNNER_VALUE_FLAGS, MAX_SCRIPT_OPERANDS, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, SECRET_VIEW_LIMITS, IBAN_LENGTH, IBAN_BBAN_SHAPE, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, CF_ODD, CF_EVEN, SPANISH_LETTERS, HETU_CHARS, MAX, UNTRUSTED_TOOLS, SIGNALS, MENTION_QUOTE_RE, MENTION_WORD_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, CIPHER_KEY_NAME_RE, CIPHER_NAME_PARTS, CIPHER_KEY_VALUE_RE, LARAVEL_KEY_VALUE_RE, SQL_PASSWORD_VALUE_RE, GRAFANA_KEY_VALUE_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DB_URL_PASSWORD_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, MCP_DESTINATION_KEYS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE, CARD_PATTERNS, PII_ROUTING_RE, ROUTING_LABEL_RE, ROUTING_LABEL_WINDOW, NATIONAL_ID_SHAPES, NATIONAL_ID_LABEL_RE, NATIONAL_ID_LABEL_WINDOW, NATIONAL_ID_GUARD_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, IBAN_DESTINATION_KEYS, WHOLE_IBAN_RE, JSON_ESCAPE_RE, JSON_ESCAPES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
7071
7437
  var init_dist = __esm({
7072
7438
  "packages/policy-engine/dist/index.mjs"() {
7073
7439
  "use strict";
@@ -7075,15 +7441,17 @@ var init_dist = __esm({
7075
7441
  import_safe_regex2 = __toESM(require("safe-regex2"), 1);
7076
7442
  import_picomatch2 = __toESM(require("picomatch"), 1);
7077
7443
  import_mvdan_sh = __toESM(require("mvdan-sh"), 1);
7078
- import_safe_regex22 = __toESM(require("safe-regex2"), 1);
7079
7444
  import_mvdan_sh2 = __toESM(require("mvdan-sh"), 1);
7080
- import_safe_regex23 = __toESM(require("safe-regex2"), 1);
7445
+ import_safe_regex22 = __toESM(require("safe-regex2"), 1);
7081
7446
  import_crypto2 = require("crypto");
7082
7447
  import_crypto3 = require("crypto");
7083
7448
  import_mvdan_sh3 = __toESM(require("mvdan-sh"), 1);
7084
7449
  import_crypto4 = require("crypto");
7085
7450
  import_mvdan_sh4 = __toESM(require("mvdan-sh"), 1);
7086
7451
  import_crypto5 = __toESM(require("crypto"), 1);
7452
+ MAX_REGEX_LENGTH = 256;
7453
+ REGEX_CACHE_MAX = 500;
7454
+ regexCache = /* @__PURE__ */ new Map();
7087
7455
  aws_default = {
7088
7456
  name: "aws",
7089
7457
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -7385,77 +7753,6 @@ var init_dist = __esm({
7385
7753
  ],
7386
7754
  dangerousWords: ["wipefs"]
7387
7755
  };
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
7756
  github_default = {
7460
7757
  name: "github",
7461
7758
  description: "Protects GitHub repositories from destructive AI operations",
@@ -7482,56 +7779,6 @@ var init_dist = __esm({
7482
7779
  ],
7483
7780
  dangerousWords: []
7484
7781
  };
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
7782
  k8s_default = {
7536
7783
  name: "k8s",
7537
7784
  description: "Protects Kubernetes clusters from destructive AI operations",
@@ -7784,7 +8031,7 @@ var init_dist = __esm({
7784
8031
  {
7785
8032
  field: "command",
7786
8033
  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|$|[;&|>)<])",
8034
+ 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
8035
  flags: "i"
7789
8036
  }
7790
8037
  ],
@@ -7798,7 +8045,7 @@ var init_dist = __esm({
7798
8045
  {
7799
8046
  field: "command",
7800
8047
  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)",
8048
+ 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
8049
  flags: "i"
7803
8050
  }
7804
8051
  ],
@@ -7864,272 +8111,879 @@ var init_dist = __esm({
7864
8111
  ],
7865
8112
  dangerousWords: []
7866
8113
  };
7867
- publishing_default = {
7868
- name: "publishing",
7869
- description: "Asks before something goes public or ships outside the organisation (a release, a package, a deploy, a post), whatever the request said. The same switch covers the shell commands that publish.",
7870
- aliases: ["release", "deploy"],
8114
+ redis_default = {
8115
+ name: "redis",
8116
+ description: "Protects Redis instances from destructive AI operations",
8117
+ aliases: [],
8118
+ _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.",
7871
8119
  smartRules: [
7872
8120
  {
7873
- name: "shield:publishing:review-release",
7874
- tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
7875
- conditions: [],
7876
- verdict: "review",
7877
- reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
7878
- },
7879
- {
7880
- name: "shield:publishing:review-package",
7881
- tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
7882
- conditions: [],
7883
- verdict: "review",
7884
- reason: "This publishes a package. node9 asks before publishing (Publishing shield)."
7885
- },
7886
- {
7887
- name: "shield:publishing:review-deploy",
7888
- tool: "{,mcp__*__}{deploy,create_deployment,deploy_site,promote_deployment,trigger_deploy,deploy_to_production,rollout}",
7889
- conditions: [],
7890
- verdict: "review",
7891
- reason: "This deploys to an environment other people use. node9 asks before publishing (Publishing shield)."
7892
- },
7893
- {
7894
- name: "shield:publishing:review-post",
7895
- tool: "{,mcp__*__}{post_tweet,create_tweet,post_webpage,publish_post,create_post,publish_page,send_announcement,post_announcement,create_linkedin_post,schedule_post}",
7896
- conditions: [],
7897
- verdict: "review",
7898
- reason: "This posts outside the organisation. node9 asks before publishing (Publishing shield)."
7899
- },
7900
- {
7901
- name: "shield:publishing:review-visibility",
7902
- tool: "{,mcp__*__}{set_visibility,make_public,set_public,publish_repository,change_visibility,update_visibility}",
7903
- conditions: [],
7904
- verdict: "review",
7905
- reason: "This makes something public. node9 asks before publishing (Publishing shield)."
7906
- },
7907
- {
7908
- name: "shield:publishing:review-npm-publish",
7909
- tool: "bash",
8121
+ name: "shield:redis:block-flushall",
8122
+ tool: "*",
7910
8123
  conditions: [
7911
8124
  {
7912
8125
  field: "command",
7913
8126
  op: "matches",
7914
- value: "(?:^|[^\\w-])(?:npm|pnpm)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8127
+ value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
7915
8128
  flags: "i"
7916
8129
  }
7917
8130
  ],
7918
- verdict: "review",
7919
- reason: "npm publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
8131
+ verdict: "block",
8132
+ reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
7920
8133
  },
7921
8134
  {
7922
- name: "shield:publishing:review-yarn-publish",
7923
- tool: "bash",
8135
+ name: "shield:redis:block-flushdb",
8136
+ tool: "*",
7924
8137
  conditions: [
7925
8138
  {
7926
8139
  field: "command",
7927
8140
  op: "matches",
7928
- value: "(?:^|[^\\w-])(?:yarn)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8141
+ value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
7929
8142
  flags: "i"
7930
8143
  }
7931
8144
  ],
7932
- verdict: "review",
7933
- reason: "yarn publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
8145
+ verdict: "block",
8146
+ reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
7934
8147
  },
7935
8148
  {
7936
- name: "shield:publishing:review-twine-upload",
7937
- tool: "bash",
8149
+ name: "shield:redis:block-config-resetstat",
8150
+ tool: "*",
7938
8151
  conditions: [
7939
8152
  {
7940
8153
  field: "command",
7941
8154
  op: "matches",
7942
- value: "(?:^|[^\\w-])(?:twine)(?![\\w-])[^;&|]{0,200}?\\s(?:upload)(?![\\w-])",
8155
+ value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
7943
8156
  flags: "i"
7944
8157
  }
7945
8158
  ],
7946
- verdict: "review",
7947
- reason: "twine upload publishes to PyPI. node9 asks before publishing (Publishing shield)."
8159
+ verdict: "block",
8160
+ reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
7948
8161
  },
7949
8162
  {
7950
- name: "shield:publishing:review-python-publish",
7951
- tool: "bash",
8163
+ name: "shield:redis:review-config-set",
8164
+ tool: "*",
7952
8165
  conditions: [
7953
8166
  {
7954
8167
  field: "command",
7955
8168
  op: "matches",
7956
- value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8169
+ value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
7957
8170
  flags: "i"
7958
8171
  }
7959
8172
  ],
7960
8173
  verdict: "review",
7961
- reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
8174
+ reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
7962
8175
  },
7963
8176
  {
7964
- name: "shield:publishing:review-release-tool-publish",
7965
- tool: "bash",
8177
+ name: "shield:redis:review-del-wildcard",
8178
+ tool: "*",
7966
8179
  conditions: [
7967
8180
  {
7968
8181
  field: "command",
7969
8182
  op: "matches",
7970
- value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8183
+ value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
7971
8184
  flags: "i"
7972
8185
  }
7973
8186
  ],
7974
8187
  verdict: "review",
7975
- reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
7976
- },
8188
+ reason: "Wildcard key deletion requires human approval (Redis shield)"
8189
+ }
8190
+ ],
8191
+ dangerousWords: ["FLUSHALL", "FLUSHDB"]
8192
+ };
8193
+ finance_default = {
8194
+ name: "finance",
8195
+ 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.",
8196
+ aliases: ["payments", "money"],
8197
+ smartRules: [
7977
8198
  {
7978
- name: "shield:publishing:review-cargo-publish",
7979
- tool: "bash",
7980
- conditions: [
7981
- {
7982
- field: "command",
7983
- op: "matches",
7984
- value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7985
- flags: "i"
7986
- }
7987
- ],
8199
+ name: "shield:finance:review-refund",
8200
+ tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
8201
+ conditions: [],
7988
8202
  verdict: "review",
7989
- reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
8203
+ reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
7990
8204
  },
7991
8205
  {
7992
- name: "shield:publishing:review-gem-push",
7993
- tool: "bash",
7994
- conditions: [
7995
- {
7996
- field: "command",
7997
- op: "matches",
7998
- value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
7999
- flags: "i"
8000
- }
8001
- ],
8206
+ name: "shield:finance:review-payout",
8207
+ tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
8208
+ conditions: [],
8002
8209
  verdict: "review",
8003
- reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
8210
+ reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
8004
8211
  },
8005
8212
  {
8006
- name: "shield:publishing:review-docker-push",
8213
+ name: "shield:finance:review-transfer",
8214
+ tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction}",
8215
+ conditions: [],
8216
+ verdict: "review",
8217
+ reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
8218
+ },
8219
+ {
8220
+ name: "shield:finance:review-charge",
8221
+ 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}",
8222
+ conditions: [],
8223
+ verdict: "review",
8224
+ reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
8225
+ },
8226
+ {
8227
+ name: "shield:finance:review-payment-link",
8228
+ tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
8229
+ conditions: [],
8230
+ verdict: "review",
8231
+ reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
8232
+ },
8233
+ {
8234
+ name: "shield:finance:review-subscription",
8235
+ tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
8236
+ conditions: [],
8237
+ verdict: "review",
8238
+ reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
8239
+ },
8240
+ {
8241
+ name: "shield:finance:review-dispute",
8242
+ tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
8243
+ conditions: [],
8244
+ verdict: "review",
8245
+ reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
8246
+ },
8247
+ {
8248
+ name: "shield:finance:review-card",
8249
+ tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
8250
+ conditions: [],
8251
+ verdict: "review",
8252
+ reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
8253
+ },
8254
+ {
8255
+ name: "shield:finance:review-withdraw",
8256
+ tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
8257
+ conditions: [],
8258
+ verdict: "review",
8259
+ reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
8260
+ }
8261
+ ],
8262
+ dangerousWords: []
8263
+ };
8264
+ identity_default = {
8265
+ name: "identity",
8266
+ 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.",
8267
+ aliases: ["access", "iam"],
8268
+ smartRules: [
8269
+ {
8270
+ name: "shield:identity:review-password",
8271
+ tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
8272
+ conditions: [],
8273
+ verdict: "review",
8274
+ reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
8275
+ },
8276
+ {
8277
+ name: "shield:identity:review-key",
8278
+ 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}",
8279
+ conditions: [],
8280
+ verdict: "review",
8281
+ reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
8282
+ },
8283
+ {
8284
+ name: "shield:identity:review-access",
8285
+ 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}",
8286
+ conditions: [],
8287
+ verdict: "review",
8288
+ reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
8289
+ },
8290
+ {
8291
+ name: "shield:identity:review-membership",
8292
+ 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}",
8293
+ conditions: [],
8294
+ verdict: "review",
8295
+ reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
8296
+ },
8297
+ {
8298
+ name: "shield:identity:review-user",
8299
+ 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}",
8300
+ conditions: [],
8301
+ verdict: "review",
8302
+ reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
8303
+ },
8304
+ {
8305
+ name: "shield:identity:review-mfa",
8306
+ tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
8307
+ conditions: [],
8308
+ verdict: "review",
8309
+ reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
8310
+ }
8311
+ ],
8312
+ dangerousWords: []
8313
+ };
8314
+ publishing_default = {
8315
+ name: "publishing",
8316
+ description: "Asks before something goes public or ships outside the organisation (a release, a package, a deploy, a post), whatever the request said. The same switch covers the shell commands that publish.",
8317
+ aliases: ["release", "deploy"],
8318
+ smartRules: [
8319
+ {
8320
+ name: "shield:publishing:review-release",
8321
+ tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
8322
+ conditions: [],
8323
+ verdict: "review",
8324
+ reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
8325
+ },
8326
+ {
8327
+ name: "shield:publishing:review-package",
8328
+ tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
8329
+ conditions: [],
8330
+ verdict: "review",
8331
+ reason: "This publishes a package. node9 asks before publishing (Publishing shield)."
8332
+ },
8333
+ {
8334
+ name: "shield:publishing:review-deploy",
8335
+ tool: "{,mcp__*__}{deploy,create_deployment,deploy_site,promote_deployment,trigger_deploy,deploy_to_production,rollout}",
8336
+ conditions: [],
8337
+ verdict: "review",
8338
+ reason: "This deploys to an environment other people use. node9 asks before publishing (Publishing shield)."
8339
+ },
8340
+ {
8341
+ name: "shield:publishing:review-post",
8342
+ tool: "{,mcp__*__}{post_tweet,create_tweet,post_webpage,publish_post,create_post,publish_page,send_announcement,post_announcement,create_linkedin_post,schedule_post}",
8343
+ conditions: [],
8344
+ verdict: "review",
8345
+ reason: "This posts outside the organisation. node9 asks before publishing (Publishing shield)."
8346
+ },
8347
+ {
8348
+ name: "shield:publishing:review-visibility",
8349
+ tool: "{,mcp__*__}{set_visibility,make_public,set_public,publish_repository,change_visibility,update_visibility}",
8350
+ conditions: [],
8351
+ verdict: "review",
8352
+ reason: "This makes something public. node9 asks before publishing (Publishing shield)."
8353
+ },
8354
+ {
8355
+ name: "shield:publishing:review-npm-publish",
8007
8356
  tool: "bash",
8008
8357
  conditions: [
8009
8358
  {
8010
8359
  field: "command",
8011
8360
  op: "matches",
8012
- value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
8361
+ value: "(?:^|[^\\w-])(?:npm|pnpm)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8013
8362
  flags: "i"
8014
8363
  }
8015
8364
  ],
8016
8365
  verdict: "review",
8017
- reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
8366
+ reason: "npm publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
8018
8367
  },
8019
8368
  {
8020
- name: "shield:publishing:review-gh-release",
8369
+ name: "shield:publishing:review-yarn-publish",
8021
8370
  tool: "bash",
8022
8371
  conditions: [
8023
8372
  {
8024
8373
  field: "command",
8025
8374
  op: "matches",
8026
- value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
8375
+ value: "(?:^|[^\\w-])(?:yarn)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8027
8376
  flags: "i"
8028
8377
  }
8029
8378
  ],
8030
8379
  verdict: "review",
8031
- reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
8032
- }
8033
- ],
8034
- dangerousWords: []
8035
- };
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: [
8380
+ reason: "yarn publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
8381
+ },
8042
8382
  {
8043
- name: "shield:redis:block-flushall",
8044
- tool: "*",
8383
+ name: "shield:publishing:review-twine-upload",
8384
+ tool: "bash",
8045
8385
  conditions: [
8046
8386
  {
8047
8387
  field: "command",
8048
8388
  op: "matches",
8049
- value: "(redis|valkey)-cli.*\\bFLUSHALL\\b|\\bFLUSHALL\\b.*(redis|valkey)-cli|^ ?FLUSHALL\\b|\\.flushall\\s*\\(",
8389
+ value: "(?:^|[^\\w-])(?:twine)(?![\\w-])[^;&|]{0,200}?\\s(?:upload)(?![\\w-])",
8050
8390
  flags: "i"
8051
8391
  }
8052
8392
  ],
8053
- verdict: "block",
8054
- reason: "FLUSHALL deletes every key in every database \u2014 blocked by Redis shield"
8393
+ verdict: "review",
8394
+ reason: "twine upload publishes to PyPI. node9 asks before publishing (Publishing shield)."
8055
8395
  },
8056
8396
  {
8057
- name: "shield:redis:block-flushdb",
8058
- tool: "*",
8397
+ name: "shield:publishing:review-python-publish",
8398
+ tool: "bash",
8059
8399
  conditions: [
8060
8400
  {
8061
8401
  field: "command",
8062
8402
  op: "matches",
8063
- value: "(redis|valkey)-cli.*\\bFLUSHDB\\b|\\bFLUSHDB\\b.*(redis|valkey)-cli|^ ?FLUSHDB\\b|\\.flushdb\\s*\\(",
8403
+ value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8064
8404
  flags: "i"
8065
8405
  }
8066
8406
  ],
8067
- verdict: "block",
8068
- reason: "FLUSHDB deletes all keys in the current database \u2014 blocked by Redis shield"
8407
+ verdict: "review",
8408
+ reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
8069
8409
  },
8070
8410
  {
8071
- name: "shield:redis:block-config-resetstat",
8072
- tool: "*",
8411
+ name: "shield:publishing:review-release-tool-publish",
8412
+ tool: "bash",
8073
8413
  conditions: [
8074
8414
  {
8075
8415
  field: "command",
8076
8416
  op: "matches",
8077
- value: "(redis|valkey)-cli.*CONFIG\\s+RESETSTAT|^ ?CONFIG\\s+RESETSTAT",
8417
+ value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8078
8418
  flags: "i"
8079
8419
  }
8080
8420
  ],
8081
- verdict: "block",
8082
- reason: "CONFIG RESETSTAT resets server statistics irreversibly \u2014 blocked by Redis shield"
8421
+ verdict: "review",
8422
+ reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
8083
8423
  },
8084
8424
  {
8085
- name: "shield:redis:review-config-set",
8086
- tool: "*",
8425
+ name: "shield:publishing:review-cargo-publish",
8426
+ tool: "bash",
8087
8427
  conditions: [
8088
8428
  {
8089
8429
  field: "command",
8090
8430
  op: "matches",
8091
- value: "(redis|valkey)-cli.*\\bCONFIG\\s+SET\\b|\\bCONFIG\\s+SET\\b.*(redis|valkey)-cli|^ ?CONFIG\\s+SET\\b",
8431
+ value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
8092
8432
  flags: "i"
8093
8433
  }
8094
8434
  ],
8095
8435
  verdict: "review",
8096
- reason: "CONFIG SET changes live server configuration and requires human approval (Redis shield)"
8436
+ reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
8097
8437
  },
8098
8438
  {
8099
- name: "shield:redis:review-del-wildcard",
8100
- tool: "*",
8439
+ name: "shield:publishing:review-gem-push",
8440
+ tool: "bash",
8101
8441
  conditions: [
8102
8442
  {
8103
8443
  field: "command",
8104
8444
  op: "matches",
8105
- value: "(redis|valkey)-cli.*\\bDEL\\b.*[*?\\[]|^ ?DEL\\b.*[*?\\[]|-cli.*--scan.*xargs.*del",
8445
+ value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
8106
8446
  flags: "i"
8107
8447
  }
8108
8448
  ],
8109
8449
  verdict: "review",
8110
- reason: "Wildcard key deletion requires human approval (Redis shield)"
8450
+ reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
8451
+ },
8452
+ {
8453
+ name: "shield:publishing:review-docker-push",
8454
+ tool: "bash",
8455
+ conditions: [
8456
+ {
8457
+ field: "command",
8458
+ op: "matches",
8459
+ value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
8460
+ flags: "i"
8461
+ }
8462
+ ],
8463
+ verdict: "review",
8464
+ reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
8465
+ },
8466
+ {
8467
+ name: "shield:publishing:review-gh-release",
8468
+ tool: "bash",
8469
+ conditions: [
8470
+ {
8471
+ field: "command",
8472
+ op: "matches",
8473
+ value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
8474
+ flags: "i"
8475
+ }
8476
+ ],
8477
+ verdict: "review",
8478
+ reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
8111
8479
  }
8112
8480
  ],
8113
- dangerousWords: ["FLUSHALL", "FLUSHDB"]
8481
+ dangerousWords: []
8114
8482
  };
8483
+ ACTION_TEMPLATES = {
8484
+ finance: finance_default.smartRules,
8485
+ identity: identity_default.smartRules,
8486
+ publishing: publishing_default.smartRules
8487
+ };
8488
+ ACTION_CHECKS = {
8489
+ finance: { checkId: "actions.money", slug: "money", label: "Money check" },
8490
+ identity: { checkId: "actions.access", slug: "access", label: "Accounts and access check" },
8491
+ publishing: { checkId: "actions.publish", slug: "publish", label: "Publishing check" }
8492
+ };
8493
+ GAP = "[^;&|]{0,200}?";
8494
+ prog = (alt) => `(?:^|[^\\w-])(?:${alt})(?![\\w-])`;
8495
+ httpTo = (host2) => prog("curl|wget|http|https|xh") + "[^;&|]{0,400}?" + host2;
8496
+ httpWrite = (methods) => ({
8497
+ field: "command",
8498
+ op: "matches",
8499
+ 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)`
8500
+ });
8501
+ HTTP_WRITE = httpWrite("POST|DELETE");
8502
+ HTTP_WRITE_ANY = httpWrite("POST|PUT|PATCH|DELETE");
8503
+ NOT_STRIPE_TEST = {
8504
+ field: "command",
8505
+ op: "notMatches",
8506
+ value: "\\b[sr]k_test_"
8507
+ };
8508
+ postTo = (host2) => `\\.post\\(\\s*["'\`][^"'\`]{0,80}${host2}`;
8509
+ STRIPE_OBJECTS = "refunds|payouts|charges|transfers|payment_intents|payment_links|invoices|subscriptions|topups";
8510
+ APP_SHIELD_SPECS = [
8511
+ {
8512
+ name: "stripe",
8513
+ label: "Stripe",
8514
+ 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.",
8515
+ template: "finance",
8516
+ server: "*stripe*",
8517
+ doors: [
8518
+ {
8519
+ name: "shield:stripe:review-cli",
8520
+ tool: "bash",
8521
+ conditions: [
8522
+ {
8523
+ field: "command",
8524
+ op: "matches",
8525
+ value: prog("stripe") + GAP + `\\s(?:${STRIPE_OBJECTS})\\s+(?:create|cancel|capture|confirm|update|pay|finalize_invoice|send_invoice|void_invoice)(?![\\w-])`,
8526
+ flags: "i"
8527
+ },
8528
+ {
8529
+ field: "command",
8530
+ op: "matches",
8531
+ // The stripe CLI works in test mode unless told otherwise.
8532
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8533
+ }
8534
+ ],
8535
+ conditionMode: "all",
8536
+ verdict: "review",
8537
+ reason: "The stripe CLI is about to move money. node9 asks before money moves (Stripe)."
8538
+ },
8539
+ {
8540
+ name: "shield:stripe:review-cli-request",
8541
+ tool: "bash",
8542
+ conditions: [
8543
+ {
8544
+ field: "command",
8545
+ op: "matches",
8546
+ value: prog("stripe") + GAP + `\\s(?:post|delete)\\s+/?v1/(?:${STRIPE_OBJECTS})(?![\\w-])`,
8547
+ flags: "i"
8548
+ },
8549
+ {
8550
+ field: "command",
8551
+ op: "matches",
8552
+ // The stripe CLI works in test mode unless told otherwise.
8553
+ value: "\\s--live(?![\\w-])|\\b[sr]k_live_"
8554
+ }
8555
+ ],
8556
+ conditionMode: "all",
8557
+ verdict: "review",
8558
+ reason: "A raw stripe CLI request is about to move money. node9 asks before money moves (Stripe)."
8559
+ },
8560
+ {
8561
+ name: "shield:stripe:review-api",
8562
+ tool: "bash",
8563
+ conditions: [
8564
+ {
8565
+ field: "command",
8566
+ op: "matches",
8567
+ value: httpTo(`api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})(?![\\w-])`),
8568
+ flags: "i"
8569
+ },
8570
+ HTTP_WRITE,
8571
+ NOT_STRIPE_TEST
8572
+ ],
8573
+ conditionMode: "all",
8574
+ verdict: "review",
8575
+ reason: "A request to the Stripe API is about to move money. node9 asks before money moves (Stripe)."
8576
+ },
8577
+ {
8578
+ name: "shield:stripe:review-code",
8579
+ tool: "bash",
8580
+ conditions: [
8581
+ {
8582
+ field: "command",
8583
+ op: "matches",
8584
+ // Python: stripe.Refund.create(...)
8585
+ value: "\\bstripe\\.(?:Refund|Payout|Transfer|Charge|PaymentIntent|PaymentLink|Invoice|Subscription|Topup)\\.(?:create|cancel|capture|confirm|modify|pay|finalize_invoice|send_invoice|void_invoice)\\b"
8586
+ },
8587
+ NOT_STRIPE_TEST
8588
+ ],
8589
+ conditionMode: "all",
8590
+ verdict: "review",
8591
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8592
+ },
8593
+ {
8594
+ name: "shield:stripe:review-code-node",
8595
+ tool: "bash",
8596
+ conditions: [
8597
+ {
8598
+ field: "command",
8599
+ op: "matches",
8600
+ // Node: stripe.refunds.create(...)
8601
+ value: "\\bstripe\\.(?:refunds|payouts|transfers|charges|paymentIntents|paymentLinks|invoices|subscriptions|topups)\\.(?:create|cancel|capture|confirm|update|pay|finalizeInvoice|sendInvoice|voidInvoice)\\s*\\("
8602
+ },
8603
+ NOT_STRIPE_TEST
8604
+ ],
8605
+ conditionMode: "all",
8606
+ verdict: "review",
8607
+ reason: "This code moves money through the Stripe library. node9 asks before money moves (Stripe)."
8608
+ },
8609
+ {
8610
+ name: "shield:stripe:review-code-http",
8611
+ tool: "bash",
8612
+ conditions: [
8613
+ {
8614
+ field: "command",
8615
+ op: "matches",
8616
+ // requests.post("https://api.stripe.com/v1/refunds", ...)
8617
+ value: `\\.post\\(\\s*["'\`][^"'\`]{0,80}api\\.stripe\\.com/v1/(?:${STRIPE_OBJECTS})`
8618
+ },
8619
+ NOT_STRIPE_TEST
8620
+ ],
8621
+ conditionMode: "all",
8622
+ verdict: "review",
8623
+ reason: "This code posts to the Stripe API. node9 asks before money moves (Stripe)."
8624
+ }
8625
+ ]
8626
+ },
8627
+ {
8628
+ name: "paypal",
8629
+ label: "PayPal",
8630
+ 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.",
8631
+ template: "finance",
8632
+ server: "*paypal*",
8633
+ doors: [
8634
+ {
8635
+ name: "shield:paypal:review-api",
8636
+ tool: "bash",
8637
+ conditions: [
8638
+ {
8639
+ field: "command",
8640
+ op: "matches",
8641
+ // The live hosts only: api.paypal.com and api-m.paypal.com. The
8642
+ // sandbox (api-m.sandbox.paypal.com) moves no money.
8643
+ value: httpTo("api(?:-m)?\\.paypal\\.com/v[12]/"),
8644
+ flags: "i"
8645
+ },
8646
+ {
8647
+ field: "command",
8648
+ op: "matches",
8649
+ // The endpoints that move money.
8650
+ value: "\\.paypal\\.com/v[12]/(?:payments/(?:captures|authorizations|sale|payouts|payment|referenced-payouts)|checkout/orders/[^\\s/]{1,64}/(?:capture|authorize)|invoicing/invoices|billing/subscriptions)",
8651
+ flags: "i"
8652
+ },
8653
+ HTTP_WRITE
8654
+ ],
8655
+ conditionMode: "all",
8656
+ verdict: "review",
8657
+ reason: "A request to the PayPal API is about to move money. node9 asks before money moves (PayPal)."
8658
+ },
8659
+ {
8660
+ name: "shield:paypal:review-code",
8661
+ tool: "bash",
8662
+ conditions: [
8663
+ {
8664
+ field: "command",
8665
+ op: "matches",
8666
+ // The Node SDKs (checkout-server-sdk, paypal-server-sdk) and the
8667
+ // Python paypalrestsdk.
8668
+ value: "\\b(?:CapturesRefundRequest|OrdersCaptureRequest|AuthorizationsCaptureRequest|PayoutsPostRequest)\\b|\\b(?:captureOrder|refundCapturedPayment|captureAuthorizedPayment|createBatchPayout)\\s*\\(|\\bpaypalrestsdk\\.(?:Payout|Sale|Capture|Invoice|BillingAgreement)\\b"
8669
+ }
8670
+ ],
8671
+ verdict: "review",
8672
+ reason: "This code moves money through PayPal. node9 asks before money moves (PayPal)."
8673
+ },
8674
+ {
8675
+ name: "shield:paypal:review-code-http",
8676
+ tool: "bash",
8677
+ conditions: [
8678
+ {
8679
+ field: "command",
8680
+ op: "matches",
8681
+ // The endpoints that move money; the token call every script
8682
+ // starts with (/v1/oauth2/token) is not one.
8683
+ value: postTo(
8684
+ "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)"
8685
+ )
8686
+ }
8687
+ ],
8688
+ verdict: "review",
8689
+ reason: "This code posts to the PayPal API. node9 asks before money moves (PayPal)."
8690
+ }
8691
+ ]
8692
+ },
8693
+ {
8694
+ name: "square",
8695
+ label: "Square",
8696
+ 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.",
8697
+ template: "finance",
8698
+ // Square's servers, never Squarespace (a website builder).
8699
+ server: "{*square,*square[-_]*,*squareup*}",
8700
+ mcpTools: [
8701
+ {
8702
+ // Square's official MCP server has one tool for every API call; the
8703
+ // service and method name what it does.
8704
+ name: "shield:square:review-mcp-request",
8705
+ tool: "{,mcp__*__}make_api_request",
8706
+ conditions: [
8707
+ {
8708
+ field: "service",
8709
+ op: "matches",
8710
+ value: "^(?:refunds|payments|orders|invoices|subscriptions|disputes|gift_cards|gift_card_activities|checkout|terminal)$",
8711
+ flags: "i"
8712
+ },
8713
+ {
8714
+ field: "method",
8715
+ op: "notMatches",
8716
+ value: "^(?:list|get|retrieve|search|batch_?retrieve|calculate)",
8717
+ flags: "i"
8718
+ }
8719
+ ],
8720
+ conditionMode: "all",
8721
+ verdict: "review",
8722
+ reason: "A request through the Square MCP server is about to move money. node9 asks before money moves (Square)."
8723
+ }
8724
+ ],
8725
+ doors: [
8726
+ {
8727
+ name: "shield:square:review-api",
8728
+ tool: "bash",
8729
+ conditions: [
8730
+ {
8731
+ field: "command",
8732
+ op: "matches",
8733
+ // The live host only; the sandbox is connect.squareupsandbox.com.
8734
+ value: httpTo(
8735
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8736
+ ),
8737
+ flags: "i"
8738
+ },
8739
+ HTTP_WRITE_ANY
8740
+ ],
8741
+ conditionMode: "all",
8742
+ verdict: "review",
8743
+ reason: "A request to the Square API is about to move money. node9 asks before money moves (Square)."
8744
+ },
8745
+ {
8746
+ name: "shield:square:review-code",
8747
+ tool: "bash",
8748
+ conditions: [
8749
+ {
8750
+ field: "command",
8751
+ op: "matches",
8752
+ // The Square SDK in the script: Node (`square`) or Python (`square`, `squareup`).
8753
+ value: `\\bSquareClient\\b|\\bsquare\\.client\\b|from\\s+square[.\\s]|from\\s+["']square["']|require\\(\\s*["']square["']`
8754
+ },
8755
+ {
8756
+ field: "command",
8757
+ op: "matches",
8758
+ 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*\\("
8759
+ }
8760
+ ],
8761
+ conditionMode: "all",
8762
+ verdict: "review",
8763
+ reason: "This code moves money through the Square SDK. node9 asks before money moves (Square)."
8764
+ },
8765
+ {
8766
+ name: "shield:square:review-code-http",
8767
+ tool: "bash",
8768
+ conditions: [
8769
+ {
8770
+ field: "command",
8771
+ op: "matches",
8772
+ value: postTo(
8773
+ "connect\\.squareup\\.com/v2/(?:refunds|payments|orders/[^\\s/?\"'`]{1,64}/pay|invoices|subscriptions|disputes|gift-cards/activities)(?![\\w-]|/search)"
8774
+ )
8775
+ }
8776
+ ],
8777
+ verdict: "review",
8778
+ reason: "This code posts to the Square API. node9 asks before money moves (Square)."
8779
+ }
8780
+ ]
8781
+ },
8782
+ {
8783
+ name: "plaid",
8784
+ label: "Plaid",
8785
+ 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.",
8786
+ template: "finance",
8787
+ server: "*plaid*",
8788
+ doors: [
8789
+ {
8790
+ name: "shield:plaid:review-api",
8791
+ tool: "bash",
8792
+ conditions: [
8793
+ {
8794
+ field: "command",
8795
+ op: "matches",
8796
+ // Every Plaid endpoint is a POST, so the path is what tells a
8797
+ // write; the live host only, never sandbox.plaid.com. A script
8798
+ // that posts to the URL meets this door too.
8799
+ value: httpTo(
8800
+ "production\\.plaid\\.com/(?:transfer/(?:create|authorization/create|recurring/create)|payment_initiation/payment/create|bank_transfer/create)(?![\\w-])"
8801
+ ),
8802
+ flags: "i"
8803
+ }
8804
+ ],
8805
+ verdict: "review",
8806
+ reason: "A request to the Plaid API is about to move money. node9 asks before money moves (Plaid)."
8807
+ },
8808
+ {
8809
+ name: "shield:plaid:review-code",
8810
+ tool: "bash",
8811
+ conditions: [
8812
+ { field: "command", op: "matches", value: "\\bplaid", flags: "i" },
8813
+ {
8814
+ field: "command",
8815
+ op: "matches",
8816
+ value: "\\b(?:transfer(?:Authorization|Recurring)?Create|paymentInitiationPaymentCreate|bankTransferCreate|transfer_(?:authorization_|recurring_)?create|payment_initiation_payment_create|bank_transfer_create)\\s*\\("
8817
+ }
8818
+ ],
8819
+ conditionMode: "all",
8820
+ verdict: "review",
8821
+ reason: "This code moves money through Plaid. node9 asks before money moves (Plaid)."
8822
+ }
8823
+ ]
8824
+ },
8825
+ {
8826
+ name: "okta",
8827
+ label: "Okta",
8828
+ 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.",
8829
+ template: "identity",
8830
+ server: "*okta*",
8831
+ doors: [
8832
+ {
8833
+ name: "shield:okta:review-api",
8834
+ tool: "bash",
8835
+ conditions: [
8836
+ {
8837
+ field: "command",
8838
+ op: "matches",
8839
+ // The management API only: sign-in (/api/v1/authn, /sessions,
8840
+ // /oauth2) is not under these paths.
8841
+ value: httpTo(
8842
+ "[\\w-]+\\.okta(?:-emea)?\\.com/api/v1/(?:users|groups|apps|iam|api-tokens|idps|authorizationServers|policies)(?![\\w-])"
8843
+ ),
8844
+ flags: "i"
8845
+ },
8846
+ HTTP_WRITE_ANY
8847
+ ],
8848
+ conditionMode: "all",
8849
+ verdict: "review",
8850
+ reason: "A request to the Okta API is about to change who has access. node9 asks before access changes (Okta)."
8851
+ },
8852
+ {
8853
+ name: "shield:okta:review-code",
8854
+ tool: "bash",
8855
+ conditions: [
8856
+ {
8857
+ field: "command",
8858
+ op: "matches",
8859
+ // The Okta SDK in the script: a method named createUser alone is
8860
+ // in half the applications ever written.
8861
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8862
+ },
8863
+ {
8864
+ field: "command",
8865
+ op: "matches",
8866
+ value: "\\b(?:(?:create|delete|deactivate|suspend|unlock|activate)User|(?:reset|expire)Password|resetFactors|(?:assign|unassign)User(?:To|From)Group|assignRoleTo(?:User|Group))\\s*\\("
8867
+ }
8868
+ ],
8869
+ conditionMode: "all",
8870
+ verdict: "review",
8871
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8872
+ },
8873
+ {
8874
+ name: "shield:okta:review-code-py",
8875
+ tool: "bash",
8876
+ conditions: [
8877
+ {
8878
+ field: "command",
8879
+ op: "matches",
8880
+ value: "@okta/okta-sdk|\\bokta\\.Client\\b|from\\s+okta[.\\s]|\\bimport\\s+okta\\b"
8881
+ },
8882
+ {
8883
+ field: "command",
8884
+ op: "matches",
8885
+ 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*\\("
8886
+ }
8887
+ ],
8888
+ conditionMode: "all",
8889
+ verdict: "review",
8890
+ reason: "This code changes who has access in Okta. node9 asks before access changes (Okta)."
8891
+ }
8892
+ ]
8893
+ },
8894
+ {
8895
+ name: "auth0",
8896
+ label: "Auth0",
8897
+ 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.",
8898
+ template: "identity",
8899
+ server: "*auth0*",
8900
+ mcpTools: [
8901
+ {
8902
+ // Auth0's official MCP server: applications (their secrets and
8903
+ // callbacks), APIs and their scopes, and Actions (code that runs at
8904
+ // every sign-in).
8905
+ name: "shield:auth0:review-tenant-config",
8906
+ 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}",
8907
+ conditions: [],
8908
+ verdict: "review",
8909
+ reason: "This changes an Auth0 application, API or sign-in Action. node9 asks before access changes (Auth0)."
8910
+ }
8911
+ ],
8912
+ doors: [
8913
+ {
8914
+ name: "shield:auth0:review-cli",
8915
+ tool: "bash",
8916
+ conditions: [
8917
+ {
8918
+ field: "command",
8919
+ op: "matches",
8920
+ 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-])",
8921
+ flags: "i"
8922
+ }
8923
+ ],
8924
+ verdict: "review",
8925
+ reason: "The auth0 CLI is about to change who has access. node9 asks before access changes (Auth0)."
8926
+ },
8927
+ {
8928
+ name: "shield:auth0:review-api",
8929
+ tool: "bash",
8930
+ conditions: [
8931
+ {
8932
+ field: "command",
8933
+ op: "matches",
8934
+ // The Management API only; /oauth/token is signing in.
8935
+ value: httpTo(
8936
+ "\\.auth0\\.com/api/v2/(?:users|roles|clients|grants|client-grants|organizations|tickets|jobs/users-imports|device-credentials)(?![\\w-])"
8937
+ ),
8938
+ flags: "i"
8939
+ },
8940
+ HTTP_WRITE_ANY
8941
+ ],
8942
+ conditionMode: "all",
8943
+ verdict: "review",
8944
+ reason: "A request to the Auth0 Management API is about to change who has access. node9 asks before access changes (Auth0)."
8945
+ },
8946
+ {
8947
+ name: "shield:auth0:review-code",
8948
+ tool: "bash",
8949
+ conditions: [
8950
+ {
8951
+ field: "command",
8952
+ op: "matches",
8953
+ // The Auth0 SDK in the script (Node `auth0`, Python `auth0`).
8954
+ value: `\\bManagementClient\\b|from\\s+auth0[.\\s]|from\\s+["']auth0["']|require\\(\\s*["']auth0["']`
8955
+ },
8956
+ {
8957
+ field: "command",
8958
+ op: "matches",
8959
+ 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*\\("
8960
+ }
8961
+ ],
8962
+ conditionMode: "all",
8963
+ verdict: "review",
8964
+ reason: "This code changes who has access in Auth0. node9 asks before access changes (Auth0)."
8965
+ }
8966
+ ]
8967
+ }
8968
+ ];
8969
+ APP_SHIELDS = APP_SHIELD_SPECS.map(buildAppShield);
8970
+ APP_SHIELD_NAMES = new Set(APP_SHIELD_SPECS.map((s) => s.name));
8971
+ APP_SERVER_GLOB = `{${APP_SHIELD_SPECS.map((s) => s.server).join(",")}}`;
8972
+ GENERAL_ACTION_RULES = buildGeneralRules();
8115
8973
  BUILTIN_SHIELDS = {
8116
8974
  [aws_default.name]: aws_default,
8117
8975
  [bash_safe_default.name]: bash_safe_default,
8118
8976
  [docker_default.name]: docker_default,
8119
8977
  [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
8978
  [github_default.name]: github_default,
8126
- [identity_default.name]: identity_default,
8127
8979
  [k8s_default.name]: k8s_default,
8128
8980
  [mongodb_default.name]: mongodb_default,
8129
8981
  [postgres_default.name]: postgres_default,
8130
8982
  [project_jail_default.name]: project_jail_default,
8131
- [publishing_default.name]: publishing_default,
8132
- [redis_default.name]: redis_default
8983
+ [redis_default.name]: redis_default,
8984
+ // App shields generated from the action templates (node9Firewall#390): one
8985
+ // per app, every door (MCP server, CLI, API, library), off by default.
8986
+ ...Object.fromEntries(APP_SHIELDS.map((s) => [s.name, s]))
8133
8987
  };
8134
8988
  assertBuiltinShieldRegexesAreSafe();
8135
8989
  SSRF_MAX_HOST = 253;
@@ -8185,9 +9039,9 @@ var init_dist = __esm({
8185
9039
  },
8186
9040
  // ── Commands ──────────────────────────────────────────────────────────────
8187
9041
  "commands.inline-exec": {
8188
- plain: "The agent runs a small program written inside the command itself.",
9042
+ 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
9043
  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."
9044
+ 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
9045
  },
8192
9046
  "commands.eval-dynamic": {
8193
9047
  plain: "The agent runs code whose content is built at run time, so node9 cannot read it in advance.",
@@ -8219,6 +9073,21 @@ var init_dist = __esm({
8219
9073
  example: "chmod 777 deploy.sh",
8220
9074
  advice: "Keep Review. A narrower permission (755, 644) almost always does the job."
8221
9075
  },
9076
+ "actions.money": {
9077
+ 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.",
9078
+ example: "refund_order on a server called acme-billing",
9079
+ 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."
9080
+ },
9081
+ "actions.access": {
9082
+ plain: "The agent changes a password, a key, a role or who belongs to a team, through an app node9 has no protection for.",
9083
+ example: "invite_user or grant_role on a company admin server",
9084
+ advice: "Keep Review. Set Log on a sandbox workspace where agents manage test users all day."
9085
+ },
9086
+ "actions.publish": {
9087
+ 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.",
9088
+ example: "create_release or deploy_to_production on a company MCP server",
9089
+ advice: "Keep Review. Set Log on a release machine whose pipeline publishes on purpose, never on a developer laptop."
9090
+ },
8222
9091
  "commands.sudo": {
8223
9092
  plain: "The agent runs a command as the administrator (root).",
8224
9093
  example: "sudo apt-get install -y nginx",
@@ -8379,11 +9248,57 @@ var init_dist = __esm({
8379
9248
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
8380
9249
  },
8381
9250
  "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.",
9251
+ 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
9252
  example: "npm install of a package flagged as malware",
8384
9253
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
8385
9254
  }
8386
9255
  };
9256
+ LEVELS = ["audit", "soft", "standard", "strict", "block-all"];
9257
+ MODE_VALUES = ["observe", ...LEVELS];
9258
+ SOFT = {
9259
+ "commands.inline-exec": "log",
9260
+ "commands.eval-dynamic": "log",
9261
+ "commands.rm": "log",
9262
+ "commands.chmod": "log",
9263
+ "commands.sudo": "log",
9264
+ "commands.git-destructive": "log",
9265
+ "commands.sql-ddl": "log",
9266
+ "commands.sql-no-where": "log",
9267
+ "commands.temp-binary": "log",
9268
+ "commands.disk-destroy": "log",
9269
+ "commands.dangerous-word": "log",
9270
+ "files.system-dirs": "log",
9271
+ "files.shell-startup": "log",
9272
+ "files.password-hashes": "log",
9273
+ "data.pipe-chain": "log",
9274
+ "behavior.untrusted-content": "log"
9275
+ };
9276
+ STRICT = {
9277
+ "commands.sql-ddl": "block",
9278
+ "commands.disk-destroy": "block",
9279
+ "commands.node9-disarm": "block",
9280
+ "files.system-dirs": "block",
9281
+ "files.shell-startup": "block",
9282
+ "files.password-hashes": "block",
9283
+ "data.secrets-weak": "block",
9284
+ "data.pipe-chain": "block",
9285
+ "network.unknown-host": "review",
9286
+ "network.internal-addresses": "block",
9287
+ "behavior.prompt-injection": "log",
9288
+ "loading.skill-tamper": "block"
9289
+ };
9290
+ BLOCK_ALL = {
9291
+ ...STRICT,
9292
+ "commands.unknown": "review",
9293
+ "commands.temp-binary": "block"
9294
+ };
9295
+ LEVEL_CHECKS = {
9296
+ audit: {},
9297
+ soft: SOFT,
9298
+ standard: {},
9299
+ strict: STRICT,
9300
+ "block-all": BLOCK_ALL
9301
+ };
8387
9302
  VERDICTS = ["off", "log", "review", "block"];
8388
9303
  CHECK_VERDICT_RANK = { off: 0, log: 1, review: 2, block: 3 };
8389
9304
  ALL = VERDICTS;
@@ -8431,7 +9346,7 @@ var init_dist = __esm({
8431
9346
  id: "commands.inline-exec",
8432
9347
  group: "commands",
8433
9348
  title: "Inline code execution",
8434
- catches: "node -e, python -c, bash -c, a heredoc into an interpreter",
9349
+ 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
9350
  values: ALL,
8436
9351
  defaultValue: "review",
8437
9352
  legacy: ["Node9 Standard (Inline Execution)"]
@@ -8788,6 +9703,38 @@ var init_dist = __esm({
8788
9703
  values: ALL,
8789
9704
  defaultValue: "block",
8790
9705
  legacy: ["\u{1F4E6} Node9 Package Check (Malicious)", "\u{1F4E6} Node9 Package Check (Review)"]
9706
+ },
9707
+ // ── Actions in apps (node9Firewall#390) ──────────────────────────────────
9708
+ // The safety net under the app shields: an action that moves money, changes
9709
+ // access or publishes, on an MCP server no app shield claims (an internal
9710
+ // billing server, a vendor with no shield, Zapier). Review by default; the
9711
+ // rules come from the action templates (shields/apps.ts).
9712
+ {
9713
+ id: "actions.money",
9714
+ group: "actions",
9715
+ title: "Money moves",
9716
+ catches: "a refund, payout, transfer, charge or withdrawal through an app with no protection of its own",
9717
+ values: ALL,
9718
+ defaultValue: "review",
9719
+ legacy: generalActionRuleNames("finance")
9720
+ },
9721
+ {
9722
+ id: "actions.access",
9723
+ group: "actions",
9724
+ title: "Accounts and access change",
9725
+ catches: "a password, key, role, permission or user changed through an app with no protection of its own",
9726
+ values: ALL,
9727
+ defaultValue: "review",
9728
+ legacy: generalActionRuleNames("identity")
9729
+ },
9730
+ {
9731
+ id: "actions.publish",
9732
+ group: "actions",
9733
+ title: "Something is published",
9734
+ catches: "a release, package, deploy or public post through an app with no protection of its own",
9735
+ values: ALL,
9736
+ defaultValue: "review",
9737
+ legacy: generalActionRuleNames("publishing")
8791
9738
  }
8792
9739
  ];
8793
9740
  SHIELD_RULE_NAME = /^shield:([^:]+):(block|review|allow)-(.+)$/;
@@ -8808,7 +9755,7 @@ var init_dist = __esm({
8808
9755
  "commands.rm": (s) => knob(s.commandChecks?.rmAdvisory),
8809
9756
  "commands.chmod": (s) => knob(s.commandChecks?.chmod),
8810
9757
  "commands.sql-ddl": (s) => knob(s.commandChecks?.sqlDdl),
8811
- "commands.unknown": (s) => s.mode === void 0 ? void 0 : s.mode === "strict" ? "review" : "off",
9758
+ "commands.unknown": (s) => s.mode === void 0 ? void 0 : isBlockAll(s.mode) ? "review" : "off",
8812
9759
  "data.secrets": (s) => enabledOr(s.dlp?.enabled, "block"),
8813
9760
  "data.secrets-weak": (s) => {
8814
9761
  if (s.dlp?.enabled === false) return "off";
@@ -8916,6 +9863,217 @@ var init_dist = __esm({
8916
9863
  BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
8917
9864
  HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
8918
9865
  HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
9866
+ SYSTEM_AUTH_FILES = [
9867
+ "/etc/passwd",
9868
+ "/etc/shadow",
9869
+ "/etc/group",
9870
+ "/etc/gshadow",
9871
+ "/etc/sudoers"
9872
+ ];
9873
+ SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9874
+ PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9875
+ STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9876
+ SYSTEM_DIRS = [
9877
+ "/usr",
9878
+ "/etc",
9879
+ "/boot",
9880
+ "/bin",
9881
+ "/sbin",
9882
+ "/lib",
9883
+ "/lib64",
9884
+ "/var/log"
9885
+ ];
9886
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9887
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9888
+ AGENT_INSTRUCTION_FILES = [
9889
+ ".gitlab-ci.yml",
9890
+ "CLAUDE.md",
9891
+ "AGENTS.md",
9892
+ ".cursorrules",
9893
+ ".mcp.json",
9894
+ ".vscode/settings.json",
9895
+ "package.json"
9896
+ ];
9897
+ AGENT_INSTRUCTION_DIRS = [
9898
+ ".github/workflows",
9899
+ ".claude",
9900
+ ".cursor",
9901
+ ".git/hooks",
9902
+ ".husky"
9903
+ ];
9904
+ LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9905
+ MAX_INLINE_CODE = 64 * 1024;
9906
+ WHAT = {
9907
+ deletes: "deletes files",
9908
+ "writes-outside": "writes a file outside the project",
9909
+ sends: "sends data over the network",
9910
+ listens: "opens a listening socket or a shell",
9911
+ "runs-commands": "runs other commands or processes",
9912
+ "reads-protected": "reads a protected file",
9913
+ "builds-code": "runs code it builds at run time",
9914
+ permissions: "changes permissions or ownership",
9915
+ monitors: "captures the screen, the clipboard or the keyboard",
9916
+ amplifies: "writes or sends in an endless loop"
9917
+ };
9918
+ OUTSIDE = String.raw`(?:~|\.\.[\\/]|/(?!(?:tmp|var/tmp)/(?![^'"\n]*\.\.)|dev/(?:null|stdout|stderr)\b))`;
9919
+ pyImport = (mods) => new RegExp(String.raw`\b(?:import\s+(?:[\w.]+(?:\s+as\s+\w+)?\s*,\s*)*|from\s+)(?:${mods})\b`);
9920
+ either = (...res) => new RegExp(res.map((r) => `(?:${r.source})`).join("|"), "m");
9921
+ PYTHON = [
9922
+ [
9923
+ "deletes",
9924
+ /\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/
9925
+ ],
9926
+ [
9927
+ "writes-outside",
9928
+ new RegExp(
9929
+ 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}`
9930
+ )
9931
+ ],
9932
+ [
9933
+ "sends",
9934
+ either(
9935
+ pyImport(
9936
+ String.raw`socket|requests|urllib2|urllib3|http\.client|httplib|smtplib|ftplib|paramiko|telnetlib|aiohttp|httpx|websockets?|xmlrpc|poplib|imaplib|pycurl|scapy`
9937
+ ),
9938
+ /\burllib\.request\b|\burlopen\s*\(|\bfrom\s+urllib\s+import\s+[^\n;]*\brequest\b|\basyncio\.open_connection\s*\(/
9939
+ )
9940
+ ],
9941
+ [
9942
+ "listens",
9943
+ either(
9944
+ pyImport(String.raw`pty|socketserver|http\.server`),
9945
+ /\bpty\.spawn\s*\(|\.bind\s*\(\s*\(|\basyncio\.start_server\s*\(/
9946
+ )
9947
+ ],
9948
+ [
9949
+ "runs-commands",
9950
+ either(
9951
+ pyImport("subprocess|pexpect|commands|psutil"),
9952
+ /\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+\*/
9953
+ )
9954
+ ],
9955
+ [
9956
+ "builds-code",
9957
+ /(?<![\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*\[/
9958
+ ],
9959
+ [
9960
+ // Spyware shapes: a screenshot, the clipboard, a key logger.
9961
+ "monitors",
9962
+ either(
9963
+ pyImport(String.raw`pyperclip|pynput|keyboard|pyautogui|pyscreenshot|mss|clipboard`),
9964
+ /\bImageGrab\b|\bpyautogui\.screenshot\b|\bscreencapture\b/
9965
+ )
9966
+ ],
9967
+ [
9968
+ "permissions",
9969
+ /\bos\.(?:chmod|lchmod|fchmod|chown|lchown|fchown|setuid|setgid|seteuid|setegid|setreuid|setregid|setresuid)\s*\(|\.chmod\s*\(|\bshutil\.chown\s*\(/
9970
+ ]
9971
+ ];
9972
+ NODE = [
9973
+ [
9974
+ "deletes",
9975
+ /\.(?:rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync)\s*\(|\brimraf\b|\bDeno\.remove(?:Sync)?\s*\(/
9976
+ ],
9977
+ [
9978
+ "writes-outside",
9979
+ new RegExp(
9980
+ 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}`
9981
+ )
9982
+ ],
9983
+ [
9984
+ "sends",
9985
+ /\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*\(/
9986
+ ],
9987
+ ["listens", /\bcreateServer\s*\(|\b(?:Deno\.(?:listen\w*|serve)|Bun\.(?:listen|serve))\s*\(/],
9988
+ [
9989
+ "runs-commands",
9990
+ /\bchild_process\b|\bprocess\.(?:binding|dlopen|kill)\s*\(|\bDeno\.(?:run|Command)\b|\bBun\.(?:spawn|spawnSync|\$)\b|\bexecSync\s*\(|\bspawnSync\s*\(/
9991
+ ],
9992
+ [
9993
+ "builds-code",
9994
+ /(?<![\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)]/
9995
+ ],
9996
+ [
9997
+ "permissions",
9998
+ /\.(?:chmod|chmodSync|chown|chownSync|lchown|lchownSync|fchmod|fchmodSync|fchown|fchownSync)\s*\(|\bprocess\.set(?:e)?[ug]id\s*\(|\bDeno\.(?:chmod|chown)(?:Sync)?\s*\(/
9999
+ ]
10000
+ ];
10001
+ PERL = [
10002
+ ["deletes", /\b(?:unlink|rmdir|rmtree|remove_tree)\b/],
10003
+ [
10004
+ "writes-outside",
10005
+ new RegExp(
10006
+ String.raw`['"]\s*(?:\+?>>?|\+<)\s*['"]?\s*,?\s*['"]?${OUTSIDE}|\b(?:rename|symlink|link|copy|move)\s*\(?[^;\n]*['"]${OUTSIDE}`
10007
+ )
10008
+ ],
10009
+ [
10010
+ "sends",
10011
+ /\b(?:IO::Socket|Socket|LWP|HTTP::Tiny|Net::\w+|Mojo::UserAgent|WWW::Mechanize|IO::Socket::INET)\b/
10012
+ ],
10013
+ [
10014
+ "runs-commands",
10015
+ /\b(?:system|exec|fork|kill|syscall)\b|`[^`]*`|\bqx\s*[{([/|!'"]|\bopen\s*\(?[^;\n]*(?:['"]\s*\||\|\s*['"]|['"]-\|['"])/
10016
+ ],
10017
+ ["builds-code", /\beval\b|\bStorable::thaw\b|\bdo\s+['"]/],
10018
+ ["permissions", /\b(?:chmod|chown)\b/]
10019
+ ];
10020
+ RUBY = [
10021
+ [
10022
+ "deletes",
10023
+ /\bFile\.(?:delete|unlink)\b|\bFileUtils\.(?:rm\w*|remove\w*)\b|\bDir\.(?:rmdir|delete|unlink)\b|\.(?:rmtree|delete_all)\b/
10024
+ ],
10025
+ [
10026
+ "writes-outside",
10027
+ new RegExp(
10028
+ 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}`
10029
+ )
10030
+ ],
10031
+ [
10032
+ "sends",
10033
+ /\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)['"]/
10034
+ ],
10035
+ ["listens", /\b(?:TCPServer|UNIXServer|WEBrick|PTY)\b/],
10036
+ [
10037
+ "runs-commands",
10038
+ /(?<![\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/
10039
+ ],
10040
+ [
10041
+ "builds-code",
10042
+ /(?<![\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/
10043
+ ],
10044
+ ["permissions", /\b(?:File|FileUtils)\.(?:chmod|chown|lchmod|lchown)\w*\b/]
10045
+ ];
10046
+ PHP = [
10047
+ ["deletes", /\b(?:unlink|rmdir)\s*\(/],
10048
+ [
10049
+ "writes-outside",
10050
+ new RegExp(
10051
+ String.raw`\b(?:file_put_contents|symlink|link|copy|rename|mkdir|touch)\s*\([^)\n]*['"]${OUTSIDE}|\bfopen\s*\(\s*['"]${OUTSIDE}[^'"\n]*['"]\s*,\s*['"][waxc+]`
10052
+ )
10053
+ ],
10054
+ [
10055
+ "sends",
10056
+ /\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):/
10057
+ ],
10058
+ ["listens", /\b(?:stream_socket_server|socket_bind|socket_listen)\s*\(/],
10059
+ [
10060
+ "runs-commands",
10061
+ /\b(?:system|exec|shell_exec|passthru|proc_open|popen|pcntl_exec|pcntl_fork|posix_kill)\s*\(|`[^`]*`/
10062
+ ],
10063
+ [
10064
+ "builds-code",
10065
+ /\b(?:eval|assert|create_function|unserialize|call_user_func(?:_array)?)\s*\(|\bbase64_decode\b/
10066
+ ],
10067
+ ["permissions", /\b(?:chmod|chown|chgrp|lchown|lchgrp)\s*\(/]
10068
+ ];
10069
+ TABLES = {
10070
+ python: PYTHON,
10071
+ node: NODE,
10072
+ perl: PERL,
10073
+ ruby: RUBY,
10074
+ php: PHP
10075
+ };
10076
+ STRING_LITERAL = /(['"`])((?:\\.|(?!\1)[^\\\n]){1,4096})\1/g;
8919
10077
  ({ syntax } = import_mvdan_sh.default);
8920
10078
  sharedParser = syntax.NewParser();
8921
10079
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -8953,6 +10111,10 @@ var init_dist = __esm({
8953
10111
  AST_CACHE_MAX = 5e3;
8954
10112
  astCache = /* @__PURE__ */ new Map();
8955
10113
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
10114
+ INTERPRETER_LINE = /^#![ \t]*[\w./+-]+(?:[ \t]+[\w./+=:-]+)*[ \t]*\r?(?=\n|$)/;
10115
+ PROGRAM_DIR = /^(?:\/usr(?:\/local)?|\/opt\/homebrew|\/home\/linuxbrew\/\.linuxbrew)?\/s?bin\/[^/]+$|^\/snap\/bin\/[^/]+$|^(?![/~\\]|[A-Za-z]:|.*\.\.)/;
10116
+ ACCESS_VIEW_CACHE_MAX = 1e3;
10117
+ accessViewCache = /* @__PURE__ */ new Map();
8956
10118
  MAX_READING_PAYLOADS = 8;
8957
10119
  defaultExpansionOps = null;
8958
10120
  VARIABLE_READERS = /* @__PURE__ */ new Set([
@@ -9662,6 +10824,14 @@ var init_dist = __esm({
9662
10824
  INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
9663
10825
  MAX_PAYLOAD_NESTING = 8;
9664
10826
  payloadNesting = 0;
10827
+ MAX_INLINE_CALLS = 16;
10828
+ 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;
10829
+ UNREADABLE_SOURCE = {
10830
+ pipe: "reads its code from another program through a pipe",
10831
+ file: "reads its code from a file on stdin",
10832
+ variable: "runs code the shell fills in at run time",
10833
+ payload: "runs a shell payload that evaluates, sources or runs further code"
10834
+ };
9665
10835
  SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
9666
10836
  SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
9667
10837
  MAX_SCRIPT_OPERANDS = 8;
@@ -9861,9 +11031,6 @@ var init_dist = __esm({
9861
11031
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
9862
11032
  NONE = { kind: "none" };
9863
11033
  UNKNOWN = { kind: "unknown" };
9864
- MAX_REGEX_LENGTH = 256;
9865
- REGEX_CACHE_MAX = 500;
9866
- regexCache = /* @__PURE__ */ new Map();
9867
11034
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
9868
11035
  BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
9869
11036
  "bash",
@@ -9978,45 +11145,6 @@ var init_dist = __esm({
9978
11145
  (s) => parseShared(`echo ${s}`).Stmts[0].Cmd.Args[1].Parts[0].Exp.Op
9979
11146
  )
9980
11147
  );
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
11148
  exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
10021
11149
  RANK = { allow: 0, review: 1, block: 2 };
10022
11150
  BIP39_ENGLISH = `abandon
@@ -13509,7 +14637,11 @@ zoo`.split("\n");
13509
14637
  "commands.disk-destroy",
13510
14638
  "commands.dangerous-word",
13511
14639
  "commands.node9-disarm",
13512
- "data.pipe-chain"
14640
+ "data.pipe-chain",
14641
+ // The general action checks (node9Firewall#390): smart rules the map governs.
14642
+ "actions.money",
14643
+ "actions.access",
14644
+ "actions.publish"
13513
14645
  ]);
13514
14646
  KNOB_GOVERNED = {
13515
14647
  "data.secrets": ["off", "block"],
@@ -14224,7 +15356,8 @@ var SmartConditionSchema = import_zod.z.object({
14224
15356
  }
14225
15357
  ),
14226
15358
  value: import_zod.z.string().optional(),
14227
- flags: import_zod.z.string().optional()
15359
+ flags: import_zod.z.string().optional(),
15360
+ reading: import_zod.z.enum(["access"]).optional()
14228
15361
  }).refine(
14229
15362
  (c) => {
14230
15363
  if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
@@ -14235,6 +15368,9 @@ var SmartConditionSchema = import_zod.z.object({
14235
15368
  var SmartRuleSchema = import_zod.z.object({
14236
15369
  name: import_zod.z.string().optional(),
14237
15370
  tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
15371
+ // Scope to an MCP server label (glob), or keep off one (node9Firewall#390).
15372
+ server: import_zod.z.string().min(1).optional(),
15373
+ notServer: import_zod.z.string().min(1).optional(),
14238
15374
  conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
14239
15375
  conditionMode: import_zod.z.enum(["all", "any"]).optional(),
14240
15376
  verdict: import_zod.z.enum(["allow", "review", "block"], {
@@ -14255,7 +15391,8 @@ var SmartRuleSchema = import_zod.z.object({
14255
15391
  var ConfigFileSchema = import_zod.z.object({
14256
15392
  version: import_zod.z.string().optional(),
14257
15393
  settings: import_zod.z.object({
14258
- mode: import_zod.z.enum(["standard", "strict", "audit", "observe"]).optional(),
15394
+ // The protection level (levels.ts in the engine); `observe` is Audit's older name.
15395
+ mode: import_zod.z.enum(["observe", "audit", "soft", "standard", "strict", "block-all"]).optional(),
14259
15396
  autoStartDaemon: import_zod.z.boolean().optional(),
14260
15397
  // enableUndo / policy.snapshot: removed with the undo feature. The
14261
15398
  // schema strips undeclared keys silently (plain zod object — no
@@ -14384,8 +15521,8 @@ var ConfigFileSchema = import_zod.z.object({
14384
15521
  }).strict();
14385
15522
  function formatIssues(issues) {
14386
15523
  const lines = issues.map((issue) => {
14387
- const path27 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
14388
- return ` \u2022 ${path27}: ${issue.message}`;
15524
+ const path28 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
15525
+ return ` \u2022 ${path28}: ${issue.message}`;
14389
15526
  });
14390
15527
  return `Invalid config:
14391
15528
  ${lines.join("\n")}`;
@@ -14398,9 +15535,9 @@ function prunePaths(root, paths) {
14398
15535
  const yi = y[y.length - 1];
14399
15536
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
14400
15537
  });
14401
- for (const path27 of ordered) {
15538
+ for (const path28 of ordered) {
14402
15539
  let cur = root;
14403
- for (const key of path27.slice(0, -1)) {
15540
+ for (const key of path28.slice(0, -1)) {
14404
15541
  if (cur === null || typeof cur !== "object") {
14405
15542
  cur = void 0;
14406
15543
  break;
@@ -14408,7 +15545,7 @@ function prunePaths(root, paths) {
14408
15545
  cur = cur[key];
14409
15546
  }
14410
15547
  if (cur === null || typeof cur !== "object") continue;
14411
- const last = path27[path27.length - 1];
15548
+ const last = path28[path28.length - 1];
14412
15549
  if (Array.isArray(cur)) {
14413
15550
  const i = Number(last);
14414
15551
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -14444,7 +15581,7 @@ function sanitizeConfig(raw) {
14444
15581
  return issue.keys.map((k) => [...at, k]);
14445
15582
  }
14446
15583
  return [at.slice(0, -level || void 0)];
14447
- }).filter((path27) => path27.length > 0);
15584
+ }).filter((path28) => path28.length > 0);
14448
15585
  if (paths.length === 0 || !prunePaths(working, paths)) break;
14449
15586
  }
14450
15587
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -14470,6 +15607,85 @@ var import_fs2 = __toESM(require("fs"));
14470
15607
  var import_path2 = __toESM(require("path"));
14471
15608
  var import_os2 = __toESM(require("os"));
14472
15609
  init_dist();
15610
+
15611
+ // src/shields/build.ts
15612
+ init_dist();
15613
+ function escapeRegex(s) {
15614
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15615
+ }
15616
+ function slug(s) {
15617
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
15618
+ }
15619
+ var B = "[\\s/\\\\]";
15620
+ var SEP = "[/\\\\]";
15621
+ function pathToRegexFragment(rawPath) {
15622
+ const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
15623
+ const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
15624
+ if (segments2.length === 0) return "";
15625
+ return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
15626
+ }
15627
+ function pathMatchesFragment(candidate, rawPath) {
15628
+ const value = pathToRegexFragment(rawPath);
15629
+ if (!value || !candidate) return false;
15630
+ const re = getCompiledRegex(value);
15631
+ if (!re) return false;
15632
+ return re.test(candidate);
15633
+ }
15634
+ function pathRules(rawPath, verdict, reason) {
15635
+ const value = pathToRegexFragment(rawPath);
15636
+ if (!value) return [];
15637
+ const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
15638
+ const s = slug(rawPath);
15639
+ return [
15640
+ // `reading: 'access'`: a script's interpreter line and the program a
15641
+ // command runs are not reads of the jailed path (node9Firewall#385).
15642
+ {
15643
+ name: `${verdict}-path-${s}-bash`,
15644
+ tool: "bash",
15645
+ conditions: [{ field: "command", op: "matches", value, reading: "access" }],
15646
+ verdict,
15647
+ reason: why
15648
+ },
15649
+ // Keep the historical `-anytool` name for the file_path rule: the
15650
+ // rule→shield attribution maps (Report SHIELDS panel) key on rule names.
15651
+ {
15652
+ name: `${verdict}-path-${s}-anytool`,
15653
+ tool: "*",
15654
+ conditions: [{ field: "file_path", op: "matches", value }],
15655
+ verdict,
15656
+ reason: why
15657
+ },
15658
+ {
15659
+ name: `${verdict}-path-${s}-anytool-path`,
15660
+ tool: "*",
15661
+ conditions: [{ field: "path", op: "matches", value }],
15662
+ verdict,
15663
+ reason: why
15664
+ },
15665
+ {
15666
+ name: `${verdict}-path-${s}-anytool-pattern`,
15667
+ tool: "*",
15668
+ conditions: [{ field: "pattern", op: "matches", value }],
15669
+ verdict,
15670
+ reason: why
15671
+ }
15672
+ ];
15673
+ }
15674
+ var LEGACY_BASH_PATH_RULE = /^(?:block|review)-path-.+-bash$/;
15675
+ function withAccessReading(def) {
15676
+ let changed = false;
15677
+ const smartRules = def.smartRules.map((r) => {
15678
+ const [c, ...rest] = r.conditions ?? [];
15679
+ 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) {
15680
+ return r;
15681
+ }
15682
+ changed = true;
15683
+ return { ...r, conditions: [{ ...c, reading: "access" }] };
15684
+ });
15685
+ return changed ? { ...def, smartRules } : def;
15686
+ }
15687
+
15688
+ // src/shields.ts
14473
15689
  init_dist();
14474
15690
  var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
14475
15691
  function loadUserShields() {
@@ -14496,7 +15712,7 @@ function loadUserShields() {
14496
15712
  `);
14497
15713
  continue;
14498
15714
  }
14499
- result[v.ok.name] = v.ok;
15715
+ result[v.ok.name] = withAccessReading(v.ok);
14500
15716
  } catch (err) {
14501
15717
  process.stderr.write(`[node9] Failed to load user shield ${file2}: ${String(err)}
14502
15718
  `);
@@ -14551,7 +15767,7 @@ function readShieldOverrides() {
14551
15767
  }
14552
15768
 
14553
15769
  // src/config/managed.ts
14554
- var MODE_ORDER = ["observe", "audit", "standard", "strict"];
15770
+ var MODE_ORDER = ["observe", "audit", "soft", "standard", "strict", "block-all"];
14555
15771
  var EGRESS_MODE_ORDER = ["off", "review", "block"];
14556
15772
  function rankIn(order, value) {
14557
15773
  return value === void 0 ? -1 : order.indexOf(value);
@@ -14664,74 +15880,12 @@ function applyManagedCommandChecks(local, managed, locked) {
14664
15880
  }
14665
15881
  function applyManagedApprovers(local, managed) {
14666
15882
  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
- ];
15883
+ ...local,
15884
+ native: typeof managed.native === "boolean" ? managed.native : local.native,
15885
+ browser: typeof managed.browser === "boolean" ? managed.browser : local.browser,
15886
+ cloud: typeof managed.cloud === "boolean" ? managed.cloud : local.cloud,
15887
+ terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
15888
+ };
14735
15889
  }
14736
15890
 
14737
15891
  // src/auth/trusted-hosts.ts
@@ -14792,6 +15946,7 @@ function isTrustedHost(host2) {
14792
15946
 
14793
15947
  // src/config/index.ts
14794
15948
  init_dist();
15949
+ init_dist();
14795
15950
 
14796
15951
  // src/config/v2.ts
14797
15952
  init_dist();
@@ -14875,7 +16030,7 @@ function v2ToLegacy(raw, options = {}) {
14875
16030
  continue;
14876
16031
  }
14877
16032
  if (id === "commands.unknown") {
14878
- warnings.push(`checks.${id}: set "mode": "strict" instead`);
16033
+ warnings.push(`checks.${id}: set "mode": "block-all" instead`);
14879
16034
  continue;
14880
16035
  }
14881
16036
  const allowed = configurableValues(id);
@@ -15104,7 +16259,9 @@ var DEFAULT_CONFIG = {
15104
16259
  op: "matches",
15105
16260
  // Single spaces on purpose: a nested `\\s+` inside the alternation
15106
16261
  // 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",
16262
+ // `apps`: turning an app off, allowing one of its actions, or
16263
+ // linking a server to an app (the general checks leave it).
16264
+ 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
16265
  flags: "i"
15109
16266
  }
15110
16267
  ],
@@ -15190,7 +16347,12 @@ var DEFAULT_CONFIG = {
15190
16347
  verdict: "block",
15191
16348
  reason: "Piping remote script into a shell is a supply-chain attack vector",
15192
16349
  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
- }
16350
+ },
16351
+ // ── Actions in apps (node9Firewall#390) ───────────────────────────────
16352
+ // actions.money / actions.access / actions.publish: the action templates
16353
+ // on every MCP server no app shield claims, plus the shell publishing
16354
+ // rules. Review by default; the check value governs them.
16355
+ ...GENERAL_ACTION_RULES
15194
16356
  ],
15195
16357
  dlp: { enabled: true, scanIgnoredTools: true, pii: "block" },
15196
16358
  egress: {
@@ -15227,6 +16389,7 @@ var DEFAULT_CONFIG = {
15227
16389
  trustedHosts: [],
15228
16390
  trustedHostsManaged: false,
15229
16391
  appPermissions: {},
16392
+ appLinks: {},
15230
16393
  managedJailPaths: []
15231
16394
  },
15232
16395
  environments: {}
@@ -15406,6 +16569,11 @@ var KNOB_TO_CHECK = {
15406
16569
  evalDynamic: "commands.eval-dynamic",
15407
16570
  pipeChainHigh: "data.pipe-chain"
15408
16571
  };
16572
+ function statedSignalFields(policy) {
16573
+ return Object.keys(tuningFromLegacyPolicy(policy)["loading.malicious-package"] ?? {}).filter(
16574
+ (k) => k === "newPackage" || k === "installScript"
16575
+ );
16576
+ }
15409
16577
  function catalogSettingsFrom(settings, policy) {
15410
16578
  return catalogSettingsFromConfig(settings, policy);
15411
16579
  }
@@ -15461,6 +16629,7 @@ function getConfig(cwd) {
15461
16629
  trustedHosts: [],
15462
16630
  trustedHostsManaged: false,
15463
16631
  appPermissions: {},
16632
+ appLinks: {},
15464
16633
  managedJailPaths: []
15465
16634
  };
15466
16635
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
@@ -15472,6 +16641,8 @@ function getConfig(cwd) {
15472
16641
  const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
15473
16642
  let ssrfStrictSource = "default";
15474
16643
  const workspaceChecks = {};
16644
+ const managedStatedChecks = /* @__PURE__ */ new Set();
16645
+ const managedStatedSignals = /* @__PURE__ */ new Set();
15475
16646
  const applyLayer = (source, isProject = false, isCloud = false) => {
15476
16647
  if (!source) return;
15477
16648
  const s = source.settings || {};
@@ -15679,9 +16850,11 @@ function getConfig(cwd) {
15679
16850
  if (raw.managedConfig && typeof raw.managedConfig === "object") {
15680
16851
  const mc = raw.managedConfig;
15681
16852
  const locked = Array.isArray(mc.locked) ? mc.locked.filter((f) => typeof f === "string") : [];
16853
+ for (const id of knobStatedChecks(mc)) managedStatedChecks.add(id);
16854
+ for (const k of statedSignalFields(mc)) managedStatedSignals.add(k);
15682
16855
  if (typeof mc.mode === "string") {
15683
16856
  if (keyed) {
15684
- if (["observe", "audit", "standard", "strict"].includes(mc.mode)) {
16857
+ if (isModeValue(mc.mode)) {
15685
16858
  mergedSettings.mode = mc.mode;
15686
16859
  }
15687
16860
  } else {
@@ -15845,19 +17018,26 @@ function getConfig(cwd) {
15845
17018
  }
15846
17019
  if (Array.isArray(mc.jailPaths)) {
15847
17020
  for (const jp of mc.jailPaths) {
15848
- const path27 = typeof jp?.path === "string" ? jp.path.trim() : "";
15849
- if (!path27) continue;
17021
+ const path28 = typeof jp?.path === "string" ? jp.path.trim() : "";
17022
+ if (!path28) continue;
15850
17023
  const verdict = jp?.verdict === "review" ? "review" : "block";
15851
- for (const r of pathRules(path27, verdict, "org-managed jail")) {
17024
+ for (const r of pathRules(path28, verdict, "org-managed jail")) {
15852
17025
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
15853
17026
  }
15854
- mergedPolicy.managedJailPaths.push({ path: path27, verdict });
17027
+ mergedPolicy.managedJailPaths.push({ path: path28, verdict });
15855
17028
  }
15856
17029
  }
15857
17030
  if (Array.isArray(mc.trustedHosts)) {
15858
17031
  mergedPolicy.trustedHostsManaged = true;
15859
17032
  mergedPolicy.trustedHosts = mc.trustedHosts.filter((h) => typeof h === "string").map((h) => normalizeHost2(h));
15860
17033
  }
17034
+ if (mc.appLinks && typeof mc.appLinks === "object" && !Array.isArray(mc.appLinks)) {
17035
+ const links = {};
17036
+ for (const [key, slug2] of Object.entries(mc.appLinks)) {
17037
+ if (typeof slug2 === "string" && /^[a-z0-9-]{1,40}$/.test(slug2)) links[key] = slug2;
17038
+ }
17039
+ mergedPolicy.appLinks = links;
17040
+ }
15861
17041
  if (mc.appPermissions && typeof mc.appPermissions === "object" && !Array.isArray(mc.appPermissions)) {
15862
17042
  const coerced = {};
15863
17043
  for (const [srv, tools] of Object.entries(mc.appPermissions)) {
@@ -15956,14 +17136,15 @@ function getConfig(cwd) {
15956
17136
  const envMode = process.env.NODE9_MODE;
15957
17137
  if (envMode && !modeCloudControlled && // PR-2: one listening point includes the env var — a keyed machine's
15958
17138
  // mode comes from the workspace (or the shipped default), never the shell.
15959
- !keyed && ["observe", "audit", "standard", "strict"].includes(envMode)) {
17139
+ !keyed && isModeValue(envMode)) {
15960
17140
  mergedSettings.mode = envMode;
15961
17141
  }
15962
- if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
17142
+ if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && isRecordOnly(mergedSettings.mode)) {
15963
17143
  mergedSettings.mode = "standard";
15964
17144
  }
15965
- const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
15966
- if (modeCloudControlled && mergedSettings.mode === "strict") {
17145
+ const cloudStrictLevel = levelOf(mergedSettings.mode) === "strict" || levelOf(mergedSettings.mode) === "block-all";
17146
+ const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && cloudStrictLevel;
17147
+ if (modeCloudControlled && cloudStrictLevel) {
15967
17148
  for (const name of Object.keys(mergedEnvironments)) {
15968
17149
  if (mergedEnvironments[name]?.requireApproval === false) {
15969
17150
  const cleaned = { ...mergedEnvironments[name] };
@@ -15981,6 +17162,35 @@ function getConfig(cwd) {
15981
17162
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
15982
17163
  mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
15983
17164
  const resolvedSsrfStrictSource = keyed && ssrfStrictSource === "local" ? "default" : ssrfStrictSource;
17165
+ const level = levelOf(mergedSettings.mode);
17166
+ const levelKnobChecks = Object.entries(levelChecks(level)).filter(([id]) => isKnobGoverned(id));
17167
+ const signals = levelPackageSignals(level);
17168
+ if (levelKnobChecks.length > 0 || signals) {
17169
+ const policyOf = (layer) => layer?.policy ?? {};
17170
+ const trusted = /* @__PURE__ */ new Set([...managedStatedChecks, ...Object.keys(workspaceChecks)]);
17171
+ const trustedSignals = new Set(managedStatedSignals);
17172
+ const project = /* @__PURE__ */ new Set();
17173
+ const projectSignals = /* @__PURE__ */ new Set();
17174
+ if (!keyed) {
17175
+ for (const id of knobStatedChecks(policyOf(globalConfig))) trusted.add(id);
17176
+ for (const k of statedSignalFields(policyOf(globalConfig))) trustedSignals.add(k);
17177
+ for (const id of knobStatedChecks(policyOf(projectConfig))) project.add(id);
17178
+ for (const k of statedSignalFields(policyOf(projectConfig))) projectSignals.add(k);
17179
+ }
17180
+ const before = resolveCheckMap(catalogSettingsFromConfig({}, mergedPolicy));
17181
+ for (const [id, v] of levelKnobChecks) {
17182
+ if (trusted.has(id)) continue;
17183
+ if (project.has(id) && CHECK_VERDICT_RANK[v] <= CHECK_VERDICT_RANK[before[id] ?? "off"])
17184
+ continue;
17185
+ checkToLegacyPolicy(mergedPolicy, id, v);
17186
+ }
17187
+ if (signals) {
17188
+ for (const key of ["newPackage", "installScript"]) {
17189
+ if (trustedSignals.has(key) || projectSignals.has(key)) continue;
17190
+ mergedPolicy.packageCheck[key] = signals[key];
17191
+ }
17192
+ }
17193
+ }
15984
17194
  const explicit = {};
15985
17195
  const checkSources = {};
15986
17196
  if (keyed) {
@@ -16152,6 +17362,9 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
16152
17362
  // src/utils/regex.ts
16153
17363
  init_dist();
16154
17364
 
17365
+ // src/policy/index.ts
17366
+ init_dist();
17367
+
16155
17368
  // src/dlp.ts
16156
17369
  var import_fs5 = __toESM(require("fs"));
16157
17370
  var import_path5 = __toESM(require("path"));
@@ -16403,7 +17616,8 @@ async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
16403
17616
  cwd: prepared.cwd,
16404
17617
  activeEnvironment,
16405
17618
  skipIgnoredFastPath: opts?.skipIgnoredFastPath,
16406
- argumentScan: opts?.argumentScan
17619
+ argumentScan: opts?.argumentScan,
17620
+ mcpServer: opts?.mcpServer
16407
17621
  },
16408
17622
  {
16409
17623
  checkProvenance,
@@ -17071,14 +18285,272 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
17071
18285
 
17072
18286
  // src/auth/orchestrator.ts
17073
18287
  var import_crypto10 = require("crypto");
17074
- var import_os19 = __toESM(require("os"));
18288
+
18289
+ // src/apps/state.ts
18290
+ var import_fs12 = __toESM(require("fs"));
18291
+ var import_os10 = __toESM(require("os"));
18292
+ var import_path12 = __toESM(require("path"));
18293
+
18294
+ // src/apps/catalog.ts
18295
+ var APP_CATALOG = [
18296
+ {
18297
+ slug: "redis",
18298
+ label: "Redis",
18299
+ shield: "redis",
18300
+ servers: ["redis", "valkey"],
18301
+ commands: ["redis-cli", "valkey-cli"],
18302
+ packages: ["redis", "ioredis", "valkey"],
18303
+ envPrefixes: ["REDIS_"]
18304
+ },
18305
+ {
18306
+ slug: "postgres",
18307
+ label: "PostgreSQL",
18308
+ shield: "postgres",
18309
+ servers: ["postgres", "pgsql"],
18310
+ commands: ["psql", "pg_dump", "pg_restore", "pgcli"],
18311
+ packages: ["pg", "postgres", "psycopg", "psycopg2", "psycopg2-binary", "asyncpg"],
18312
+ envPrefixes: ["PGHOST", "PGDATABASE", "POSTGRES_"]
18313
+ },
18314
+ {
18315
+ slug: "mongodb",
18316
+ label: "MongoDB",
18317
+ shield: "mongodb",
18318
+ servers: ["mongo"],
18319
+ commands: ["mongosh", "mongo", "mongodump", "mongorestore"],
18320
+ packages: ["mongodb", "mongoose", "pymongo", "motor"],
18321
+ envPrefixes: ["MONGO"]
18322
+ },
18323
+ {
18324
+ slug: "github",
18325
+ label: "GitHub",
18326
+ shield: "github",
18327
+ servers: ["github"],
18328
+ commands: ["gh"],
18329
+ packages: ["@octokit/", "octokit", "pygithub"],
18330
+ envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"]
18331
+ },
18332
+ {
18333
+ slug: "aws",
18334
+ label: "AWS",
18335
+ shield: "aws",
18336
+ servers: ["aws"],
18337
+ commands: ["aws", "sam", "cdk"],
18338
+ packages: ["aws-sdk", "@aws-sdk/", "boto3", "aws-cdk-lib"],
18339
+ envPrefixes: ["AWS_ACCESS_KEY_ID", "AWS_PROFILE"]
18340
+ },
18341
+ {
18342
+ slug: "k8s",
18343
+ label: "Kubernetes",
18344
+ shield: "k8s",
18345
+ servers: ["kubernetes", "k8s", "kubectl"],
18346
+ commands: ["kubectl", "helm", "k9s"],
18347
+ packages: ["@kubernetes/client-node", "kubernetes"],
18348
+ envPrefixes: ["KUBECONFIG"]
18349
+ },
18350
+ {
18351
+ slug: "docker",
18352
+ label: "Docker",
18353
+ shield: "docker",
18354
+ servers: ["docker"],
18355
+ commands: ["docker", "docker-compose", "podman"],
18356
+ packages: ["dockerode", "docker"],
18357
+ envPrefixes: ["DOCKER_HOST"]
18358
+ },
18359
+ {
18360
+ slug: "stripe",
18361
+ label: "Stripe",
18362
+ shield: "stripe",
18363
+ servers: ["stripe"],
18364
+ commands: ["stripe"],
18365
+ packages: ["stripe", "@stripe/"],
18366
+ envPrefixes: ["STRIPE_"]
18367
+ },
18368
+ {
18369
+ slug: "paypal",
18370
+ label: "PayPal",
18371
+ shield: "paypal",
18372
+ servers: ["paypal"],
18373
+ commands: [],
18374
+ packages: ["@paypal/", "paypalrestsdk", "paypal-checkout-serversdk", "paypal-server-sdk"],
18375
+ envPrefixes: ["PAYPAL_"]
18376
+ },
18377
+ {
18378
+ slug: "square",
18379
+ label: "Square",
18380
+ shield: "square",
18381
+ servers: ["square"],
18382
+ // Squarespace is a website builder. SQUARE_ already misses SQUARESPACE_.
18383
+ notServers: ["squarespace"],
18384
+ commands: [],
18385
+ packages: ["square", "squareup"],
18386
+ envPrefixes: ["SQUARE_"]
18387
+ },
18388
+ {
18389
+ slug: "plaid",
18390
+ label: "Plaid",
18391
+ shield: "plaid",
18392
+ servers: ["plaid"],
18393
+ commands: [],
18394
+ packages: ["plaid", "plaid-python"],
18395
+ envPrefixes: ["PLAID_"]
18396
+ },
18397
+ {
18398
+ slug: "okta",
18399
+ label: "Okta",
18400
+ shield: "okta",
18401
+ servers: ["okta"],
18402
+ commands: ["okta"],
18403
+ packages: ["@okta/", "okta"],
18404
+ envPrefixes: ["OKTA_"]
18405
+ },
18406
+ {
18407
+ slug: "auth0",
18408
+ label: "Auth0",
18409
+ shield: "auth0",
18410
+ servers: ["auth0"],
18411
+ commands: ["auth0"],
18412
+ packages: ["auth0", "@auth0/"],
18413
+ envPrefixes: ["AUTH0_"]
18414
+ }
18415
+ ];
18416
+ var APP_BY_SLUG = new Map(
18417
+ APP_CATALOG.map((a) => [a.slug, a])
18418
+ );
18419
+
18420
+ // src/apps/state.ts
18421
+ init_audit();
18422
+
18423
+ // src/daemon/mcp-tools.ts
18424
+ var import_fs11 = __toESM(require("fs"));
18425
+ var import_path11 = __toESM(require("path"));
18426
+ var import_os9 = __toESM(require("os"));
18427
+ function getMcpToolsFile() {
18428
+ return import_path11.default.join(import_os9.default.homedir(), ".node9", "mcp-tools.json");
18429
+ }
18430
+ function readMcpToolsConfig() {
18431
+ try {
18432
+ const file2 = getMcpToolsFile();
18433
+ if (!import_fs11.default.existsSync(file2)) return {};
18434
+ const raw = import_fs11.default.readFileSync(file2, "utf-8");
18435
+ return JSON.parse(raw);
18436
+ } catch {
18437
+ return {};
18438
+ }
18439
+ }
18440
+
18441
+ // src/apps/state.ts
18442
+ function appsFilePath(home = import_os10.default.homedir()) {
18443
+ return import_path12.default.join(home, ".node9", "apps.json");
18444
+ }
18445
+ var SIGNAL_KINDS = /* @__PURE__ */ new Set(["mcp-server", "command", "package", "env-name"]);
18446
+ function emptyFile() {
18447
+ return { version: 1, apps: /* @__PURE__ */ Object.create(null), links: /* @__PURE__ */ Object.create(null) };
18448
+ }
18449
+ function ownString(map, key) {
18450
+ if (!Object.prototype.hasOwnProperty.call(map, key)) return void 0;
18451
+ const v = map[key];
18452
+ return typeof v === "string" ? v : void 0;
18453
+ }
18454
+ function readSignals(raw) {
18455
+ if (!Array.isArray(raw)) return [];
18456
+ const out = [];
18457
+ for (const x of raw) {
18458
+ if (!x || typeof x.kind !== "string" || typeof x.value !== "string") continue;
18459
+ if (!SIGNAL_KINDS.has(x.kind)) continue;
18460
+ out.push({
18461
+ kind: x.kind,
18462
+ value: x.value.slice(0, 256),
18463
+ ...typeof x.project === "string" ? { project: x.project } : {}
18464
+ });
18465
+ }
18466
+ return out;
18467
+ }
18468
+ function parseAppsFile(text) {
18469
+ const raw = JSON.parse(text);
18470
+ const file2 = emptyFile();
18471
+ for (const [slug2, r] of Object.entries(raw.apps ?? {})) {
18472
+ if (!APP_BY_SLUG.has(slug2) || !r || r.state !== "on" && r.state !== "off") continue;
18473
+ file2.apps[slug2] = {
18474
+ state: r.state,
18475
+ by: r.by === "user" || r.by === "workspace" ? r.by : "detected",
18476
+ at: typeof r.at === "string" ? r.at : "",
18477
+ signals: readSignals(r.signals)
18478
+ };
18479
+ }
18480
+ for (const [server, slug2] of Object.entries(raw.links ?? {})) {
18481
+ if (typeof slug2 === "string" && APP_BY_SLUG.has(slug2)) file2.links[server.toLowerCase()] = slug2;
18482
+ }
18483
+ return file2;
18484
+ }
18485
+ function readAppsFile(home) {
18486
+ return readAppsFileStrict(home) ?? emptyFile();
18487
+ }
18488
+ function readAppsFileStrict(home) {
18489
+ let text;
18490
+ try {
18491
+ text = import_fs12.default.readFileSync(appsFilePath(home), "utf8");
18492
+ } catch (e) {
18493
+ return e.code === "ENOENT" ? emptyFile() : null;
18494
+ }
18495
+ try {
18496
+ return parseAppsFile(text);
18497
+ } catch {
18498
+ return null;
18499
+ }
18500
+ }
18501
+ var linkCache = null;
18502
+ var NO_LINKS = /* @__PURE__ */ Object.create(null);
18503
+ function localLinks() {
18504
+ const file2 = appsFilePath();
18505
+ try {
18506
+ const mtimeMs = import_fs12.default.statSync(file2).mtimeMs;
18507
+ if (!linkCache || linkCache.file !== file2 || linkCache.mtimeMs !== mtimeMs) {
18508
+ linkCache = { mtimeMs, file: file2, links: readAppsFile().links };
18509
+ }
18510
+ return linkCache.links;
18511
+ } catch {
18512
+ return NO_LINKS;
18513
+ }
18514
+ }
18515
+ function resolveLinkedServer(label2, opts = {}) {
18516
+ if (typeof label2 !== "string" || !label2) return label2;
18517
+ const applied = new Set(opts.appliedShields ?? []);
18518
+ const live = (slug2) => {
18519
+ const app = slug2 ? APP_BY_SLUG.get(slug2) : void 0;
18520
+ return app && applied.has(app.shield) ? slug2 : void 0;
18521
+ };
18522
+ const lower2 = label2.toLowerCase();
18523
+ if (!opts.workspaceGoverned) {
18524
+ const local = live(ownString(localLinks(), lower2));
18525
+ if (local) return local;
18526
+ }
18527
+ const managed = opts.managedLinks ?? {};
18528
+ if (opts.serverKey) {
18529
+ const byKey = live(ownString(managed, opts.serverKey));
18530
+ if (byKey) return byKey;
18531
+ }
18532
+ const keys = Object.keys(managed);
18533
+ if (keys.length === 0) return label2;
18534
+ const tools = readMcpToolsConfig();
18535
+ for (const key of keys) {
18536
+ const name = key.startsWith("cfg:") ? key.split(":").slice(2).join(":") : Object.prototype.hasOwnProperty.call(tools, key) ? tools[key]?.name : void 0;
18537
+ if (typeof name === "string" && name.toLowerCase() === lower2) {
18538
+ const linked = live(ownString(managed, key));
18539
+ if (linked) return linked;
18540
+ }
18541
+ }
18542
+ return label2;
18543
+ }
18544
+
18545
+ // src/auth/orchestrator.ts
18546
+ var import_os20 = __toESM(require("os"));
17075
18547
 
17076
18548
  // src/ui/native.ts
17077
18549
  var import_child_process = require("child_process");
17078
- var import_path12 = __toESM(require("path"));
18550
+ var import_path14 = __toESM(require("path"));
17079
18551
 
17080
18552
  // src/context-sniper.ts
17081
- var import_path11 = __toESM(require("path"));
18553
+ var import_path13 = __toESM(require("path"));
17082
18554
  function smartTruncate(str, maxLen = 500) {
17083
18555
  if (str.length <= maxLen) return str;
17084
18556
  const edge = Math.floor(maxLen / 2) - 3;
@@ -17146,7 +18618,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
17146
18618
  intent = "EDIT";
17147
18619
  if (obj.file_path) {
17148
18620
  editFilePath = String(obj.file_path);
17149
- editFileName = import_path11.default.basename(editFilePath);
18621
+ editFileName = import_path13.default.basename(editFilePath);
17150
18622
  }
17151
18623
  const result = extractContext(String(obj.new_string), matchedWord);
17152
18624
  contextSnippet = result.snippet;
@@ -17214,7 +18686,7 @@ function formatArgs(args, matchedField, matchedWord) {
17214
18686
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
17215
18687
  const obj = parsed;
17216
18688
  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";
18689
+ const file2 = obj.file_path ? import_path14.default.basename(String(obj.file_path)) : "file";
17218
18690
  const oldPreview = smartTruncate(String(obj.old_string), 120);
17219
18691
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
17220
18692
  return {
@@ -17417,24 +18889,24 @@ end run`;
17417
18889
  }
17418
18890
 
17419
18891
  // 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"));
18892
+ var import_fs14 = __toESM(require("fs"));
18893
+ var import_os12 = __toESM(require("os"));
18894
+ var import_path16 = __toESM(require("path"));
17423
18895
  init_dist();
17424
18896
 
17425
18897
  // 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"));
18898
+ var import_fs13 = __toESM(require("fs"));
18899
+ var import_os11 = __toESM(require("os"));
18900
+ var import_path15 = __toESM(require("path"));
17429
18901
  init_dist();
17430
18902
  var USER_JAIL_SHIELD = "user-jail";
17431
18903
  function jailStorePath() {
17432
- return import_path13.default.join(import_os9.default.homedir(), ".node9", "jail-paths.json");
18904
+ return import_path15.default.join(import_os11.default.homedir(), ".node9", "jail-paths.json");
17433
18905
  }
17434
18906
  function readJailPaths() {
17435
18907
  let text;
17436
18908
  try {
17437
- text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
18909
+ text = import_fs13.default.readFileSync(jailStorePath(), "utf8");
17438
18910
  } catch (err) {
17439
18911
  if (err.code === "ENOENT") return [];
17440
18912
  throw err;
@@ -17463,7 +18935,7 @@ function findJailedPathIn(candidate, paths) {
17463
18935
 
17464
18936
  // src/canary/registry.ts
17465
18937
  function canaryStorePath() {
17466
- return import_path14.default.join(import_os10.default.homedir(), ".node9", "canaries.json");
18938
+ return import_path16.default.join(import_os12.default.homedir(), ".node9", "canaries.json");
17467
18939
  }
17468
18940
  function isRecord(x) {
17469
18941
  if (!x || typeof x !== "object") return false;
@@ -17474,7 +18946,7 @@ function loadCanaries(opts) {
17474
18946
  const p = canaryStorePath();
17475
18947
  let raw;
17476
18948
  try {
17477
- raw = import_fs12.default.readFileSync(p, "utf-8");
18949
+ raw = import_fs14.default.readFileSync(p, "utf-8");
17478
18950
  } catch (e) {
17479
18951
  if (e.code === "ENOENT") return [];
17480
18952
  throw e;
@@ -17500,22 +18972,22 @@ function canaryValues() {
17500
18972
  init_dist();
17501
18973
 
17502
18974
  // 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"));
18975
+ var import_fs15 = __toESM(require("fs"));
18976
+ var import_os13 = __toESM(require("os"));
18977
+ var import_path17 = __toESM(require("path"));
17506
18978
  init_dist();
17507
18979
  var MAX_SCRIPT_BYTES = 64 * 1024;
17508
18980
  var MAX_RECORDED_WRITES = 32;
17509
18981
  function readScriptFile(resolved) {
17510
18982
  let fd;
17511
18983
  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);
18984
+ fd = import_fs15.default.openSync(resolved, import_fs15.default.constants.O_RDONLY | (import_fs15.default.constants.O_NONBLOCK ?? 0));
18985
+ const st = import_fs15.default.fstatSync(fd);
17514
18986
  if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
17515
18987
  const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
17516
18988
  let n = 0;
17517
18989
  while (n < buf.length) {
17518
- const got = import_fs13.default.readSync(fd, buf, n, buf.length - n, n);
18990
+ const got = import_fs15.default.readSync(fd, buf, n, buf.length - n, n);
17519
18991
  if (got === 0) break;
17520
18992
  n += got;
17521
18993
  }
@@ -17526,7 +18998,7 @@ function readScriptFile(resolved) {
17526
18998
  } finally {
17527
18999
  if (fd !== void 0) {
17528
19000
  try {
17529
- import_fs13.default.closeSync(fd);
19001
+ import_fs15.default.closeSync(fd);
17530
19002
  } catch {
17531
19003
  }
17532
19004
  }
@@ -17534,9 +19006,9 @@ function readScriptFile(resolved) {
17534
19006
  }
17535
19007
  function absolute(p, cwd, home) {
17536
19008
  const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
17537
- return import_path15.default.resolve(cwd, spelled);
19009
+ return import_path17.default.resolve(cwd, spelled);
17538
19010
  }
17539
- async function resolveSessionScript(toolName, args, cwd, layers, deps, home = import_os11.default.homedir()) {
19011
+ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = import_os13.default.homedir()) {
17540
19012
  try {
17541
19013
  const d = deps ?? { checkWritten, readScript: readScriptFile };
17542
19014
  const shell = resolveShellInput(toolName, args, layers);
@@ -17565,7 +19037,7 @@ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = im
17565
19037
  return null;
17566
19038
  }
17567
19039
  }
17568
- function sessionWriteTargets(toolName, args, fileAccess, cwd, home = import_os11.default.homedir()) {
19040
+ function sessionWriteTargets(toolName, args, fileAccess, cwd, home = import_os13.default.homedir()) {
17569
19041
  const base2 = cwd ?? process.cwd();
17570
19042
  const out = /* @__PURE__ */ new Set();
17571
19043
  const argsObj = args && typeof args === "object" ? args : {};
@@ -17595,26 +19067,26 @@ init_audit();
17595
19067
  init_dist();
17596
19068
 
17597
19069
  // src/exposure/known-hosts.ts
17598
- var import_fs15 = __toESM(require("fs"));
17599
- var import_path17 = __toESM(require("path"));
19070
+ var import_fs17 = __toESM(require("fs"));
19071
+ var import_path19 = __toESM(require("path"));
17600
19072
 
17601
19073
  // src/utils/git-remotes.ts
17602
- var import_fs14 = __toESM(require("fs"));
17603
- var import_path16 = __toESM(require("path"));
19074
+ var import_fs16 = __toESM(require("fs"));
19075
+ var import_path18 = __toESM(require("path"));
17604
19076
  init_dist();
17605
19077
  function readSmallFile(p, max) {
17606
19078
  let fd;
17607
19079
  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);
19080
+ fd = import_fs16.default.openSync(p, import_fs16.default.constants.O_RDONLY | (import_fs16.default.constants.O_NOFOLLOW ?? 0));
19081
+ const st = import_fs16.default.fstatSync(fd);
17610
19082
  if (!st.isFile() || st.size > max) return null;
17611
19083
  const buf = Buffer.alloc(st.size);
17612
- const n = import_fs14.default.readSync(fd, buf, 0, st.size, 0);
19084
+ const n = import_fs16.default.readSync(fd, buf, 0, st.size, 0);
17613
19085
  return buf.subarray(0, n).toString("utf8");
17614
19086
  } catch {
17615
19087
  return null;
17616
19088
  } finally {
17617
- if (fd !== void 0) import_fs14.default.closeSync(fd);
19089
+ if (fd !== void 0) import_fs16.default.closeSync(fd);
17618
19090
  }
17619
19091
  }
17620
19092
  function gitRemoteHost(url) {
@@ -17633,20 +19105,20 @@ function gitRemoteHost(url) {
17633
19105
  return null;
17634
19106
  }
17635
19107
  function findGitConfig(dir2, walkUp = true) {
17636
- let cur = import_path16.default.resolve(dir2);
19108
+ let cur = import_path18.default.resolve(dir2);
17637
19109
  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);
19110
+ const dotGit = import_path18.default.join(cur, ".git");
19111
+ const direct = readSmallFile(import_path18.default.join(dotGit, "config"), 1e6);
17640
19112
  if (direct !== null) return direct;
17641
19113
  const pointer = readSmallFile(dotGit, 4096);
17642
19114
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
17643
19115
  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);
19116
+ let gitDir = import_path18.default.resolve(cur, m[1]);
19117
+ const common = readSmallFile(import_path18.default.join(gitDir, "commondir"), 4096);
19118
+ if (common !== null) gitDir = import_path18.default.resolve(gitDir, common.trim());
19119
+ return readSmallFile(import_path18.default.join(gitDir, "config"), 1e6);
17648
19120
  }
17649
- const parent = import_path16.default.dirname(cur);
19121
+ const parent = import_path18.default.dirname(cur);
17650
19122
  if (parent === cur) return null;
17651
19123
  cur = parent;
17652
19124
  }
@@ -17656,12 +19128,12 @@ var cache = /* @__PURE__ */ new Map();
17656
19128
  function readGitRemotes(dir2, opts = {}) {
17657
19129
  if (!dir2) return [];
17658
19130
  const walkUp = opts.walkUp !== false;
17659
- const key = `${walkUp ? "up" : "at"}:${import_path16.default.resolve(dir2)}`;
19131
+ const key = `${walkUp ? "up" : "at"}:${import_path18.default.resolve(dir2)}`;
17660
19132
  const hit = cache.get(key);
17661
19133
  if (hit) return hit;
17662
19134
  const out = [];
17663
19135
  try {
17664
- const cfg = findGitConfig(import_path16.default.resolve(dir2), walkUp);
19136
+ const cfg = findGitConfig(import_path18.default.resolve(dir2), walkUp);
17665
19137
  if (cfg !== null) {
17666
19138
  let current = null;
17667
19139
  for (const line of cfg.split(/\r?\n/)) {
@@ -17697,15 +19169,15 @@ var ECOSYSTEMS = [
17697
19169
  ];
17698
19170
  var cache2 = /* @__PURE__ */ new Map();
17699
19171
  function manifestRoot(dir2) {
17700
- let cur = import_path17.default.resolve(dir2);
19172
+ let cur = import_path19.default.resolve(dir2);
17701
19173
  for (let i = 0; i < 64; i++) {
17702
19174
  for (const [m] of ECOSYSTEMS) {
17703
19175
  try {
17704
- if (import_fs15.default.existsSync(import_path17.default.join(cur, m))) return cur;
19176
+ if (import_fs17.default.existsSync(import_path19.default.join(cur, m))) return cur;
17705
19177
  } catch {
17706
19178
  }
17707
19179
  }
17708
- const parent = import_path17.default.dirname(cur);
19180
+ const parent = import_path19.default.dirname(cur);
17709
19181
  if (parent === cur) return null;
17710
19182
  cur = parent;
17711
19183
  }
@@ -17714,7 +19186,7 @@ function manifestRoot(dir2) {
17714
19186
  function knownProjectHosts(dir2) {
17715
19187
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
17716
19188
  if (!dir2) return empty;
17717
- const key = import_path17.default.resolve(dir2);
19189
+ const key = import_path19.default.resolve(dir2);
17718
19190
  const hit = cache2.get(key);
17719
19191
  if (hit) return hit;
17720
19192
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
@@ -17727,10 +19199,10 @@ function knownProjectHosts(dir2) {
17727
19199
  const root = manifestRoot(key);
17728
19200
  if (root !== null) {
17729
19201
  for (const [m, registry] of ECOSYSTEMS) {
17730
- if (import_fs15.default.existsSync(import_path17.default.join(root, m))) out.hosts.add(registry);
19202
+ if (import_fs17.default.existsSync(import_path19.default.join(root, m))) out.hosts.add(registry);
17731
19203
  }
17732
19204
  try {
17733
- if (import_fs15.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
19205
+ if (import_fs17.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17734
19206
  out.hosts.add("host:rubygems.org");
17735
19207
  } catch {
17736
19208
  }
@@ -17742,9 +19214,9 @@ function knownProjectHosts(dir2) {
17742
19214
  }
17743
19215
 
17744
19216
  // 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"));
19217
+ var import_fs18 = __toESM(require("fs"));
19218
+ var import_os14 = __toESM(require("os"));
19219
+ var import_path20 = __toESM(require("path"));
17748
19220
  init_dist();
17749
19221
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
17750
19222
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
@@ -17774,7 +19246,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17774
19246
  if (typeof a.content === "string") {
17775
19247
  try {
17776
19248
  const next = JSON.parse(a.content);
17777
- const prev = JSON.parse(import_fs16.default.readFileSync(absPath, "utf8"));
19249
+ const prev = JSON.parse(import_fs18.default.readFileSync(absPath, "utf8"));
17778
19250
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
17779
19251
  } catch {
17780
19252
  return LIFECYCLE_RE.test(a.content);
@@ -17784,8 +19256,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
17784
19256
  }
17785
19257
  function judgeWritesAfterExposure(opts) {
17786
19258
  const holds = [];
17787
- const home = opts.home ?? import_os12.default.homedir();
17788
- const tmp = normalizeFilePath(import_os12.default.tmpdir());
19259
+ const home = opts.home ?? import_os14.default.homedir();
19260
+ const tmp = normalizeFilePath(import_os14.default.tmpdir());
17789
19261
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
17790
19262
  const seen = /* @__PURE__ */ new Set();
17791
19263
  for (const access of opts.accesses) {
@@ -17795,7 +19267,7 @@ function judgeWritesAfterExposure(opts) {
17795
19267
  seen.add(p);
17796
19268
  if (p === "/dev" || p.startsWith("/dev/")) continue;
17797
19269
  if (root !== null && isInside(root, p)) {
17798
- if (isAgentInstructionPath(p, root) && (import_path18.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
19270
+ if (isAgentInstructionPath(p, root) && (import_path20.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17799
19271
  holds.push({ path: p, why: "agent-instruction" });
17800
19272
  continue;
17801
19273
  }
@@ -17817,25 +19289,25 @@ function judgeWritesAfterExposure(opts) {
17817
19289
 
17818
19290
  // src/exposure/marker.ts
17819
19291
  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"));
19292
+ var import_fs19 = __toESM(require("fs"));
19293
+ var import_os15 = __toESM(require("os"));
19294
+ var import_path21 = __toESM(require("path"));
17823
19295
 
17824
19296
  // src/daemon/exposure-store.ts
17825
19297
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
17826
19298
 
17827
19299
  // src/exposure/marker.ts
17828
19300
  function dir() {
17829
- return import_path19.default.join(import_os13.default.homedir(), ".node9", "exposure-sessions");
19301
+ return import_path21.default.join(import_os15.default.homedir(), ".node9", "exposure-sessions");
17830
19302
  }
17831
19303
  function file(sessionId) {
17832
19304
  const id = import_crypto8.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
17833
- return import_path19.default.join(dir(), id);
19305
+ return import_path21.default.join(dir(), id);
17834
19306
  }
17835
19307
  function maybeSessionExposed(sessionId) {
17836
19308
  if (!sessionId) return false;
17837
19309
  try {
17838
- const st = import_fs17.default.statSync(file(sessionId));
19310
+ const st = import_fs19.default.statSync(file(sessionId));
17839
19311
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
17840
19312
  } catch (e) {
17841
19313
  return e.code !== "ENOENT";
@@ -17843,27 +19315,7 @@ function maybeSessionExposed(sessionId) {
17843
19315
  }
17844
19316
 
17845
19317
  // 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
19318
+ var import_fs20 = __toESM(require("fs"));
17867
19319
  var EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
17868
19320
  var AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
17869
19321
  function foldServerName(name) {
@@ -17873,7 +19325,7 @@ var inventoryCache = null;
17873
19325
  var inventoryStamp = null;
17874
19326
  function inventoryFileStamp() {
17875
19327
  try {
17876
- const st = import_fs19.default.statSync(getMcpToolsFile());
19328
+ const st = import_fs20.default.statSync(getMcpToolsFile());
17877
19329
  return `${st.mtimeMs}:${st.size}`;
17878
19330
  } catch {
17879
19331
  return "";
@@ -17957,16 +19409,16 @@ function sourceWord(toolName) {
17957
19409
  }
17958
19410
 
17959
19411
  // 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"));
19412
+ var import_fs21 = __toESM(require("fs"));
19413
+ var import_path22 = __toESM(require("path"));
19414
+ var import_os16 = __toESM(require("os"));
17963
19415
  init_dist();
17964
19416
  function getTrustedRecipientsPath() {
17965
- return import_path21.default.join(import_os15.default.homedir(), ".node9", "trusted-recipients.json");
19417
+ return import_path22.default.join(import_os16.default.homedir(), ".node9", "trusted-recipients.json");
17966
19418
  }
17967
19419
  function readTrustedRecipients() {
17968
19420
  try {
17969
- const raw = import_fs20.default.readFileSync(getTrustedRecipientsPath(), "utf8");
19421
+ const raw = import_fs21.default.readFileSync(getTrustedRecipientsPath(), "utf8");
17970
19422
  const parsed = JSON.parse(raw);
17971
19423
  return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
17972
19424
  } catch {
@@ -17975,10 +19427,10 @@ function readTrustedRecipients() {
17975
19427
  }
17976
19428
  function writeTrustedRecipients(entries) {
17977
19429
  const filePath = getTrustedRecipientsPath();
17978
- import_fs20.default.mkdirSync(import_path21.default.dirname(filePath), { recursive: true });
19430
+ import_fs21.default.mkdirSync(import_path22.default.dirname(filePath), { recursive: true });
17979
19431
  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);
19432
+ import_fs21.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
19433
+ import_fs21.default.renameSync(tmp, filePath);
17982
19434
  }
17983
19435
  function addTrustedRecipient(raw, addedBy = "user") {
17984
19436
  const value = normalizeRecipient(raw);
@@ -17993,7 +19445,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
17993
19445
  var cache3 = null;
17994
19446
  function mtimeOf() {
17995
19447
  try {
17996
- return import_fs20.default.statSync(getTrustedRecipientsPath()).mtimeMs;
19448
+ return import_fs21.default.statSync(getTrustedRecipientsPath()).mtimeMs;
17997
19449
  } catch {
17998
19450
  return 0;
17999
19451
  }
@@ -18007,41 +19459,41 @@ function isTrustedRecipient(value) {
18007
19459
  }
18008
19460
 
18009
19461
  // 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"));
19462
+ var import_fs23 = __toESM(require("fs"));
19463
+ var import_os17 = __toESM(require("os"));
19464
+ var import_path23 = __toESM(require("path"));
18013
19465
  init_audit();
18014
19466
 
18015
19467
  // src/utils/read-capped.ts
18016
- var import_fs21 = __toESM(require("fs"));
19468
+ var import_fs22 = __toESM(require("fs"));
18017
19469
  function readCapped(file2, maxBytes) {
18018
19470
  if (maxBytes < 0) return null;
18019
19471
  let fd;
18020
19472
  try {
18021
- fd = import_fs21.default.openSync(file2, "r");
19473
+ fd = import_fs22.default.openSync(file2, "r");
18022
19474
  } catch {
18023
19475
  return null;
18024
19476
  }
18025
19477
  try {
18026
- const st = import_fs21.default.fstatSync(fd);
19478
+ const st = import_fs22.default.fstatSync(fd);
18027
19479
  if (!st.isFile()) return null;
18028
19480
  const want = Math.min(st.size, maxBytes + 1);
18029
19481
  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);
19482
+ let read2 = 0;
19483
+ while (read2 < want) {
19484
+ const n = import_fs22.default.readSync(fd, buf, read2, want - read2, read2);
18033
19485
  if (n <= 0) break;
18034
- read += n;
19486
+ read2 += n;
18035
19487
  }
18036
19488
  return {
18037
- bytes: buf.subarray(0, Math.min(read, maxBytes)),
18038
- truncated: read > maxBytes
19489
+ bytes: buf.subarray(0, Math.min(read2, maxBytes)),
19490
+ truncated: read2 > maxBytes
18039
19491
  };
18040
19492
  } catch {
18041
19493
  return null;
18042
19494
  } finally {
18043
19495
  try {
18044
- import_fs21.default.closeSync(fd);
19496
+ import_fs22.default.closeSync(fd);
18045
19497
  } catch {
18046
19498
  }
18047
19499
  }
@@ -18086,7 +19538,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18086
19538
  let ciContext;
18087
19539
  if (process.env.CI) {
18088
19540
  try {
18089
- const ciContextPath = import_path22.default.join(import_os16.default.homedir(), ".node9", "ci-context.json");
19541
+ const ciContextPath = import_path23.default.join(import_os17.default.homedir(), ".node9", "ci-context.json");
18090
19542
  const ci = readCappedText(ciContextPath, 1e4);
18091
19543
  if (!ci) throw new Error("ci-context.json unreadable");
18092
19544
  if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
@@ -18128,9 +19580,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
18128
19580
  context: {
18129
19581
  agent: meta?.agent,
18130
19582
  mcpServer: meta?.mcpServer,
18131
- hostname: import_os16.default.hostname(),
19583
+ hostname: import_os17.default.hostname(),
18132
19584
  cwd: process.cwd(),
18133
- platform: import_os16.default.platform()
19585
+ platform: import_os17.default.platform()
18134
19586
  },
18135
19587
  // The SaaS validates riskMetadata strictly and would drop it whole on
18136
19588
  // an unknown key, so only fields its schema declares go on the wire.
@@ -18204,14 +19656,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18204
19656
  });
18205
19657
  clearTimeout(timer);
18206
19658
  if (!res.ok) {
18207
- import_fs22.default.appendFileSync(
19659
+ import_fs23.default.appendFileSync(
18208
19660
  HOOK_DEBUG_LOG,
18209
19661
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
18210
19662
  `
18211
19663
  );
18212
19664
  }
18213
19665
  } catch (err) {
18214
- import_fs22.default.appendFileSync(
19666
+ import_fs23.default.appendFileSync(
18215
19667
  HOOK_DEBUG_LOG,
18216
19668
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
18217
19669
  `
@@ -18220,18 +19672,18 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
18220
19672
  }
18221
19673
 
18222
19674
  // 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"));
19675
+ var import_fs24 = __toESM(require("fs"));
19676
+ var import_path24 = __toESM(require("path"));
19677
+ var import_os18 = __toESM(require("os"));
18226
19678
  init_dist();
18227
19679
  init_dist();
18228
19680
  function loopStateFile() {
18229
- return import_path23.default.join(import_os17.default.homedir(), ".node9", "loop-state.json");
19681
+ return import_path24.default.join(import_os18.default.homedir(), ".node9", "loop-state.json");
18230
19682
  }
18231
19683
  function readState() {
18232
19684
  try {
18233
- if (!import_fs23.default.existsSync(loopStateFile())) return [];
18234
- const raw = import_fs23.default.readFileSync(loopStateFile(), "utf-8");
19685
+ if (!import_fs24.default.existsSync(loopStateFile())) return [];
19686
+ const raw = import_fs24.default.readFileSync(loopStateFile(), "utf-8");
18235
19687
  const parsed = JSON.parse(raw);
18236
19688
  if (!Array.isArray(parsed)) return [];
18237
19689
  return parsed;
@@ -18240,11 +19692,11 @@ function readState() {
18240
19692
  }
18241
19693
  }
18242
19694
  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());
19695
+ const dir2 = import_path24.default.dirname(loopStateFile());
19696
+ if (!import_fs24.default.existsSync(dir2)) import_fs24.default.mkdirSync(dir2, { recursive: true });
19697
+ const tmpPath = `${loopStateFile()}.${import_os18.default.hostname()}.${process.pid}.tmp`;
19698
+ import_fs24.default.writeFileSync(tmpPath, JSON.stringify(records));
19699
+ import_fs24.default.renameSync(tmpPath, loopStateFile());
18248
19700
  }
18249
19701
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18250
19702
  try {
@@ -18258,21 +19710,21 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
18258
19710
 
18259
19711
  // src/supply-chain/check.ts
18260
19712
  var import_picomatch3 = __toESM(require("picomatch"));
18261
- var import_path26 = __toESM(require("path"));
19713
+ var import_path27 = __toESM(require("path"));
18262
19714
  init_dist();
18263
19715
 
18264
19716
  // 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"));
19717
+ var import_fs25 = __toESM(require("fs"));
19718
+ var import_os19 = __toESM(require("os"));
19719
+ var import_path25 = __toESM(require("path"));
18268
19720
  var import_crypto9 = require("crypto");
18269
19721
  init_dist();
18270
19722
  var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
18271
19723
  function osvRoot() {
18272
- return import_path24.default.join(import_os18.default.homedir(), ".node9", "osv");
19724
+ return import_path25.default.join(import_os19.default.homedir(), ".node9", "osv");
18273
19725
  }
18274
19726
  function ecoDir(eco) {
18275
- return import_path24.default.join(osvRoot(), eco);
19727
+ return import_path25.default.join(osvRoot(), eco);
18276
19728
  }
18277
19729
  function indexKey(eco, name) {
18278
19730
  switch (eco) {
@@ -18291,11 +19743,11 @@ function shardOf(key) {
18291
19743
  return (0, import_crypto9.createHash)("sha1").update(key).digest("hex").slice(0, 2);
18292
19744
  }
18293
19745
  function shardPath(eco, shard) {
18294
- return import_path24.default.join(ecoDir(eco), "shards", `${shard}.json`);
19746
+ return import_path25.default.join(ecoDir(eco), "shards", `${shard}.json`);
18295
19747
  }
18296
19748
  function readMeta(eco) {
18297
19749
  try {
18298
- const m = JSON.parse(import_fs24.default.readFileSync(import_path24.default.join(ecoDir(eco), "meta.json"), "utf8"));
19750
+ const m = JSON.parse(import_fs25.default.readFileSync(import_path25.default.join(ecoDir(eco), "meta.json"), "utf8"));
18299
19751
  if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
18300
19752
  return m;
18301
19753
  } catch {
@@ -18304,7 +19756,7 @@ function readMeta(eco) {
18304
19756
  }
18305
19757
  function readShard(eco, shard) {
18306
19758
  try {
18307
- const s = JSON.parse(import_fs24.default.readFileSync(shardPath(eco, shard), "utf8"));
19759
+ const s = JSON.parse(import_fs25.default.readFileSync(shardPath(eco, shard), "utf8"));
18308
19760
  return s && typeof s === "object" && !Array.isArray(s) ? s : {};
18309
19761
  } catch {
18310
19762
  return {};
@@ -18430,14 +19882,102 @@ async function queryOsvMalicious(queries) {
18430
19882
  }
18431
19883
 
18432
19884
  // src/supply-chain/registry.ts
19885
+ var import_semver2 = require("semver");
19886
+
19887
+ // src/supply-chain/provenance.ts
18433
19888
  var import_semver = require("semver");
18434
19889
  var TIMEOUT_MS2 = 1500;
18435
- var MAX_BYTES = 2 * 1024 * 1024;
19890
+ var MAX_BYTES = 256 * 1024;
19891
+ function normalizeRepo(raw, host2 = "github.com") {
19892
+ let s = raw.trim().toLowerCase();
19893
+ s = s.replace(/^git\+/, "").replace(/^[a-z]+:\/\//, "");
19894
+ s = s.replace(/@refs\/.*$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
19895
+ if (!s.includes(".") || !s.split("/")[0].includes(".")) s = `${host2}/${s}`;
19896
+ return /^[a-z0-9.-]+\/[\w.-]+\/[\w.-]+/.test(s) ? s : null;
19897
+ }
19898
+ function npmRepoOf(doc) {
19899
+ const list = doc?.attestations;
19900
+ if (!Array.isArray(list)) return null;
19901
+ for (const a of list) {
19902
+ const att = a;
19903
+ if (!att.predicateType?.startsWith("https://slsa.dev/provenance/")) continue;
19904
+ const payload = att.bundle?.dsseEnvelope?.payload;
19905
+ if (typeof payload !== "string") continue;
19906
+ try {
19907
+ const statement = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
19908
+ const p = statement.predicate;
19909
+ const repo = p?.buildDefinition?.externalParameters?.workflow?.repository ?? p?.invocation?.configSource?.uri;
19910
+ if (typeof repo === "string") return normalizeRepo(repo);
19911
+ } catch {
19912
+ }
19913
+ }
19914
+ return null;
19915
+ }
19916
+ function pypiRepoOf(doc) {
19917
+ const bundles = doc?.attestation_bundles;
19918
+ if (!Array.isArray(bundles)) return null;
19919
+ for (const b of bundles) {
19920
+ const pub = b.publisher;
19921
+ if (typeof pub?.repository === "string") {
19922
+ const host2 = /gitlab/i.test(pub.kind ?? "") ? "gitlab.com" : "github.com";
19923
+ return normalizeRepo(pub.repository, host2);
19924
+ }
19925
+ if (/^google$/i.test(pub?.kind ?? "") && typeof pub?.email === "string") {
19926
+ return `google:${pub.email.trim().toLowerCase()}`;
19927
+ }
19928
+ }
19929
+ return null;
19930
+ }
19931
+ async function npmSourceRepo(name, version) {
19932
+ const base2 = endpoints().npmRegistry;
19933
+ if (!base2) return "unknown";
19934
+ const url = `${base2}/-/npm/v1/attestations/${name.replaceAll("/", "%2F")}@${encodeURIComponent(version)}`;
19935
+ return read(url, npmRepoOf);
19936
+ }
19937
+ async function read(url, repoOf) {
19938
+ try {
19939
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
19940
+ if (doc === null) return "none";
19941
+ return repoOf(doc) ?? "unknown";
19942
+ } catch {
19943
+ return "unknown";
19944
+ }
19945
+ }
19946
+ async function pypiSourceRepo(name, version, filename) {
19947
+ const base2 = endpoints().pypi;
19948
+ if (!base2) return "unknown";
19949
+ const url = `${base2}/integrity/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${encodeURIComponent(filename)}/provenance`;
19950
+ return read(url, pypiRepoOf);
19951
+ }
19952
+ var PYPI_FINAL = /^\d+(?:\.\d+)*$/;
19953
+ function comparePypi(a, b) {
19954
+ const x = a.split(".").map(Number);
19955
+ const y = b.split(".").map(Number);
19956
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
19957
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
19958
+ if (d !== 0) return d;
19959
+ }
19960
+ return 0;
19961
+ }
19962
+ function previousRelease(eco, versions, version) {
19963
+ if (eco === "npm") {
19964
+ if (!(0, import_semver.valid)(version)) return void 0;
19965
+ const below2 = versions.filter((v) => (0, import_semver.valid)(v) && !(0, import_semver.prerelease)(v) && (0, import_semver.rcompare)(v, version) > 0);
19966
+ return below2.sort(import_semver.rcompare)[0] ?? null;
19967
+ }
19968
+ if (!PYPI_FINAL.test(version)) return void 0;
19969
+ const below = versions.filter((v) => PYPI_FINAL.test(v) && comparePypi(v, version) < 0);
19970
+ return below.sort((a, b) => comparePypi(b, a))[0] ?? null;
19971
+ }
19972
+
19973
+ // src/supply-chain/registry.ts
19974
+ var TIMEOUT_MS3 = 1500;
19975
+ var MAX_BYTES2 = 2 * 1024 * 1024;
18436
19976
  var VERSION_DOC_MAX_BYTES = 256 * 1024;
18437
19977
  var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
18438
19978
  async function npmVersionDocument(url, selector) {
18439
19979
  const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
18440
- timeoutMs: TIMEOUT_MS2,
19980
+ timeoutMs: TIMEOUT_MS3,
18441
19981
  maxBytes: VERSION_DOC_MAX_BYTES
18442
19982
  });
18443
19983
  if (!doc?.version) return null;
@@ -18461,16 +20001,16 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18461
20001
  let doc;
18462
20002
  try {
18463
20003
  doc = await fetchJson(url, {
18464
- timeoutMs: TIMEOUT_MS2,
18465
- maxBytes: MAX_BYTES,
20004
+ timeoutMs: TIMEOUT_MS3,
20005
+ maxBytes: MAX_BYTES2,
18466
20006
  headers: { Accept: "application/vnd.npm.install-v1+json" }
18467
20007
  });
18468
20008
  } catch (err) {
18469
20009
  if (!/too large/.test(err.message)) throw err;
18470
- const range = !version && spec && (0, import_semver.validRange)(spec) ? spec : null;
20010
+ const range = !version && spec && (0, import_semver2.validRange)(spec) ? spec : null;
18471
20011
  const selector = version ?? (spec && !range ? spec : "latest");
18472
20012
  const info2 = await npmVersionDocument(url, selector);
18473
- if (info2 && range && !(0, import_semver.satisfies)(info2.version, range)) return null;
20013
+ if (info2 && range && !(0, import_semver2.satisfies)(info2.version, range)) return null;
18474
20014
  return info2;
18475
20015
  }
18476
20016
  if (!doc) return null;
@@ -18479,8 +20019,8 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18479
20019
  if (!resolved) {
18480
20020
  if (!spec) resolved = latest;
18481
20021
  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;
20022
+ else if ((0, import_semver2.validRange)(spec)) {
20023
+ resolved = latest && (0, import_semver2.satisfies)(latest, spec) ? latest : (0, import_semver2.maxSatisfying)(Object.keys(doc.versions ?? {}), spec) ?? void 0;
18484
20024
  }
18485
20025
  }
18486
20026
  if (!resolved) return null;
@@ -18490,13 +20030,23 @@ async function npmInfo(name, version, freshWindowMs, raw) {
18490
20030
  version: resolved,
18491
20031
  hasInstallScript: v?.hasInstallScript === true
18492
20032
  };
20033
+ const previous = previousRelease("npm", Object.keys(doc.versions ?? {}), resolved);
20034
+ if (v && previous !== void 0) {
20035
+ const pv = previous ? doc.versions?.[previous] : void 0;
20036
+ info.history = {
20037
+ previous,
20038
+ previousHasInstallScript: pv ? pv.hasInstallScript === true : void 0,
20039
+ provenance: !!v.dist?.attestations?.provenance,
20040
+ previousProvenance: pv ? !!pv.dist?.attestations?.provenance : void 0
20041
+ };
20042
+ }
18493
20043
  if (!Number.isFinite(modifiedMs)) return info;
18494
20044
  if (Date.now() - modifiedMs >= freshWindowMs) {
18495
20045
  info.publishedAtMs = resolved === latest ? modifiedMs : void 0;
18496
20046
  return info;
18497
20047
  }
18498
20048
  try {
18499
- const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BYTES });
20049
+ const full = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18500
20050
  const t = full?.time?.[resolved];
18501
20051
  const tMs = t ? Date.parse(t) : NaN;
18502
20052
  if (Number.isFinite(tMs)) {
@@ -18514,15 +20064,37 @@ async function pypiInfo(name, version) {
18514
20064
  const base2 = endpoints().pypi;
18515
20065
  if (!base2) return null;
18516
20066
  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 });
20067
+ const doc = await fetchJson(url, { timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BYTES2 });
18518
20068
  if (!doc?.info?.version) return null;
18519
20069
  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,
20070
+ const resolved = version ?? doc.info.version;
20071
+ const info = {
20072
+ version: resolved,
18522
20073
  // The earliest file of the release is when it became installable.
18523
- publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
20074
+ publishedAtMs: times.length > 0 ? Math.min(...times) : void 0,
20075
+ file: doc.urls?.find((u) => typeof u.filename === "string")?.filename
20076
+ };
20077
+ if (doc.releases) info.history = pypiHistoryOf(doc.releases, resolved) ?? void 0;
20078
+ return info;
20079
+ }
20080
+ function pypiHistoryOf(releases, version) {
20081
+ const versions = Object.keys(releases).filter((v) => (releases[v] ?? []).length > 0);
20082
+ const previous = previousRelease("PyPI", versions, version);
20083
+ if (previous === void 0) return null;
20084
+ return {
20085
+ previous,
20086
+ previousFile: previous ? releases[previous]?.find((f) => typeof f.filename === "string")?.filename : void 0
18524
20087
  };
18525
20088
  }
20089
+ async function pypiHistory(name, version) {
20090
+ const base2 = endpoints().pypi;
20091
+ if (!base2) return null;
20092
+ const doc = await fetchJson(`${base2}/pypi/${encodeURIComponent(name)}/json`, {
20093
+ timeoutMs: TIMEOUT_MS3,
20094
+ maxBytes: MAX_BYTES2
20095
+ });
20096
+ return doc?.releases ? pypiHistoryOf(doc.releases, version) : null;
20097
+ }
18526
20098
  var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
18527
20099
  async function registryInfo(eco, name, version, freshWindowMs, raw) {
18528
20100
  if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
@@ -18531,19 +20103,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
18531
20103
  }
18532
20104
 
18533
20105
  // src/supply-chain/local-resolve.ts
18534
- var import_fs25 = __toESM(require("fs"));
18535
- var import_path25 = __toESM(require("path"));
20106
+ var import_fs26 = __toESM(require("fs"));
20107
+ var import_path26 = __toESM(require("path"));
18536
20108
  var MAX_LEVELS = 32;
18537
20109
  var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
18538
20110
  function resolveInstalled(name, cwd) {
18539
- if (!cwd || !import_path25.default.isAbsolute(cwd)) return null;
20111
+ if (!cwd || !import_path26.default.isAbsolute(cwd)) return null;
18540
20112
  if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
18541
- let dir2 = import_path25.default.resolve(cwd);
20113
+ let dir2 = import_path26.default.resolve(cwd);
18542
20114
  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");
20115
+ const pkgDir = import_path26.default.join(dir2, "node_modules", ...name.split("/"));
20116
+ const manifest = import_path26.default.join(pkgDir, "package.json");
18545
20117
  try {
18546
- const raw = import_fs25.default.readFileSync(manifest, "utf8");
20118
+ const raw = import_fs26.default.readFileSync(manifest, "utf8");
18547
20119
  const parsed = JSON.parse(raw);
18548
20120
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
18549
20121
  return { version: parsed.version, dir: pkgDir };
@@ -18551,7 +20123,7 @@ function resolveInstalled(name, cwd) {
18551
20123
  return null;
18552
20124
  } catch {
18553
20125
  }
18554
- const parent = import_path25.default.dirname(dir2);
20126
+ const parent = import_path26.default.dirname(dir2);
18555
20127
  if (parent === dir2) break;
18556
20128
  dir2 = parent;
18557
20129
  }
@@ -18673,7 +20245,7 @@ function checkInstalled(p, version, misses) {
18673
20245
  }
18674
20246
  async function checkOne(p, cfg, misses, networkCapped, cwd) {
18675
20247
  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;
20248
+ const localCwd = cwd && import_path27.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path27.default.resolve(dir2, step), cwd) : void 0;
18677
20249
  const installed = resolveInstalled(p.name, localCwd);
18678
20250
  if (installed && (!p.version || p.version === installed.version))
18679
20251
  return checkInstalled(p, installed.version, misses);
@@ -18716,8 +20288,61 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
18716
20288
  detail: `${label(p, version)} runs an install script`
18717
20289
  });
18718
20290
  }
20291
+ if (info && version && (cfg.newPackage !== "off" || cfg.installScript !== "off")) {
20292
+ return byHistory(p, version, info, findings, misses);
20293
+ }
18719
20294
  return findings;
18720
20295
  }
20296
+ var SOFT2 = /* @__PURE__ */ new Set(["new", "install-script"]);
20297
+ async function byHistory(p, version, info, findings, misses) {
20298
+ let out = findings;
20299
+ let h = info.history;
20300
+ if (!h && p.ecosystem === "PyPI" && out.some((f) => SOFT2.has(f.kind))) {
20301
+ try {
20302
+ h = await pypiHistory(p.name, version) ?? void 0;
20303
+ } catch (err) {
20304
+ misses.push(`${label(p, version)}: release history ${err.message}`);
20305
+ }
20306
+ }
20307
+ if (!h || !h.previous) return out;
20308
+ const previous = h.previous;
20309
+ if (h.previousHasInstallScript === true) {
20310
+ out = out.filter((f) => f.kind !== "install-script");
20311
+ }
20312
+ const drop = () => ({
20313
+ pkg: p,
20314
+ version,
20315
+ kind: "provenance-drop",
20316
+ detail: `${label(p, version)} was published without the provenance its previous release (${previous}) had`
20317
+ });
20318
+ if (p.ecosystem === "npm") {
20319
+ if (h.previousProvenance && h.provenance === false) return [...out, drop()];
20320
+ if (!out.some((f) => SOFT2.has(f.kind)) || !h.provenance || !h.previousProvenance) return out;
20321
+ const [now, before] = await Promise.all([
20322
+ npmSourceRepo(p.name, version),
20323
+ npmSourceRepo(p.name, previous)
20324
+ ]);
20325
+ return vouched(p, version, now, before, out, misses);
20326
+ }
20327
+ if (p.ecosystem === "PyPI") {
20328
+ if (!out.some((f) => SOFT2.has(f.kind)) || !info.file || !h.previousFile) return out;
20329
+ const [now, before] = await Promise.all([
20330
+ pypiSourceRepo(p.name, version, info.file),
20331
+ pypiSourceRepo(p.name, previous, h.previousFile)
20332
+ ]);
20333
+ if (now === "none" && before !== "none" && before !== "unknown") return [...out, drop()];
20334
+ return vouched(p, version, now, before, out, misses);
20335
+ }
20336
+ return out;
20337
+ }
20338
+ function vouched(p, version, now, before, findings, misses) {
20339
+ if (now === "unknown" || before === "unknown") {
20340
+ misses.push(`${label(p, version)}: provenance unreadable, findings kept`);
20341
+ return findings;
20342
+ }
20343
+ if (now === "none" || before === "none" || now !== before) return findings;
20344
+ return findings.filter((f) => !SOFT2.has(f.kind));
20345
+ }
18721
20346
  async function runPackageCheck(toolName, args, cfg, cwd) {
18722
20347
  if (!cfg.enabled) return ALLOW;
18723
20348
  try {
@@ -19036,7 +20661,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19036
20661
  appendHookDebug(toolName, args, meta, hashAuditArgs);
19037
20662
  }
19038
20663
  const isManual = meta?.agent === "Terminal";
19039
- const isObserveMode = config.settings.mode === "observe";
20664
+ const isObserveMode = isRecordOnly(config.settings.mode);
19040
20665
  let explainableLabel = "Local Config";
19041
20666
  let dlpReviewFlagged = false;
19042
20667
  let packageReview = null;
@@ -19094,13 +20719,20 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19094
20719
  explainableLabel = "Shell inspection incomplete";
19095
20720
  policyRuleDescription = "The executable command could not be inspected. Approve this call explicitly.";
19096
20721
  }
19097
- const fileReview = !isObserveMode && config.settings.mode !== "audit" && fileFinding?.decision === "review";
20722
+ const fileReview = !isObserveMode && fileFinding?.decision === "review";
19098
20723
  const evaluateCurrentPolicy = async (skipIgnoredFastPath = false) => {
19099
20724
  const own = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
19100
20725
  fileAccess,
19101
20726
  config,
19102
20727
  skipIgnoredFastPath,
19103
- argumentScan: argScan
20728
+ argumentScan: argScan,
20729
+ // A server a person linked to an app is judged as that app (fw#390).
20730
+ mcpServer: resolveLinkedServer(meta?.mcpServer, {
20731
+ serverKey: meta?.serverKey,
20732
+ managedLinks: config.policy.appLinks,
20733
+ appliedShields: config.policy.appliedShields,
20734
+ workspaceGoverned: config.policySource === "workspace"
20735
+ })
19104
20736
  });
19105
20737
  if (!sessionScript || own.decision === "block") return own;
19106
20738
  const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
@@ -19114,7 +20746,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19114
20746
  if (fileFinding) {
19115
20747
  meta = { ...meta, ruleName: fileFinding.ruleName };
19116
20748
  hashAuditArgs = true;
19117
- const observing = isObserveMode || config.settings.mode === "audit";
20749
+ const observing = isObserveMode;
19118
20750
  const blocked = !observing && (fileFinding.decision === "block" || fileReview && config.settings.panicMode);
19119
20751
  appendLocalAudit(
19120
20752
  toolName,
@@ -19316,7 +20948,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19316
20948
  const last = rec.sources[rec.sources.length - 1];
19317
20949
  const readWord = last ? last.word ?? sourceWord(last.tool) : "content";
19318
20950
  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";
20951
+ const read2 = last ? `${/^[aeiou]/i.test(readWord) ? "an" : "a"} ${readWord}` + (last.server ? ` through ${last.server}` : "") + (when ? ` at ${when}` : "") : "outside content";
19320
20952
  if ((held.length > 0 || writeHolds.length > 0) && ucPolicy.afterExposure !== "log-only") {
19321
20953
  const noun = (d) => {
19322
20954
  const k = destinationKind(d);
@@ -19325,14 +20957,14 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19325
20957
  const parts = held.map(
19326
20958
  (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
20959
  );
19328
- const home = import_os19.default.homedir();
20960
+ const home = import_os20.default.homedir();
19329
20961
  const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
19330
20962
  for (const w of writeHolds) {
19331
20963
  parts.push(
19332
20964
  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
20965
  );
19334
20966
  }
19335
- exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
20967
+ exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
19336
20968
  exposureFromEid = last?.eid;
19337
20969
  if (exposureFromEid) exposureMeta = { exposureFromEid };
19338
20970
  exposureKeys = [
@@ -19596,15 +21228,6 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19596
21228
  }
19597
21229
  return { approved: true, checkedBy: "audit" };
19598
21230
  }
19599
- if (config.settings.mode === "audit") {
19600
- if (judge) {
19601
- const policyResult = await evaluateCurrentPolicy();
19602
- if (policyResult.decision === "review") {
19603
- appendLocalAudit(toolName, args, "allow", "audit-mode", ownRowMeta(true), hashAuditArgs);
19604
- }
19605
- }
19606
- return { approved: true, checkedBy: "audit" };
19607
- }
19608
21231
  let appPermReview = null;
19609
21232
  let appPermReviewTool = null;
19610
21233
  const callSpecificReview = () => fileReview || shellReview || dlpReviewFlagged || !!appPermReview || !!taintWarning || !!exposureReview || !!packageReview;