@node9/proxy 2.32.0 → 2.33.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/cli.mjs CHANGED
@@ -2184,32 +2184,109 @@ function evaluateSmartConditions(args, rule) {
2184
2184
  function isBashTool(tool) {
2185
2185
  return BASH_TOOL_NAMES.has(tool.toLowerCase());
2186
2186
  }
2187
- function inspectionEntry(tool, inspection = {}) {
2187
+ function inspectionLayers(policy) {
2188
+ return { trusted: policy.toolInspection, project: policy.projectToolInspection };
2189
+ }
2190
+ function isShellEntry(value) {
2191
+ return typeof value === "object" && value !== null && value.kind === "shell" && typeof value.field === "string" && Object.keys(value).length === 2;
2192
+ }
2193
+ function isInspectionLayers(input) {
2194
+ const trusted = input.trusted;
2195
+ return typeof trusted === "object" && trusted !== null && !isShellEntry(trusted);
2196
+ }
2197
+ function layersOf(input) {
2198
+ return isInspectionLayers(input) ? { trusted: input.trusted, project: input.project ?? {} } : { trusted: input, project: {} };
2199
+ }
2200
+ function entryOf(tool, inspection) {
2188
2201
  const pattern = Object.keys(inspection).find((p) => matchesPattern(tool, p));
2189
2202
  return pattern === void 0 ? void 0 : inspection[pattern];
2190
2203
  }
2204
+ function inspectionEntry(tool, inspection = {}) {
2205
+ const { trusted, project } = layersOf(inspection);
2206
+ return entryOf(tool, trusted) ?? entryOf(tool, project);
2207
+ }
2208
+ function executorName(tool) {
2209
+ const name = tool.toLowerCase();
2210
+ if (!name.startsWith("mcp__")) return name;
2211
+ const at = name.lastIndexOf("__");
2212
+ return at >= "mcp__".length ? name.slice(at + 2) : name;
2213
+ }
2214
+ function entryFields(entry) {
2215
+ if (typeof entry === "object") return [entry.field];
2216
+ return entry === "command" ? ["command"] : null;
2217
+ }
2191
2218
  function shellFields(tool, inspection) {
2192
2219
  if (isBashTool(tool)) return ["command"];
2193
- const entry = inspectionEntry(tool, inspection);
2194
- if (typeof entry === "object") return [entry.field];
2195
- if (entry !== void 0) return entry === "command" ? [entry] : null;
2196
- const name = tool.toLowerCase().replace(/^mcp__[a-z0-9_-]+?__/, "");
2197
- return MCP_EXECUTORS.has(name) ? ["command", "cmd", "script"] : null;
2220
+ const { trusted, project } = layersOf(inspection);
2221
+ const trustedEntry = entryOf(tool, trusted);
2222
+ if (trustedEntry !== void 0) return entryFields(trustedEntry);
2223
+ const builtin = MCP_EXECUTORS.has(executorName(tool)) ? BUILTIN_FIELDS : null;
2224
+ const projectEntry = entryOf(tool, project);
2225
+ if (projectEntry === void 0) return builtin;
2226
+ const added = entryFields(projectEntry);
2227
+ if (!builtin) return added;
2228
+ if (!added) return builtin;
2229
+ return [.../* @__PURE__ */ new Set([...builtin, ...added])];
2198
2230
  }
2199
2231
  function isShellShapedTool(tool, inspection = {}) {
2200
2232
  return shellFields(tool, inspection) !== null;
2201
2233
  }
2234
+ function shellWord(word) {
2235
+ if (BARE_WORD.test(word)) return word;
2236
+ return word === "" ? "''" : `'${word.replace(/'/g, `'\\''`)}'`;
2237
+ }
2238
+ function isStringArray(value) {
2239
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
2240
+ }
2241
+ function commandLine(args, fieldPath, value, native) {
2242
+ const missing = { reason: "Shell command field is missing or is not a string" };
2243
+ const unreadable = { reason: "Shell command arguments could not be read" };
2244
+ if (typeof value !== "string" && (native || !Array.isArray(value))) return missing;
2245
+ const dot = fieldPath.lastIndexOf(".");
2246
+ const parent = dot === -1 ? args : getNestedValue(args, fieldPath.slice(0, dot));
2247
+ const siblings = ARGV_FIELDS.map((key) => ({
2248
+ key,
2249
+ value: parent && typeof parent === "object" ? parent[key] : void 0
2250
+ })).filter(({ value: value2 }) => value2 !== void 0 && value2 !== null);
2251
+ let argv = siblings[0];
2252
+ if (native) argv = siblings.find(({ value: v }) => isStringArray(v));
2253
+ else if (siblings.length === 2 && JSON.stringify(siblings[0].value) !== JSON.stringify(siblings[1].value))
2254
+ return { reason: "Shell command arguments disagree" };
2255
+ let command;
2256
+ let source = fieldPath;
2257
+ if (Array.isArray(value)) {
2258
+ if (argv || !value.length || !isStringArray(value)) return unreadable;
2259
+ command = value.map(shellWord).join(" ");
2260
+ } else if (!argv) {
2261
+ return { command: value, fieldPath };
2262
+ } else {
2263
+ if (!isStringArray(argv.value)) return unreadable;
2264
+ if (!argv.value.length) return { command: value, fieldPath };
2265
+ command = `${value} ${argv.value.map(shellWord).join(" ")}`;
2266
+ source = `${fieldPath}+${argv.key}`;
2267
+ }
2268
+ if (command.length > MAX_ARGV_CHARACTERS)
2269
+ return { reason: "Shell command arguments exceed the inspectable size" };
2270
+ return { command, fieldPath: source };
2271
+ }
2202
2272
  function resolveShellInput(tool, args, inspection = {}) {
2203
2273
  const fields = shellFields(tool, inspection);
2204
2274
  if (!fields) return { kind: "not-shell" };
2205
2275
  const values = fields.map((fieldPath) => ({ fieldPath, value: getNestedValue(args, fieldPath) })).filter(({ value }) => value !== void 0 && value !== null && value !== "");
2206
- if (!values.length || values.some(({ value }) => typeof value !== "string")) {
2276
+ if (!values.length) {
2207
2277
  return { kind: "uninspectable", reason: "Shell command field is missing or is not a string" };
2208
2278
  }
2209
- if (new Set(values.map(({ value }) => value)).size !== 1) {
2279
+ const native = isBashTool(tool);
2280
+ const lines = values.map(({ fieldPath, value }) => commandLine(args, fieldPath, value, native));
2281
+ const failed = lines.find((line) => "reason" in line);
2282
+ if (failed) return { kind: "uninspectable", reason: failed.reason };
2283
+ const commands = lines.filter(
2284
+ (line) => "command" in line
2285
+ );
2286
+ if (new Set(commands.map(({ command }) => command)).size !== 1) {
2210
2287
  return { kind: "uninspectable", reason: "Multiple shell command fields disagree" };
2211
2288
  }
2212
- return { kind: "shell", command: values[0].value, fieldPath: values[0].fieldPath };
2289
+ return { kind: "shell", ...commands[0] };
2213
2290
  }
2214
2291
  function shellRuleArgs(tool, args, ruleTool, inspection = {}) {
2215
2292
  if (!ruleTool || !matchesPattern("bash", ruleTool) || matchesPattern(tool, ruleTool)) return args;
@@ -2950,20 +3027,25 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2950
3027
  }
2951
3028
  return best;
2952
3029
  }
2953
- function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ new Set() }) {
2954
- const words = [...text.matchAll(/[a-zA-Z]+/g)];
3030
+ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new Set() }) {
2955
3031
  let run = [];
3032
+ let runIndex = [];
2956
3033
  let previousEnd = 0;
2957
3034
  const rejected = /* @__PURE__ */ new Set();
2958
3035
  const inspect = () => {
3036
+ if (run.length < 12) return false;
3037
+ const ascending = new Array(run.length).fill(0);
3038
+ for (let i = 1; i < run.length; i++)
3039
+ ascending[i] = ascending[i - 1] + (runIndex[i] > runIndex[i - 1] ? 1 : 0);
2959
3040
  for (let start = 0; start + 12 <= run.length; start++) {
2960
3041
  for (const count of lengths) {
2961
3042
  if (start + count > run.length) continue;
3043
+ if (ascending[start + count - 1] - ascending[start] === count - 1) continue;
2962
3044
  const candidate = run.slice(start, start + count);
2963
3045
  const sentence2 = candidate.join(" ");
2964
3046
  if (rejected.has(sentence2)) continue;
2965
- if (++budget.candidates > 1024) {
2966
- budget.reasons.add("mnemonic-candidates");
3047
+ if (++budget.hashes > MAX_MNEMONIC_HASHES) {
3048
+ budget.reasons.add("mnemonic-budget");
2967
3049
  return false;
2968
3050
  }
2969
3051
  if (BIP39_PUBLIC_VECTORS.has(hash(sentence2).toString("hex"))) {
@@ -2981,13 +3063,18 @@ function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ n
2981
3063
  }
2982
3064
  return false;
2983
3065
  };
2984
- for (const word of words) {
2985
- if (!indices.has(word[0]) || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
3066
+ for (const word of text.matchAll(/[a-zA-Z]+/g)) {
3067
+ const index = indices.get(word[0]);
3068
+ if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
2986
3069
  if (inspect()) return true;
2987
- if (budget.reasons.has("mnemonic-candidates")) return false;
3070
+ if (budget.reasons.has("mnemonic-budget")) return false;
2988
3071
  run = [];
3072
+ runIndex = [];
3073
+ }
3074
+ if (index !== void 0) {
3075
+ run.push(word[0]);
3076
+ runIndex.push(index);
2989
3077
  }
2990
- if (indices.has(word[0])) run.push(word[0]);
2991
3078
  previousEnd = word.index + word[0].length;
2992
3079
  }
2993
3080
  return inspect();
@@ -3118,6 +3205,9 @@ function validateCask(raw) {
3118
3205
  const second = b64urlIndex(s[tsAt + 5]);
3119
3206
  return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
3120
3207
  }
3208
+ function isInvisibleChar(char) {
3209
+ return INVISIBLE_ONE.test(char);
3210
+ }
3121
3211
  function stripInvisible(text) {
3122
3212
  return text.replace(INVISIBLE_RE, "");
3123
3213
  }
@@ -3183,7 +3273,7 @@ function scanInjection(text, ctx = {}) {
3183
3273
  }
3184
3274
  }
3185
3275
  if (matched.length === 0) return null;
3186
- const untrusted = !!ctx.tool && UNTRUSTED_TOOLS.test(ctx.tool);
3276
+ const untrusted = ctx.untrusted ?? (!!ctx.tool && UNTRUSTED_TOOLS.test(ctx.tool));
3187
3277
  const score = matched.length + (untrusted ? 1 : 0);
3188
3278
  const confidence = score >= 3 ? "high" : score === 2 ? "medium" : "low";
3189
3279
  return { signals: untrusted ? [...matched, "untrusted-origin"] : matched, confidence };
@@ -3371,6 +3461,45 @@ function matchArgumentText(text, fieldPath, budget) {
3371
3461
  }
3372
3462
  return best;
3373
3463
  }
3464
+ function matchArgumentBatch(fragments, fieldPath, budget) {
3465
+ if (!fragments.length) return null;
3466
+ const text = fragments.join(BATCH_SEPARATOR);
3467
+ const lower2 = text.toLowerCase();
3468
+ let best = null;
3469
+ for (const pattern of DLP_PATTERNS) {
3470
+ if (pattern.keywords && !pattern.keywords.some((kw) => lower2.includes(kw.toLowerCase())))
3471
+ continue;
3472
+ const flags = pattern.regex.flags.replace(/[gmy]/g, "");
3473
+ if (!new RegExp(pattern.regex.source, flags + "m").test(text)) continue;
3474
+ for (const fragment of fragments) {
3475
+ if (firstAcceptedMatch(pattern, fragment) === null) continue;
3476
+ const severity = pattern.contextBoost && isAssignmentContext(fragment) ? "block" : pattern.severity;
3477
+ if (severity === "block")
3478
+ return {
3479
+ patternName: pattern.name,
3480
+ fieldPath,
3481
+ redactedSample: "[encoded secret]",
3482
+ severity
3483
+ };
3484
+ best ??= {
3485
+ patternName: pattern.name,
3486
+ fieldPath,
3487
+ redactedSample: "[encoded secret]",
3488
+ severity
3489
+ };
3490
+ if (!pattern.contextBoost) break;
3491
+ }
3492
+ }
3493
+ if (findMnemonic(text, budget)) {
3494
+ return {
3495
+ patternName: "BIP-39 Recovery Phrase",
3496
+ fieldPath,
3497
+ redactedSample: "[recovery phrase]",
3498
+ severity: "block"
3499
+ };
3500
+ }
3501
+ return best;
3502
+ }
3374
3503
  function scanArgs(args, depth = 0, fieldPath = "args") {
3375
3504
  if (depth > MAX_DEPTH2 || args === null || args === void 0) return null;
3376
3505
  if (Array.isArray(args)) {
@@ -4339,6 +4468,9 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
4339
4468
  return null;
4340
4469
  }
4341
4470
  }
4471
+ function newSecretViewBudget() {
4472
+ return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
4473
+ }
4342
4474
  function textBytes(buf) {
4343
4475
  if (buf.length < 16) return null;
4344
4476
  const text = buf.toString("utf8");
@@ -4357,13 +4489,41 @@ function percentOnce(text) {
4357
4489
  }
4358
4490
  });
4359
4491
  }
4492
+ function* wrappedBase64(text) {
4493
+ if (!text.includes("\n")) return;
4494
+ const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
4495
+ let run = [];
4496
+ let width = 0;
4497
+ const flush = function* () {
4498
+ if (run.length >= 2) yield run.join("");
4499
+ run = [];
4500
+ width = 0;
4501
+ };
4502
+ for (const raw of text.split("\n")) {
4503
+ const line = raw.trim();
4504
+ const full = line.length >= 16 && word.test(line) && !line.includes("=");
4505
+ if (full && (!run.length || line.length === width)) {
4506
+ run.push(line);
4507
+ width = line.length;
4508
+ continue;
4509
+ }
4510
+ if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
4511
+ run.push(line);
4512
+ yield* flush();
4513
+ continue;
4514
+ }
4515
+ yield* flush();
4516
+ if (full) {
4517
+ run.push(line);
4518
+ width = line.length;
4519
+ }
4520
+ }
4521
+ yield* flush();
4522
+ }
4360
4523
  function* encodedRegions(text, budget) {
4361
4524
  yield [text, false];
4525
+ for (const block of wrappedBase64(text)) yield [block, false];
4362
4526
  for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
4363
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4364
- budget.reasons.add("decode-candidates");
4365
- return;
4366
- }
4367
4527
  try {
4368
4528
  new URL(match[0]);
4369
4529
  } catch {
@@ -4383,6 +4543,8 @@ function* encodedRegions(text, budget) {
4383
4543
  ];
4384
4544
  for (const region of regions) if (region) yield [region, false];
4385
4545
  const labels = host2.split(".");
4546
+ if (host2.length > SECRET_VIEW_LIMITS.maxHostLength || labels.length > SECRET_VIEW_LIMITS.maxLabels || !labels.every((l) => l.length >= 1 && l.length <= SECRET_VIEW_LIMITS.maxLabelLength))
4547
+ continue;
4386
4548
  for (let start = 0; start < labels.length; start++) {
4387
4549
  let joined = labels[start];
4388
4550
  for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
@@ -4390,11 +4552,13 @@ function* encodedRegions(text, budget) {
4390
4552
  if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
4391
4553
  break;
4392
4554
  joined += labels[end];
4393
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4394
- budget.reasons.add("decode-candidates");
4555
+ if (joined.length < 16) continue;
4556
+ budget.joinedLabelBytes += joined.length;
4557
+ if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
4558
+ budget.reasons.add("decode-labels");
4395
4559
  return;
4396
4560
  }
4397
- if (joined.length >= 16 && joined.length <= 253) yield [joined, true];
4561
+ yield [joined, true];
4398
4562
  }
4399
4563
  }
4400
4564
  }
@@ -4431,10 +4595,6 @@ function* decodedCandidates(text, budget) {
4431
4595
  if (joined) yield ["dns-labels", region];
4432
4596
  for (const [kind, re] of families) {
4433
4597
  for (const match of region.matchAll(re)) {
4434
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4435
- budget.reasons.add("decode-candidates");
4436
- return;
4437
- }
4438
4598
  const raw = match[0];
4439
4599
  let buf;
4440
4600
  if (kind === "base64") {
@@ -4457,42 +4617,76 @@ function* decodedCandidates(text, budget) {
4457
4617
  }
4458
4618
  }
4459
4619
  }
4460
- function* secretViews(text, budget) {
4620
+ function secretViews(text, budget) {
4461
4621
  const seen = /* @__PURE__ */ new Set([text]);
4462
- const queue = [{ text, transforms: [] }];
4463
- for (let i = 0; i < queue.length; i++) {
4464
- const view = queue[i];
4465
- yield view;
4466
- for (const [kind, decoded] of decodedCandidates(view.text, budget)) {
4467
- if (seen.has(decoded)) continue;
4468
- if (view.transforms.length >= SECRET_VIEW_LIMITS.maxDepth) {
4469
- budget.reasons.add("decode-depth");
4470
- continue;
4471
- }
4472
- const bytes = Buffer.byteLength(decoded);
4473
- if (queue.length >= SECRET_VIEW_LIMITS.maxViewsPerLeaf || budget.views >= SECRET_VIEW_LIMITS.maxViews || budget.bytes + bytes > SECRET_VIEW_LIMITS.maxDerivedBytes) {
4474
- budget.reasons.add("decode-budget");
4475
- return;
4476
- }
4477
- seen.add(decoded);
4478
- budget.views++;
4479
- budget.bytes += bytes;
4480
- queue.push({ text: decoded, transforms: [...view.transforms, kind] });
4622
+ const out = [];
4623
+ let frontier = [{ text, transforms: [] }];
4624
+ for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
4625
+ const next = [];
4626
+ for (const parent of frontier) {
4627
+ let perLeaf = 0;
4628
+ for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
4629
+ if (seen.has(decoded)) continue;
4630
+ seen.add(decoded);
4631
+ const normalizing = kind === "invisibles" || kind === "percent";
4632
+ if (depth >= 2 && !normalizing) {
4633
+ budget.nestedBytes += Buffer.byteLength(decoded);
4634
+ if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
4635
+ budget.reasons.add("decode-nested");
4636
+ return out;
4637
+ }
4638
+ }
4639
+ const view = { text: decoded, transforms: [...parent.transforms, kind] };
4640
+ out.push(view);
4641
+ next.push(view);
4642
+ }
4643
+ }
4644
+ frontier = next;
4645
+ if (!frontier.length) return out;
4646
+ }
4647
+ for (const view of frontier) {
4648
+ if (!decodedCandidates(view.text, budget).next().done) {
4649
+ budget.reasons.add("decode-nested");
4650
+ break;
4481
4651
  }
4482
- if (budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) return;
4483
4652
  }
4653
+ return out;
4654
+ }
4655
+ function batches(views) {
4656
+ const out = [];
4657
+ let current = [];
4658
+ let size = 0;
4659
+ for (const { text } of views) {
4660
+ if (current.length && size + text.length > SECRET_VIEW_LIMITS.batchCharacters) {
4661
+ out.push(current);
4662
+ current = [];
4663
+ size = 0;
4664
+ }
4665
+ current.push(text);
4666
+ size += text.length;
4667
+ }
4668
+ if (current.length) out.push(current);
4669
+ return out;
4484
4670
  }
4485
4671
  function inspectArgumentSecrets(args) {
4486
- const budget = { views: 0, bytes: 0, candidates: 0, reasons: /* @__PURE__ */ new Set() };
4672
+ const budget = newSecretViewBudget();
4487
4673
  const ancestors = /* @__PURE__ */ new Set();
4488
4674
  let nodes = 0;
4489
4675
  let sourceBytes = 0;
4490
4676
  let tailBytes = 0;
4491
4677
  let best = null;
4678
+ const blocked = () => best?.severity === "block";
4492
4679
  const accept = (match) => {
4493
4680
  if (match && (!best || best.severity !== "block" && match.severity === "block")) best = match;
4494
4681
  };
4682
+ const decodeSegment = (segment, fieldPath) => {
4683
+ for (const batch of batches(secretViews(segment, budget))) {
4684
+ accept(matchArgumentBatch(batch, fieldPath, budget));
4685
+ if (blocked()) return;
4686
+ }
4687
+ };
4495
4688
  const visit2 = (value, depth, fieldPath) => {
4689
+ if (blocked()) return;
4496
4690
  if (++nodes > ARGUMENT_SCAN_LIMITS.maxNodes) {
4497
4691
  budget.reasons.add("argument-nodes");
4498
4692
  return;
@@ -4534,14 +4728,11 @@ function inspectArgumentSecrets(args) {
4534
4728
  }
4535
4729
  sourceBytes += Math.min(bytes, Math.max(0, remaining));
4536
4730
  if (!text) return;
4537
- for (const view of secretViews(text, budget)) {
4538
- const hit = matchArgumentText(view.text, fieldPath, budget);
4539
- if (hit)
4540
- accept(view.transforms.length ? { ...hit, redactedSample: "[encoded secret]" } : hit);
4541
- }
4731
+ accept(matchArgumentText(text, fieldPath, budget));
4732
+ let scannedEnd = text.length;
4542
4733
  if (!truncated && value.length > window) {
4543
- budget.reasons.add("decode-window");
4544
4734
  for (let start = window - overlap; start < value.length; start += window - overlap) {
4735
+ if (blocked()) return;
4545
4736
  const chunk2 = value.slice(start, start + window);
4546
4737
  const chunkBytes = Buffer.byteLength(chunk2);
4547
4738
  if (tailBytes + chunkBytes > ARGUMENT_SCAN_LIMITS.maxTailBytes) {
@@ -4550,9 +4741,16 @@ function inspectArgumentSecrets(args) {
4550
4741
  }
4551
4742
  tailBytes += chunkBytes;
4552
4743
  accept(matchArgumentText(chunk2, fieldPath, budget));
4744
+ scannedEnd = start + chunk2.length;
4553
4745
  if (start + window >= value.length) break;
4554
4746
  }
4555
4747
  }
4748
+ for (let start = 0; start < scannedEnd && !blocked(); ) {
4749
+ let end = Math.min(start + window, scannedEnd);
4750
+ while (end < scannedEnd && runChar(value, end - 1) && runChar(value, end)) end++;
4751
+ decodeSegment(value.slice(start, end), fieldPath);
4752
+ start = end;
4753
+ }
4556
4754
  const trimmed = text.trim();
4557
4755
  if (value.length < ARGUMENT_SCAN_LIMITS.maxJsonCharacters && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
4558
4756
  try {
@@ -4571,7 +4769,7 @@ function inspectArgumentSecrets(args) {
4571
4769
  function scanArgumentSecrets(args) {
4572
4770
  const result = inspectArgumentSecrets(args);
4573
4771
  if (result.match) return result.match;
4574
- return result.reasons.some((reason) => reason.startsWith("argument-")) ? {
4772
+ return result.coverage === "incomplete" ? {
4575
4773
  patternName: "Argument inspection incomplete",
4576
4774
  fieldPath: "args",
4577
4775
  redactedSample: "[uninspected content]",
@@ -4599,18 +4797,13 @@ function tokenize2(toolName) {
4599
4797
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
4600
4798
  }
4601
4799
  function extractShellCommand(toolName, args, toolInspection) {
4602
- const patterns = Object.keys(toolInspection);
4603
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4604
- if (!matchingPattern) return null;
4605
- const fieldPath = toolInspection[matchingPattern];
4800
+ const fieldPath = inspectionEntry(toolName, toolInspection);
4801
+ if (fieldPath === void 0) return null;
4606
4802
  const value = getNestedValue(args, typeof fieldPath === "string" ? fieldPath : fieldPath.field);
4607
4803
  return typeof value === "string" ? value : null;
4608
4804
  }
4609
4805
  function isSqlTool(toolName, toolInspection) {
4610
- const patterns = Object.keys(toolInspection);
4611
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4612
- if (!matchingPattern) return false;
4613
- const fieldName = toolInspection[matchingPattern];
4806
+ const fieldName = inspectionEntry(toolName, toolInspection);
4614
4807
  return fieldName === "sql" || fieldName === "query";
4615
4808
  }
4616
4809
  function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
@@ -4677,9 +4870,10 @@ function egressPolicyVerdict(eg) {
4677
4870
  }
4678
4871
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
4679
4872
  let protectedVerdict = null;
4873
+ const layers = inspectionLayers(config.policy);
4680
4874
  if (context.agent !== "Terminal") {
4681
4875
  try {
4682
- const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, config.policy.toolInspection);
4876
+ const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, layers);
4683
4877
  protectedVerdict = evaluateProtectedFileAccess(accesses, context, catalogSettings(config));
4684
4878
  } catch {
4685
4879
  protectedVerdict = incompleteFileVerdict();
@@ -4714,7 +4908,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4714
4908
  hooks
4715
4909
  );
4716
4910
  if (result.decision === "block") return result;
4717
- if (context.agent !== "Terminal" && resolveShellInput(toolName, args, config.policy.toolInspection).kind === "uninspectable") {
4911
+ if (context.agent !== "Terminal" && resolveShellInput(toolName, args, layers).kind === "uninspectable") {
4718
4912
  return {
4719
4913
  decision: "review",
4720
4914
  ruleName: "shell-inspection-incomplete",
@@ -4730,13 +4924,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4730
4924
  const { agent, cwd, activeEnvironment } = context;
4731
4925
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
4732
4926
  const settingsView = catalogSettings(config);
4927
+ const layers = inspectionLayers(config.policy);
4733
4928
  const cv = (id) => checkValue(settingsView, id);
4734
4929
  let loggedFinding;
4735
4930
  const noteLogged = (v) => {
4736
4931
  if (v?.logged && !loggedFinding) loggedFinding = v;
4737
4932
  };
4738
4933
  const allowed = () => loggedFinding ?? { decision: "allow" };
4739
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
4934
+ const shellInput = resolveShellInput(toolName, args, layers);
4740
4935
  const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
4741
4936
  {
4742
4937
  const dest = ssrfDestinationFloor(toolName, args, {
@@ -4841,13 +5036,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4841
5036
  return { id, value: cv(id) };
4842
5037
  };
4843
5038
  const matches = config.policy.smartRules.filter((rule) => {
4844
- if (!toolMatchesRule(toolName, rule.tool, config.policy.toolInspection)) return false;
5039
+ if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
4845
5040
  if (astSuppressed(rule)) return false;
4846
5041
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
4847
- if (!evaluateSmartConditions(
4848
- shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
4849
- rule
4850
- ))
5042
+ if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
4851
5043
  return false;
4852
5044
  const check = ruleCheck(rule);
4853
5045
  if (check?.value === "off") return false;
@@ -4894,7 +5086,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4894
5086
  let allTokens = [];
4895
5087
  let pathTokens = [];
4896
5088
  if (pendingToolEgress) return pendingToolEgress;
4897
- const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, config.policy.toolInspection) : null;
5089
+ const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, layers) : null;
4898
5090
  if (shellCommand) {
4899
5091
  const analyzed = analyzeShellCommand(shellCommand);
4900
5092
  allTokens = analyzed.allTokens;
@@ -4971,7 +5163,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4971
5163
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
4972
5164
  const rawTokens = shellCommand.trim().split(/\s+/);
4973
5165
  const sshHosts = extractAllSshHosts(rawTokens.slice(1));
4974
- allTokens.push(...sshHosts);
5166
+ allTokens = allTokens.concat(sshHosts);
4975
5167
  }
4976
5168
  if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
4977
5169
  const prov = checkProvenance2(firstToken, cwd);
@@ -5000,7 +5192,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5000
5192
  };
5001
5193
  }
5002
5194
  }
5003
- if (isSqlTool(toolName, config.policy.toolInspection)) {
5195
+ if (isSqlTool(toolName, layers)) {
5004
5196
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
5005
5197
  }
5006
5198
  } else {
@@ -5008,7 +5200,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5008
5200
  if (args && typeof args === "object") {
5009
5201
  const flattenedArgs = JSON.stringify(args).toLowerCase();
5010
5202
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
5011
- allTokens.push(...extraTokens);
5203
+ allTokens = allTokens.concat(extraTokens);
5012
5204
  }
5013
5205
  }
5014
5206
  const isManual = agent === "Terminal";
@@ -6303,7 +6495,7 @@ function* stringValues(obj, depth = 0) {
6303
6495
  }
6304
6496
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
6305
6497
  }
6306
- var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, CHECK_GROUPS, 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, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, 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, 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, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, 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, syntax2, MAX_ALTERNATIVES, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, 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, 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, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, 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, CONTROL_CHAR_RE, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, 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, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
6498
+ var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, CHECK_GROUPS, 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, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, 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, 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, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, 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_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, 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, 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, SECRET_VIEW_LIMITS, 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, 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, CONTROL_CHAR_RE, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, 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, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
6307
6499
  var init_dist = __esm({
6308
6500
  "packages/policy-engine/dist/index.mjs"() {
6309
6501
  "use strict";
@@ -8676,7 +8868,23 @@ var init_dist = __esm({
8676
8868
  "shell",
8677
8869
  "exec_command"
8678
8870
  ]);
8679
- MCP_EXECUTORS = /* @__PURE__ */ new Set([...BASH_TOOL_NAMES, "exec", "run_command", "terminal"]);
8871
+ MCP_EXECUTORS = /* @__PURE__ */ new Set([
8872
+ ...BASH_TOOL_NAMES,
8873
+ "exec",
8874
+ "run_command",
8875
+ "terminal",
8876
+ "execute_command",
8877
+ "execute_shell",
8878
+ "execute_shell_command",
8879
+ "shell_execute",
8880
+ "run_shell",
8881
+ "run_terminal_command",
8882
+ "start_process"
8883
+ ]);
8884
+ BUILTIN_FIELDS = ["command", "cmd", "script"];
8885
+ ARGV_FIELDS = ["args", "argv"];
8886
+ BARE_WORD = /^[A-Za-z0-9_./+:@%,-]+$/;
8887
+ MAX_ARGV_CHARACTERS = 262144;
8680
8888
  ({ syntax: syntax2 } = mvdanSh2);
8681
8889
  MAX_ALTERNATIVES = 64;
8682
8890
  MAX_DEPTH = 64;
@@ -10802,6 +11010,7 @@ zoo`.split("\n");
10802
11010
  indices = new Map(BIP39_ENGLISH.map((word, i) => [word, i]));
10803
11011
  lengths = [24, 21, 18, 15, 12];
10804
11012
  hash = (value) => createHash2("sha256").update(value).digest();
11013
+ MAX_MNEMONIC_HASHES = 25e3;
10805
11014
  IBAN_LENGTH = {
10806
11015
  AD: 24,
10807
11016
  AE: 23,
@@ -10909,6 +11118,7 @@ zoo`.split("\n");
10909
11118
  CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
10910
11119
  CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
10911
11120
  INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
11121
+ INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
10912
11122
  HOMOGLYPHS = {
10913
11123
  // Cyrillic lowercase
10914
11124
  \u0430: "a",
@@ -11615,6 +11825,7 @@ zoo`.split("\n");
11615
11825
  /** Max nesting depth walked; anything deeper is NOT scanned. */
11616
11826
  maxDepth: MAX_DEPTH2
11617
11827
  };
11828
+ BATCH_SEPARATOR = "\n";
11618
11829
  ({ syntax: syntax3 } = mvdanSh3);
11619
11830
  NPM_INSTALL_VERBS = /* @__PURE__ */ new Set([
11620
11831
  "install",
@@ -12084,16 +12295,24 @@ zoo`.split("\n");
12084
12295
  ["browser_batch", ["actions[].input.url"]]
12085
12296
  ]);
12086
12297
  SECRET_VIEW_LIMITS = {
12298
+ /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
12087
12299
  maxDepth: 4,
12088
- maxViewsPerLeaf: 64,
12089
- maxViews: 256,
12090
- maxDerivedBytes: 1048576,
12091
- maxCandidates: 4096
12300
+ /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
12301
+ maxNestedViewsPerLeaf: 64,
12302
+ maxNestedBytes: 1048576,
12303
+ /** Joined hostname-label text per call; past it the call is reviewed. */
12304
+ maxJoinedLabelBytes: 1048576,
12305
+ /** Decoded fragments are matched in batches of about this many characters. */
12306
+ batchCharacters: 65536,
12307
+ /** Only a valid DNS name has its labels joined. */
12308
+ maxHostLength: 253,
12309
+ maxLabelLength: 63,
12310
+ maxLabels: 127
12092
12311
  };
12093
12312
  ARGUMENT_SCAN_LIMITS = {
12094
12313
  maxDepth: 12,
12095
12314
  maxNodes: 16384,
12096
- /** Raw text is matched in windows of this size; decoding views use the first window. */
12315
+ /** Text is matched and decoded in windows of this size. */
12097
12316
  maxStringCharacters: 1e5,
12098
12317
  /** Windows overlap by this much so a token on a window edge is still seen whole. */
12099
12318
  windowOverlap: 4096,
@@ -12103,6 +12322,11 @@ zoo`.split("\n");
12103
12322
  maxTailBytes: 4194304,
12104
12323
  maxJsonCharacters: 1e4
12105
12324
  };
12325
+ RUN_CHAR = /[A-Za-z0-9+/=_%\\.-]/;
12326
+ runChar = (value, index) => {
12327
+ const char = value[index] ?? "";
12328
+ return RUN_CHAR.test(char) || char !== "" && isInvisibleChar(char);
12329
+ };
12106
12330
  VERDICT_RANK = {
12107
12331
  allow: 0,
12108
12332
  review: 1,
@@ -12362,7 +12586,7 @@ zoo`.split("\n");
12362
12586
  { view: "separators-stripped", decoder: "separators", stripped: true }
12363
12587
  ];
12364
12588
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
12365
- CANONICAL_EXTRACTOR_VERSION = "canonical-v25";
12589
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v26";
12366
12590
  DEDUPE_PREVIEW_LEN = 120;
12367
12591
  ENGINE_VERSION = "1.4.0";
12368
12592
  }
@@ -12492,6 +12716,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
12492
12716
  taintFromEid: meta.taintFromEid,
12493
12717
  ...meta.taintSource && { taintSource: meta.taintSource }
12494
12718
  } : {};
12719
+ const exposureFields = {
12720
+ ...meta?.exposure ? { exposure: meta.exposure } : {},
12721
+ ...meta?.exposureFromEid ? { exposureFromEid: meta.exposureFromEid } : {}
12722
+ };
12495
12723
  const eid = generateEventId();
12496
12724
  appendToLog(LOCAL_AUDIT_LOG, {
12497
12725
  // eid first: the outbox shipper dedups on it, and a fixed leading field
@@ -12514,6 +12742,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
12514
12742
  ...loopCountField,
12515
12743
  ...transcriptPathField,
12516
12744
  ...taintFields,
12745
+ ...exposureFields,
12517
12746
  ...testRun,
12518
12747
  agent: meta?.agent,
12519
12748
  mcpServer: meta?.mcpServer,
@@ -12542,6 +12771,82 @@ var init_audit = __esm({
12542
12771
  }
12543
12772
  });
12544
12773
 
12774
+ // src/auth/trusted-recipients.ts
12775
+ import fs2 from "fs";
12776
+ import path2 from "path";
12777
+ import os2 from "os";
12778
+ function getTrustedRecipientsPath() {
12779
+ return path2.join(os2.homedir(), ".node9", "trusted-recipients.json");
12780
+ }
12781
+ function readTrustedRecipients() {
12782
+ try {
12783
+ const raw = fs2.readFileSync(getTrustedRecipientsPath(), "utf8");
12784
+ const parsed = JSON.parse(raw);
12785
+ return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
12786
+ } catch {
12787
+ return [];
12788
+ }
12789
+ }
12790
+ function writeTrustedRecipients(entries) {
12791
+ const filePath = getTrustedRecipientsPath();
12792
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
12793
+ const tmp = filePath + ".node9-tmp";
12794
+ fs2.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
12795
+ fs2.renameSync(tmp, filePath);
12796
+ }
12797
+ function normalizeRecipient(raw) {
12798
+ const v = raw.trim();
12799
+ if (!v) return null;
12800
+ if (/^(email|host|iban):/i.test(v)) {
12801
+ const i = v.indexOf(":");
12802
+ const kind = v.slice(0, i).toLowerCase();
12803
+ const inner = normalizeRecipient(v.slice(i + 1));
12804
+ return inner && inner.startsWith(`${kind}:`) ? inner : null;
12805
+ }
12806
+ const found = extractArgDestinations({ to: v });
12807
+ return found.length === 1 ? found[0] : null;
12808
+ }
12809
+ function addTrustedRecipient(raw, addedBy = "user") {
12810
+ const value = normalizeRecipient(raw);
12811
+ if (!value) return null;
12812
+ const entries = readTrustedRecipients();
12813
+ if (!entries.some((e) => e.value === value)) {
12814
+ entries.push({ value, addedAt: Date.now(), addedBy });
12815
+ writeTrustedRecipients(entries);
12816
+ }
12817
+ return value;
12818
+ }
12819
+ function removeTrustedRecipient(raw) {
12820
+ const value = normalizeRecipient(raw) ?? raw.trim();
12821
+ const entries = readTrustedRecipients();
12822
+ const next = entries.filter((e) => e.value !== value);
12823
+ if (next.length === entries.length) return false;
12824
+ writeTrustedRecipients(next);
12825
+ return true;
12826
+ }
12827
+ function mtimeOf() {
12828
+ try {
12829
+ return fs2.statSync(getTrustedRecipientsPath()).mtimeMs;
12830
+ } catch {
12831
+ return 0;
12832
+ }
12833
+ }
12834
+ function isTrustedRecipient(value) {
12835
+ const mtime = mtimeOf();
12836
+ if (!cache || cache.mtime !== mtime) {
12837
+ cache = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
12838
+ }
12839
+ return cache.values.has(value);
12840
+ }
12841
+ var cache;
12842
+ var init_trusted_recipients = __esm({
12843
+ "src/auth/trusted-recipients.ts"() {
12844
+ "use strict";
12845
+ init_dist();
12846
+ cache = null;
12847
+ }
12848
+ });
12849
+
12545
12850
  // src/config-schema.ts
12546
12851
  import { z } from "zod";
12547
12852
  function formatIssues(issues) {
@@ -12815,15 +13120,15 @@ var init_config_schema = __esm({
12815
13120
  });
12816
13121
 
12817
13122
  // src/shields.ts
12818
- import fs2 from "fs";
12819
- import path2 from "path";
12820
- import os2 from "os";
13123
+ import fs3 from "fs";
13124
+ import path3 from "path";
13125
+ import os3 from "os";
12821
13126
  import crypto3 from "crypto";
12822
13127
  function loadUserShields() {
12823
13128
  const result = {};
12824
13129
  let entries;
12825
13130
  try {
12826
- entries = fs2.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
13131
+ entries = fs3.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
12827
13132
  } catch (err2) {
12828
13133
  if (err2.code !== "ENOENT") {
12829
13134
  process.stderr.write(
@@ -12834,9 +13139,9 @@ function loadUserShields() {
12834
13139
  return result;
12835
13140
  }
12836
13141
  for (const file2 of entries) {
12837
- const filePath = path2.join(USER_SHIELDS_DIR, file2);
13142
+ const filePath = path3.join(USER_SHIELDS_DIR, file2);
12838
13143
  try {
12839
- const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
13144
+ const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
12840
13145
  const v = validateShieldDefinition(raw);
12841
13146
  if ("error" in v) {
12842
13147
  process.stderr.write(`[node9] ${v.error}: ${filePath}
@@ -12871,7 +13176,7 @@ function listShields() {
12871
13176
  }
12872
13177
  function readShieldsFile() {
12873
13178
  try {
12874
- const raw = fs2.readFileSync(SHIELDS_STATE_FILE, "utf-8");
13179
+ const raw = fs3.readFileSync(SHIELDS_STATE_FILE, "utf-8");
12875
13180
  if (!raw.trim()) return { active: [] };
12876
13181
  const parsed = JSON.parse(raw);
12877
13182
  const active = Array.isArray(parsed.active) ? parsed.active.filter(
@@ -12892,12 +13197,12 @@ function readShieldsFile() {
12892
13197
  }
12893
13198
  }
12894
13199
  function writeShieldsFile(data) {
12895
- fs2.mkdirSync(path2.dirname(SHIELDS_STATE_FILE), { recursive: true });
13200
+ fs3.mkdirSync(path3.dirname(SHIELDS_STATE_FILE), { recursive: true });
12896
13201
  const tmp = `${SHIELDS_STATE_FILE}.${crypto3.randomBytes(6).toString("hex")}.tmp`;
12897
13202
  const toWrite = { active: data.active };
12898
13203
  if (data.overrides && Object.keys(data.overrides).length > 0) toWrite.overrides = data.overrides;
12899
- fs2.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
12900
- fs2.renameSync(tmp, SHIELDS_STATE_FILE);
13204
+ fs3.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
13205
+ fs3.renameSync(tmp, SHIELDS_STATE_FILE);
12901
13206
  }
12902
13207
  function readActiveShields() {
12903
13208
  return readShieldsFile().active;
@@ -12981,11 +13286,11 @@ function installShield(name, shieldJson) {
12981
13286
  if (v.ok.name !== name) {
12982
13287
  throw new Error(`Shield name mismatch: file declares '${v.ok.name}' but expected '${name}'`);
12983
13288
  }
12984
- fs2.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
12985
- const filePath = path2.join(USER_SHIELDS_DIR, `${name}.json`);
13289
+ fs3.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
13290
+ const filePath = path3.join(USER_SHIELDS_DIR, `${name}.json`);
12986
13291
  const tmp = `${filePath}.${crypto3.randomBytes(6).toString("hex")}.tmp`;
12987
- fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
12988
- fs2.renameSync(tmp, filePath);
13292
+ fs3.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
13293
+ fs3.renameSync(tmp, filePath);
12989
13294
  }
12990
13295
  var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS, USER_SHIELDS_DIR_PATH;
12991
13296
  var init_shields = __esm({
@@ -12993,9 +13298,9 @@ var init_shields = __esm({
12993
13298
  "use strict";
12994
13299
  init_dist();
12995
13300
  init_dist();
12996
- USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
13301
+ USER_SHIELDS_DIR = path3.join(os3.homedir(), ".node9", "shields");
12997
13302
  SHIELDS = buildSHIELDS();
12998
- SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
13303
+ SHIELDS_STATE_FILE = path3.join(os3.homedir(), ".node9", "shields.json");
12999
13304
  RULE_KEY_MIGRATIONS = [
13000
13305
  // 2026-05-21 — project-jail .env reads promoted review → block. Renamed
13001
13306
  // so the key honestly describes the verdict. Credential-file rules
@@ -13229,15 +13534,15 @@ var init_build = __esm({
13229
13534
  });
13230
13535
 
13231
13536
  // src/auth/trusted-hosts.ts
13232
- import fs3 from "fs";
13233
- import path3 from "path";
13234
- import os3 from "os";
13537
+ import fs4 from "fs";
13538
+ import path4 from "path";
13539
+ import os4 from "os";
13235
13540
  function getTrustedHostsPath() {
13236
- return path3.join(os3.homedir(), ".node9", "trusted-hosts.json");
13541
+ return path4.join(os4.homedir(), ".node9", "trusted-hosts.json");
13237
13542
  }
13238
13543
  function readTrustedHosts() {
13239
13544
  try {
13240
- const raw = fs3.readFileSync(getTrustedHostsPath(), "utf8");
13545
+ const raw = fs4.readFileSync(getTrustedHostsPath(), "utf8");
13241
13546
  const parsed = JSON.parse(raw);
13242
13547
  return Array.isArray(parsed.hosts) ? parsed.hosts : [];
13243
13548
  } catch {
@@ -13246,7 +13551,7 @@ function readTrustedHosts() {
13246
13551
  }
13247
13552
  function getFileMtime() {
13248
13553
  try {
13249
- return fs3.statSync(getTrustedHostsPath()).mtimeMs;
13554
+ return fs4.statSync(getTrustedHostsPath()).mtimeMs;
13250
13555
  } catch {
13251
13556
  return 0;
13252
13557
  }
@@ -13263,10 +13568,10 @@ function getCachedHosts() {
13263
13568
  }
13264
13569
  function writeTrustedHosts(hosts) {
13265
13570
  const filePath = getTrustedHostsPath();
13266
- fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
13571
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
13267
13572
  const tmp = filePath + ".node9-tmp";
13268
- fs3.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
13269
- fs3.renameSync(tmp, filePath);
13573
+ fs4.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
13574
+ fs4.renameSync(tmp, filePath);
13270
13575
  _cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
13271
13576
  }
13272
13577
  function addTrustedHost(host2) {
@@ -13572,9 +13877,9 @@ var init_api_url = __esm({
13572
13877
  });
13573
13878
 
13574
13879
  // src/config/index.ts
13575
- import fs4 from "fs";
13576
- import path4 from "path";
13577
- import os4 from "os";
13880
+ import fs5 from "fs";
13881
+ import path5 from "path";
13882
+ import os5 from "os";
13578
13883
  function sanitizeSsrfAllow(entries, source) {
13579
13884
  const kept = [];
13580
13885
  for (const entry of entries) {
@@ -13596,9 +13901,9 @@ function _resetConfigCache() {
13596
13901
  }
13597
13902
  function getGlobalSettings() {
13598
13903
  try {
13599
- const globalConfigPath2 = path4.join(os4.homedir(), ".node9", "config.json");
13600
- if (fs4.existsSync(globalConfigPath2)) {
13601
- const raw = JSON.parse(fs4.readFileSync(globalConfigPath2, "utf-8"));
13904
+ const globalConfigPath2 = path5.join(os5.homedir(), ".node9", "config.json");
13905
+ if (fs5.existsSync(globalConfigPath2)) {
13906
+ const raw = JSON.parse(fs5.readFileSync(globalConfigPath2, "utf-8"));
13602
13907
  const parsed = isV2File(raw) ? v2ToLegacy(raw).legacy : raw;
13603
13908
  const settings = parsed.settings || {};
13604
13909
  return {
@@ -13627,8 +13932,8 @@ function noteRejectedApiUrl(raw) {
13627
13932
  if (rejectedApiUrlsSeen.has(key)) return;
13628
13933
  rejectedApiUrlsSeen.add(key);
13629
13934
  try {
13630
- fs4.appendFileSync(
13631
- path4.join(os4.homedir(), ".node9", "hook-debug.log"),
13935
+ fs5.appendFileSync(
13936
+ path5.join(os5.homedir(), ".node9", "hook-debug.log"),
13632
13937
  `[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
13633
13938
  `
13634
13939
  );
@@ -13645,9 +13950,9 @@ function getCredentials() {
13645
13950
  };
13646
13951
  }
13647
13952
  try {
13648
- const credPath = path4.join(os4.homedir(), ".node9", "credentials.json");
13649
- if (fs4.existsSync(credPath)) {
13650
- const creds = JSON.parse(fs4.readFileSync(credPath, "utf-8"));
13953
+ const credPath = path5.join(os5.homedir(), ".node9", "credentials.json");
13954
+ if (fs5.existsSync(credPath)) {
13955
+ const creds = JSON.parse(fs5.readFileSync(credPath, "utf-8"));
13651
13956
  const profileName = process.env.NODE9_PROFILE || "default";
13652
13957
  const profile = creds[profileName];
13653
13958
  if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
@@ -13679,7 +13984,7 @@ function readRulesCacheResilient(cacheFile) {
13679
13984
  for (let attempt = 0; attempt < 3; attempt++) {
13680
13985
  let content;
13681
13986
  try {
13682
- content = fs4.readFileSync(cacheFile, "utf-8");
13987
+ content = fs5.readFileSync(cacheFile, "utf-8");
13683
13988
  existed = true;
13684
13989
  } catch (err2) {
13685
13990
  if (err2.code === "ENOENT") return {};
@@ -13694,10 +13999,10 @@ function readRulesCacheResilient(cacheFile) {
13694
13999
  }
13695
14000
  }
13696
14001
  if (existed || sawReadError) {
13697
- const backup = path4.join(path4.dirname(cacheFile), "rules-cache.last-good.json");
14002
+ const backup = path5.join(path5.dirname(cacheFile), "rules-cache.last-good.json");
13698
14003
  if (backup !== cacheFile) {
13699
14004
  try {
13700
- const raw = JSON.parse(fs4.readFileSync(backup, "utf-8"));
14005
+ const raw = JSON.parse(fs5.readFileSync(backup, "utf-8"));
13701
14006
  logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
13702
14007
  lastParsedRulesCache = raw;
13703
14008
  return raw;
@@ -13717,8 +14022,8 @@ function logCacheReadIssue(cacheFile, kind) {
13717
14022
  if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
13718
14023
  cacheReadLastLoggedAt = now;
13719
14024
  try {
13720
- fs4.appendFileSync(
13721
- path4.join(os4.homedir(), ".node9", "hook-debug.log"),
14025
+ fs5.appendFileSync(
14026
+ path5.join(os5.homedir(), ".node9", "hook-debug.log"),
13722
14027
  `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
13723
14028
  `
13724
14029
  );
@@ -13730,8 +14035,8 @@ function catalogSettingsFrom(settings, policy) {
13730
14035
  }
13731
14036
  function getConfig(cwd) {
13732
14037
  if (!cwd && cachedConfig) return cachedConfig;
13733
- const globalPath = path4.join(os4.homedir(), ".node9", "config.json");
13734
- const projectPath = path4.join(cwd ?? process.cwd(), "node9.config.json");
14038
+ const globalPath = path5.join(os5.homedir(), ".node9", "config.json");
14039
+ const projectPath = path5.join(cwd ?? process.cwd(), "node9.config.json");
13735
14040
  const globalConfig = tryLoadConfig(globalPath);
13736
14041
  const projectConfig = tryLoadConfig(projectPath, { project: true });
13737
14042
  const mergedSettings = {
@@ -13744,6 +14049,7 @@ function getConfig(cwd) {
13744
14049
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
13745
14050
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
13746
14051
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
14052
+ projectToolInspection: {},
13747
14053
  // Shipped rules carry `builtin`: the catalog's per-check value governs
13748
14054
  // them and only them (a user rule that reuses a name keeps its verdict).
13749
14055
  smartRules: DEFAULT_CONFIG.policy.smartRules.map((r) => ({ ...r, builtin: true })),
@@ -13770,7 +14076,8 @@ function getConfig(cwd) {
13770
14076
  untrustedContent: {
13771
14077
  ...DEFAULT_CONFIG.policy.untrustedContent,
13772
14078
  orgDomains: [...DEFAULT_CONFIG.policy.untrustedContent.orgDomains],
13773
- roles: {}
14079
+ roles: {},
14080
+ trustedRecipients: []
13774
14081
  },
13775
14082
  // Left empty on purpose: the local file is read fresh at policy-eval time
13776
14083
  // (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
@@ -13816,8 +14123,14 @@ function getConfig(cwd) {
13816
14123
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
13817
14124
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
13818
14125
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
13819
- if (p.toolInspection)
13820
- mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
14126
+ if (p.toolInspection) {
14127
+ if (isProject)
14128
+ mergedPolicy.projectToolInspection = {
14129
+ ...mergedPolicy.projectToolInspection,
14130
+ ...p.toolInspection
14131
+ };
14132
+ else mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
14133
+ }
13821
14134
  if (p.smartRules) {
13822
14135
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
13823
14136
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
@@ -13965,7 +14278,7 @@ function getConfig(cwd) {
13965
14278
  let cloudMandatesEnforcement = false;
13966
14279
  let cloudMandatesAppPerm = false;
13967
14280
  {
13968
- const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
14281
+ const cacheFile = path5.join(os5.homedir(), ".node9", "rules-cache.json");
13969
14282
  try {
13970
14283
  const raw = readRulesCacheResilient(cacheFile);
13971
14284
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
@@ -14358,10 +14671,10 @@ function getConfig(cwd) {
14358
14671
  }
14359
14672
  function tryLoadConfig(filePath, options = {}) {
14360
14673
  v2ChecksByPath.delete(filePath);
14361
- if (!fs4.existsSync(filePath)) return null;
14674
+ if (!fs5.existsSync(filePath)) return null;
14362
14675
  let raw;
14363
14676
  try {
14364
- raw = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
14677
+ raw = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
14365
14678
  } catch (err2) {
14366
14679
  const msg = err2 instanceof Error ? err2.message : String(err2);
14367
14680
  process.stderr.write(
@@ -14436,6 +14749,13 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
14436
14749
  if (replaceLists) cur.orgDomains = domains;
14437
14750
  else for (const d of domains) if (!cur.orgDomains.includes(d)) cur.orgDomains.push(d);
14438
14751
  }
14752
+ if (replaceLists && Array.isArray(uc.trustedRecipients)) {
14753
+ cur.trustedRecipients = [
14754
+ ...new Set(
14755
+ uc.trustedRecipients.filter((v) => typeof v === "string").map((v) => normalizeRecipient(v)).filter((v) => v !== null)
14756
+ )
14757
+ ].slice(0, 200);
14758
+ }
14439
14759
  if (uc.roles && typeof uc.roles === "object" && !Array.isArray(uc.roles)) {
14440
14760
  const next = replaceLists ? {} : cur.roles;
14441
14761
  for (const [server, tools] of Object.entries(uc.roles)) {
@@ -14456,6 +14776,7 @@ var DANGEROUS_WORDS, RUNTIME_ONLY_CONFIG_KEYS, DEFAULT_CONFIG, RM_SAFE_PATH_PATT
14456
14776
  var init_config = __esm({
14457
14777
  "src/config/index.ts"() {
14458
14778
  "use strict";
14779
+ init_trusted_recipients();
14459
14780
  init_config_schema();
14460
14781
  init_shields();
14461
14782
  init_managed();
@@ -14710,7 +15031,8 @@ var init_config = __esm({
14710
15031
  spotlight: true,
14711
15032
  afterExposure: "ask-once",
14712
15033
  orgDomains: [],
14713
- roles: {}
15034
+ roles: {},
15035
+ trustedRecipients: []
14714
15036
  },
14715
15037
  trustedHosts: [],
14716
15038
  trustedHostsManaged: false,
@@ -14803,18 +15125,18 @@ var init_regex = __esm({
14803
15125
  });
14804
15126
 
14805
15127
  // src/dlp.ts
14806
- import fs5 from "fs";
14807
- import path5 from "path";
15128
+ import fs6 from "fs";
15129
+ import path6 from "path";
14808
15130
  function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
14809
15131
  if (!filePath) return null;
14810
15132
  let resolved;
14811
15133
  try {
14812
- const absolute = path5.resolve(cwd, filePath);
14813
- resolved = fs5.realpathSync.native(absolute);
15134
+ const absolute = path6.resolve(cwd, filePath);
15135
+ resolved = fs6.realpathSync.native(absolute);
14814
15136
  } catch (err2) {
14815
15137
  const code2 = err2.code;
14816
15138
  if (code2 === "ENOENT" || code2 === "ENOTDIR") {
14817
- resolved = path5.resolve(cwd, filePath);
15139
+ resolved = path6.resolve(cwd, filePath);
14818
15140
  } else {
14819
15141
  return sensitivePathMatch(filePath);
14820
15142
  }
@@ -14830,29 +15152,29 @@ var init_dlp = __esm({
14830
15152
  });
14831
15153
 
14832
15154
  // src/policy/file-access.ts
14833
- import fs6 from "fs";
14834
- import os5 from "os";
14835
- import path6 from "path";
15155
+ import fs7 from "fs";
15156
+ import os6 from "os";
15157
+ import path7 from "path";
14836
15158
  function absoluteSpelling(p, cwd, home) {
14837
15159
  if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
14838
- return path6.isAbsolute(p) ? p : `${cwd}/${p}`;
15160
+ return path7.isAbsolute(p) ? p : `${cwd}/${p}`;
14839
15161
  }
14840
15162
  function physical(p, followFinal, links = 0) {
14841
15163
  if (links > 40) throw new Error("Too many symbolic links");
14842
15164
  if (!followFinal) {
14843
- const parent = physical(path6.dirname(p), true, links);
14844
- return { path: `${parent.path}/${path6.basename(p)}`, missing: parent.missing };
15165
+ const parent = physical(path7.dirname(p), true, links);
15166
+ return { path: `${parent.path}/${path7.basename(p)}`, missing: parent.missing };
14845
15167
  }
14846
15168
  try {
14847
- return { path: fs6.realpathSync.native(p), missing: false };
15169
+ return { path: fs7.realpathSync.native(p), missing: false };
14848
15170
  } catch (error) {
14849
15171
  const code2 = error.code;
14850
15172
  if (!["ENOENT", "ENOTDIR"].includes(code2 ?? "")) throw error;
14851
15173
  try {
14852
- if (fs6.lstatSync(p).isSymbolicLink()) {
14853
- const target = fs6.readlinkSync(p);
15174
+ if (fs7.lstatSync(p).isSymbolicLink()) {
15175
+ const target = fs7.readlinkSync(p);
14854
15176
  return physical(
14855
- path6.isAbsolute(target) ? target : `${path6.dirname(p)}/${target}`,
15177
+ path7.isAbsolute(target) ? target : `${path7.dirname(p)}/${target}`,
14856
15178
  true,
14857
15179
  links + 1
14858
15180
  );
@@ -14861,13 +15183,13 @@ function physical(p, followFinal, links = 0) {
14861
15183
  if (!["ENOENT", "ENOTDIR"].includes(linkError.code ?? ""))
14862
15184
  throw linkError;
14863
15185
  }
14864
- const parent = path6.dirname(p);
15186
+ const parent = path7.dirname(p);
14865
15187
  if (parent === p) return { path: p, missing: true };
14866
15188
  const resolved = physical(parent, true, links);
14867
- return { path: `${resolved.path}/${path6.basename(p)}`, missing: true };
15189
+ return { path: `${resolved.path}/${path7.basename(p)}`, missing: true };
14868
15190
  }
14869
15191
  }
14870
- function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = os5.homedir()) {
15192
+ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = os6.homedir()) {
14871
15193
  const context = { cwd, homeDirs: [home, "/root"] };
14872
15194
  const accesses = collectFileAccesses(tool, args, context, inspection);
14873
15195
  const handledPaths = /* @__PURE__ */ new Set();
@@ -14890,14 +15212,14 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
14890
15212
  const raw = absoluteSpelling(access.path, access.cwd ?? cwd, home);
14891
15213
  if (access.directoryTarget) {
14892
15214
  try {
14893
- if (!fs6.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
15215
+ if (!fs7.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
14894
15216
  continue;
14895
15217
  } catch {
14896
15218
  }
14897
15219
  }
14898
15220
  if (access.operation === "create") {
14899
15221
  try {
14900
- fs6.statSync(raw);
15222
+ fs7.statSync(raw);
14901
15223
  continue;
14902
15224
  } catch {
14903
15225
  }
@@ -14952,17 +15274,17 @@ var init_file_access = __esm({
14952
15274
  });
14953
15275
 
14954
15276
  // src/utils/provenance.ts
14955
- import fs7 from "fs";
14956
- import path7 from "path";
14957
- import os6 from "os";
15277
+ import fs8 from "fs";
15278
+ import path8 from "path";
15279
+ import os7 from "os";
14958
15280
  function findInPath(cmd) {
14959
- if (path7.posix.isAbsolute(cmd)) return cmd;
15281
+ if (path8.posix.isAbsolute(cmd)) return cmd;
14960
15282
  const pathEnv = process.env.PATH ?? "";
14961
- for (const dir2 of pathEnv.split(path7.delimiter)) {
15283
+ for (const dir2 of pathEnv.split(path8.delimiter)) {
14962
15284
  if (!dir2) continue;
14963
- const full = path7.join(dir2, cmd);
15285
+ const full = path8.join(dir2, cmd);
14964
15286
  try {
14965
- fs7.accessSync(full, fs7.constants.X_OK);
15287
+ fs8.accessSync(full, fs8.constants.X_OK);
14966
15288
  return full;
14967
15289
  } catch {
14968
15290
  }
@@ -14973,7 +15295,7 @@ function _classifyPath(resolved, cwd) {
14973
15295
  if (cwd && resolved.startsWith(cwd + "/")) {
14974
15296
  return { trustLevel: "user", reason: "binary in project directory" };
14975
15297
  }
14976
- const osTmp = os6.tmpdir();
15298
+ const osTmp = os7.tmpdir();
14977
15299
  const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
14978
15300
  if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
14979
15301
  return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
@@ -14991,7 +15313,7 @@ function _classifyPath(resolved, cwd) {
14991
15313
  }
14992
15314
  function checkProvenance(cmd, cwd) {
14993
15315
  const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
14994
- if (path7.posix.isAbsolute(bare)) {
15316
+ if (path8.posix.isAbsolute(bare)) {
14995
15317
  const early = _classifyPath(bare, cwd);
14996
15318
  if (early.trustLevel === "suspect") {
14997
15319
  return { resolvedPath: bare, ...early };
@@ -15007,7 +15329,7 @@ function checkProvenance(cmd, cwd) {
15007
15329
  reason: "binary not found in PATH"
15008
15330
  };
15009
15331
  }
15010
- resolved = fs7.realpathSync(found);
15332
+ resolved = fs8.realpathSync(found);
15011
15333
  } catch {
15012
15334
  return {
15013
15335
  resolvedPath: cmd,
@@ -15016,7 +15338,7 @@ function checkProvenance(cmd, cwd) {
15016
15338
  };
15017
15339
  }
15018
15340
  try {
15019
- const stat = fs7.statSync(resolved);
15341
+ const stat = fs8.statSync(resolved);
15020
15342
  if (stat.mode & 2) {
15021
15343
  return {
15022
15344
  resolvedPath: resolved,
@@ -15041,23 +15363,23 @@ var init_provenance = __esm({
15041
15363
  SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
15042
15364
  MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
15043
15365
  USER_PREFIXES = [
15044
- path7.join(os6.homedir(), "bin"),
15045
- path7.join(os6.homedir(), ".local", "bin"),
15046
- path7.join(os6.homedir(), ".cargo", "bin"),
15047
- path7.join(os6.homedir(), ".npm-global", "bin"),
15048
- path7.join(os6.homedir(), ".volta", "bin")
15366
+ path8.join(os7.homedir(), "bin"),
15367
+ path8.join(os7.homedir(), ".local", "bin"),
15368
+ path8.join(os7.homedir(), ".cargo", "bin"),
15369
+ path8.join(os7.homedir(), ".npm-global", "bin"),
15370
+ path8.join(os7.homedir(), ".volta", "bin")
15049
15371
  ];
15050
15372
  SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
15051
15373
  }
15052
15374
  });
15053
15375
 
15054
15376
  // src/policy/index.ts
15055
- import fs8 from "fs";
15056
- import path8 from "path";
15057
- import os7 from "os";
15377
+ import fs9 from "fs";
15378
+ import path9 from "path";
15379
+ import os8 from "os";
15058
15380
  async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
15059
15381
  const config = opts?.config ?? getConfig(cwd);
15060
- const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, config.policy.toolInspection);
15382
+ const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, inspectionLayers(config.policy));
15061
15383
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
15062
15384
  return evaluatePolicy(
15063
15385
  config,
@@ -15089,8 +15411,11 @@ function tokenize3(toolName) {
15089
15411
  }
15090
15412
  function explainExtractShellCommand(toolName, args, toolInspection) {
15091
15413
  const shell = resolveShellInput(toolName, args, toolInspection);
15092
- const matchingPattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
15093
- const entry = matchingPattern === void 0 ? void 0 : toolInspection[matchingPattern];
15414
+ const matchingPattern = [
15415
+ ...Object.keys(toolInspection.trusted),
15416
+ ...Object.keys(toolInspection.project ?? {})
15417
+ ].find((p) => matchesPattern(toolName, p));
15418
+ const entry = inspectionEntry(toolName, toolInspection);
15094
15419
  const legacyValue = shell.kind === "not-shell" && typeof entry === "string" ? getNestedValue(args, entry) : null;
15095
15420
  return {
15096
15421
  command: shell.kind === "shell" ? shell.command : typeof legacyValue === "string" ? legacyValue : null,
@@ -15099,15 +15424,17 @@ function explainExtractShellCommand(toolName, args, toolInspection) {
15099
15424
  };
15100
15425
  }
15101
15426
  function explainIsSqlTool(toolName, toolInspection) {
15102
- const patterns = Object.keys(toolInspection);
15103
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
15104
- if (!matchingPattern) return false;
15105
- const fieldName = toolInspection[matchingPattern];
15427
+ const fieldName = inspectionEntry(toolName, toolInspection);
15106
15428
  return fieldName === "sql" || fieldName === "query";
15107
15429
  }
15108
15430
  async function explainPolicy(toolName, args, agent = EXPLAIN_AGENT) {
15109
15431
  const config = getConfig();
15110
- const prepared = prepareFileAccesses(toolName, args, process.cwd(), config.policy.toolInspection);
15432
+ const prepared = prepareFileAccesses(
15433
+ toolName,
15434
+ args,
15435
+ process.cwd(),
15436
+ inspectionLayers(config.policy)
15437
+ );
15111
15438
  const derived = await deriveExplainTrace(toolName, args, prepared);
15112
15439
  const engine = await evaluatePolicy2(toolName, args, agent, prepared.cwd, {
15113
15440
  fileAccess: prepared,
@@ -15136,10 +15463,11 @@ async function explainPolicy(toolName, args, agent = EXPLAIN_AGENT) {
15136
15463
  }
15137
15464
  async function deriveExplainTrace(toolName, args, prepared) {
15138
15465
  const steps = [];
15139
- const globalPath = path8.join(os7.homedir(), ".node9", "config.json");
15140
- const projectPath = path8.join(process.cwd(), "node9.config.json");
15141
- const credsPath = path8.join(os7.homedir(), ".node9", "credentials.json");
15466
+ const globalPath = path9.join(os8.homedir(), ".node9", "config.json");
15467
+ const projectPath = path9.join(process.cwd(), "node9.config.json");
15468
+ const credsPath = path9.join(os8.homedir(), ".node9", "credentials.json");
15142
15469
  const config = getConfig();
15470
+ const layers = inspectionLayers(config.policy);
15143
15471
  const wsGoverned = config.policySource === "workspace";
15144
15472
  const waterfall = [
15145
15473
  {
@@ -15151,22 +15479,22 @@ async function deriveExplainTrace(toolName, args, prepared) {
15151
15479
  {
15152
15480
  tier: 2,
15153
15481
  label: wsGoverned ? "Workspace config" : "Cloud policy",
15154
- status: fs8.existsSync(credsPath) ? "active" : "missing",
15155
- note: fs8.existsSync(credsPath) ? wsGoverned ? "authoritative \u2014 this machine follows the workspace configuration (app.node9.ai)" : "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
15482
+ status: fs9.existsSync(credsPath) ? "active" : "missing",
15483
+ note: fs9.existsSync(credsPath) ? wsGoverned ? "authoritative \u2014 this machine follows the workspace configuration (app.node9.ai)" : "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
15156
15484
  },
15157
15485
  {
15158
15486
  tier: 3,
15159
15487
  label: "Project config",
15160
- status: fs8.existsSync(projectPath) ? "active" : "missing",
15488
+ status: fs9.existsSync(projectPath) ? "active" : "missing",
15161
15489
  path: projectPath,
15162
- ...wsGoverned && fs8.existsSync(projectPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15490
+ ...wsGoverned && fs9.existsSync(projectPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15163
15491
  },
15164
15492
  {
15165
15493
  tier: 4,
15166
15494
  label: "Global config",
15167
- status: fs8.existsSync(globalPath) ? "active" : "missing",
15495
+ status: fs9.existsSync(globalPath) ? "active" : "missing",
15168
15496
  path: globalPath,
15169
- ...wsGoverned && fs8.existsSync(globalPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15497
+ ...wsGoverned && fs9.existsSync(globalPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15170
15498
  },
15171
15499
  {
15172
15500
  tier: 5,
@@ -15217,10 +15545,7 @@ async function deriveExplainTrace(toolName, args, prepared) {
15217
15545
  });
15218
15546
  if (config.policy.smartRules.length > 0) {
15219
15547
  const matchedRule = config.policy.smartRules.find(
15220
- (rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && evaluateSmartConditions(
15221
- shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
15222
- rule
15223
- )
15548
+ (rule) => toolMatchesRule(toolName, rule.tool, layers) && evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule)
15224
15549
  );
15225
15550
  if (matchedRule) {
15226
15551
  const label3 = `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`;
@@ -15262,7 +15587,7 @@ async function deriveExplainTrace(toolName, args, prepared) {
15262
15587
  command: shellCommand,
15263
15588
  matchingPattern,
15264
15589
  fieldName
15265
- } = explainExtractShellCommand(toolName, args, config.policy.toolInspection);
15590
+ } = explainExtractShellCommand(toolName, args, layers);
15266
15591
  if (shellCommand) {
15267
15592
  const analyzed = analyzeShellCommand(shellCommand);
15268
15593
  allTokens = analyzed.allTokens;
@@ -15313,7 +15638,7 @@ async function deriveExplainTrace(toolName, args, prepared) {
15313
15638
  outcome: "checked",
15314
15639
  detail: "No dangerous eval detected"
15315
15640
  });
15316
- if (explainIsSqlTool(toolName, config.policy.toolInspection)) {
15641
+ if (explainIsSqlTool(toolName, layers)) {
15317
15642
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS2.has(t.toLowerCase()));
15318
15643
  steps.push({
15319
15644
  name: "SQL token stripping",
@@ -15442,27 +15767,27 @@ var init_policy = __esm({
15442
15767
  });
15443
15768
 
15444
15769
  // src/utils/atomic-write.ts
15445
- import fs9 from "fs";
15446
- import path9 from "path";
15770
+ import fs10 from "fs";
15771
+ import path10 from "path";
15447
15772
  import { randomUUID } from "crypto";
15448
15773
  function atomicWriteSync(filePath, data, options) {
15449
- const dir2 = path9.dirname(filePath);
15450
- if (!fs9.existsSync(dir2)) fs9.mkdirSync(dir2, { recursive: true });
15774
+ const dir2 = path10.dirname(filePath);
15775
+ if (!fs10.existsSync(dir2)) fs10.mkdirSync(dir2, { recursive: true });
15451
15776
  const tmpPath = `${filePath}.${randomUUID()}.tmp`;
15452
15777
  try {
15453
- fs9.writeFileSync(tmpPath, data, options);
15778
+ fs10.writeFileSync(tmpPath, data, options);
15454
15779
  } catch (err2) {
15455
15780
  try {
15456
- fs9.unlinkSync(tmpPath);
15781
+ fs10.unlinkSync(tmpPath);
15457
15782
  } catch {
15458
15783
  }
15459
15784
  throw err2;
15460
15785
  }
15461
15786
  try {
15462
- fs9.renameSync(tmpPath, filePath);
15787
+ fs10.renameSync(tmpPath, filePath);
15463
15788
  } catch (err2) {
15464
15789
  try {
15465
- fs9.unlinkSync(tmpPath);
15790
+ fs10.unlinkSync(tmpPath);
15466
15791
  } catch {
15467
15792
  }
15468
15793
  throw err2;
@@ -15475,9 +15800,9 @@ var init_atomic_write = __esm({
15475
15800
  });
15476
15801
 
15477
15802
  // src/auth/state.ts
15478
- import fs10 from "fs";
15479
- import path10 from "path";
15480
- import os8 from "os";
15803
+ import fs11 from "fs";
15804
+ import path11 from "path";
15805
+ import os9 from "os";
15481
15806
  function extractCommandPattern(toolName, args) {
15482
15807
  const lower2 = toolName.toLowerCase();
15483
15808
  if (lower2 !== "bash" && lower2 !== "execute_bash" && lower2 !== "shell") return void 0;
@@ -15489,11 +15814,11 @@ function extractCommandPattern(toolName, args) {
15489
15814
  }
15490
15815
  function checkPause() {
15491
15816
  try {
15492
- if (!fs10.existsSync(PAUSED_FILE)) return { paused: false };
15493
- const state = JSON.parse(fs10.readFileSync(PAUSED_FILE, "utf-8"));
15817
+ if (!fs11.existsSync(PAUSED_FILE)) return { paused: false };
15818
+ const state = JSON.parse(fs11.readFileSync(PAUSED_FILE, "utf-8"));
15494
15819
  if (state.expiry > 0 && Date.now() >= state.expiry) {
15495
15820
  try {
15496
- fs10.unlinkSync(PAUSED_FILE);
15821
+ fs11.unlinkSync(PAUSED_FILE);
15497
15822
  } catch {
15498
15823
  }
15499
15824
  return { paused: false };
@@ -15509,14 +15834,14 @@ function pauseNode9(durationMs, durationStr) {
15509
15834
  }
15510
15835
  function resumeNode9() {
15511
15836
  try {
15512
- if (fs10.existsSync(PAUSED_FILE)) fs10.unlinkSync(PAUSED_FILE);
15837
+ if (fs11.existsSync(PAUSED_FILE)) fs11.unlinkSync(PAUSED_FILE);
15513
15838
  } catch {
15514
15839
  }
15515
15840
  }
15516
15841
  function getActiveTrustSession(toolName, args) {
15517
15842
  try {
15518
- if (!fs10.existsSync(TRUST_FILE)) return false;
15519
- const trust = JSON.parse(fs10.readFileSync(TRUST_FILE, "utf-8"));
15843
+ if (!fs11.existsSync(TRUST_FILE)) return false;
15844
+ const trust = JSON.parse(fs11.readFileSync(TRUST_FILE, "utf-8"));
15520
15845
  const now = Date.now();
15521
15846
  const active = trust.entries.filter((e) => e.expiry > now);
15522
15847
  if (active.length !== trust.entries.length) {
@@ -15539,8 +15864,8 @@ function writeTrustSession(toolName, durationMs, args) {
15539
15864
  try {
15540
15865
  let trust = { entries: [] };
15541
15866
  try {
15542
- if (fs10.existsSync(TRUST_FILE)) {
15543
- trust = JSON.parse(fs10.readFileSync(TRUST_FILE, "utf-8"));
15867
+ if (fs11.existsSync(TRUST_FILE)) {
15868
+ trust = JSON.parse(fs11.readFileSync(TRUST_FILE, "utf-8"));
15544
15869
  }
15545
15870
  } catch {
15546
15871
  }
@@ -15562,9 +15887,9 @@ function writeTrustSession(toolName, durationMs, args) {
15562
15887
  }
15563
15888
  function getPersistentDecision(toolName) {
15564
15889
  try {
15565
- const file2 = path10.join(os8.homedir(), ".node9", "decisions.json");
15566
- if (!fs10.existsSync(file2)) return null;
15567
- const decisions = JSON.parse(fs10.readFileSync(file2, "utf-8"));
15890
+ const file2 = path11.join(os9.homedir(), ".node9", "decisions.json");
15891
+ if (!fs11.existsSync(file2)) return null;
15892
+ const decisions = JSON.parse(fs11.readFileSync(file2, "utf-8"));
15568
15893
  const d = decisions[toolName];
15569
15894
  if (d === "allow" || d === "deny") return d;
15570
15895
  } catch {
@@ -15577,16 +15902,16 @@ var init_state = __esm({
15577
15902
  "use strict";
15578
15903
  init_policy();
15579
15904
  init_atomic_write();
15580
- PAUSED_FILE = path10.join(os8.homedir(), ".node9", "PAUSED");
15581
- TRUST_FILE = path10.join(os8.homedir(), ".node9", "trust.json");
15905
+ PAUSED_FILE = path11.join(os9.homedir(), ".node9", "PAUSED");
15906
+ TRUST_FILE = path11.join(os9.homedir(), ".node9", "trust.json");
15582
15907
  }
15583
15908
  });
15584
15909
 
15585
15910
  // src/auth/daemon.ts
15586
- import fs11 from "fs";
15911
+ import fs12 from "fs";
15587
15912
  import net from "net";
15588
- import path11 from "path";
15589
- import os9 from "os";
15913
+ import path12 from "path";
15914
+ import os10 from "os";
15590
15915
  function notifyActivitySocket(data) {
15591
15916
  return new Promise((resolve2) => {
15592
15917
  try {
@@ -15625,9 +15950,9 @@ function resolveDaemonPort(env = process.env) {
15625
15950
  }
15626
15951
  function getInternalToken() {
15627
15952
  try {
15628
- const pidFile = path11.join(os9.homedir(), ".node9", "daemon.pid");
15629
- if (!fs11.existsSync(pidFile)) return null;
15630
- const data = JSON.parse(fs11.readFileSync(pidFile, "utf-8"));
15953
+ const pidFile = path12.join(os10.homedir(), ".node9", "daemon.pid");
15954
+ if (!fs12.existsSync(pidFile)) return null;
15955
+ const data = JSON.parse(fs12.readFileSync(pidFile, "utf-8"));
15631
15956
  process.kill(data.pid, 0);
15632
15957
  return data.internalToken ?? null;
15633
15958
  } catch {
@@ -15635,12 +15960,12 @@ function getInternalToken() {
15635
15960
  }
15636
15961
  }
15637
15962
  function isDaemonRunning() {
15638
- const pidFile = path11.join(os9.homedir(), ".node9", "daemon.pid");
15639
- if (fs11.existsSync(pidFile)) {
15963
+ const pidFile = path12.join(os10.homedir(), ".node9", "daemon.pid");
15964
+ if (fs12.existsSync(pidFile)) {
15640
15965
  let pid;
15641
15966
  let port;
15642
15967
  try {
15643
- const data = JSON.parse(fs11.readFileSync(pidFile, "utf-8"));
15968
+ const data = JSON.parse(fs12.readFileSync(pidFile, "utf-8"));
15644
15969
  pid = data.pid;
15645
15970
  port = data.port;
15646
15971
  } catch {
@@ -15658,7 +15983,7 @@ function isDaemonRunning() {
15658
15983
  } catch (err2) {
15659
15984
  if (err2 instanceof Error && "code" in err2 && err2.code === "ESRCH") {
15660
15985
  try {
15661
- fs11.unlinkSync(pidFile);
15986
+ fs12.unlinkSync(pidFile);
15662
15987
  } catch {
15663
15988
  }
15664
15989
  }
@@ -15976,7 +16301,7 @@ var ACTIVITY_SOCKET_PATH, DEFAULT_DAEMON_PORT, DAEMON_PORT, DAEMON_HOST;
15976
16301
  var init_daemon = __esm({
15977
16302
  "src/auth/daemon.ts"() {
15978
16303
  "use strict";
15979
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path11.join(os9.tmpdir(), "node9-activity.sock");
16304
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path12.join(os10.tmpdir(), "node9-activity.sock");
15980
16305
  DEFAULT_DAEMON_PORT = 7391;
15981
16306
  DAEMON_PORT = resolveDaemonPort();
15982
16307
  DAEMON_HOST = "127.0.0.1";
@@ -15984,7 +16309,7 @@ var init_daemon = __esm({
15984
16309
  });
15985
16310
 
15986
16311
  // src/context-sniper.ts
15987
- import path12 from "path";
16312
+ import path13 from "path";
15988
16313
  function smartTruncate(str2, maxLen = 500) {
15989
16314
  if (str2.length <= maxLen) return str2;
15990
16315
  const edge = Math.floor(maxLen / 2) - 3;
@@ -16036,7 +16361,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
16036
16361
  intent = "EDIT";
16037
16362
  if (obj.file_path) {
16038
16363
  editFilePath = String(obj.file_path);
16039
- editFileName = path12.basename(editFilePath);
16364
+ editFileName = path13.basename(editFilePath);
16040
16365
  }
16041
16366
  const result = extractContext(String(obj.new_string), matchedWord);
16042
16367
  contextSnippet = result.snippet;
@@ -16101,7 +16426,7 @@ var init_safe_text = __esm({
16101
16426
 
16102
16427
  // src/ui/native.ts
16103
16428
  import { spawn } from "child_process";
16104
- import path13 from "path";
16429
+ import path14 from "path";
16105
16430
  function resolveNativeDecision(opts) {
16106
16431
  const { code: code2, output, elapsedMs, locked } = opts;
16107
16432
  if (locked) return "deny";
@@ -16129,7 +16454,7 @@ function formatArgs(args, matchedField, matchedWord) {
16129
16454
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
16130
16455
  const obj = parsed;
16131
16456
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
16132
- const file2 = obj.file_path ? path13.basename(String(obj.file_path)) : "file";
16457
+ const file2 = obj.file_path ? path14.basename(String(obj.file_path)) : "file";
16133
16458
  const oldPreview = smartTruncate(String(obj.old_string), 120);
16134
16459
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
16135
16460
  return {
@@ -16360,16 +16685,16 @@ var init_native = __esm({
16360
16685
  });
16361
16686
 
16362
16687
  // src/shields/jail.ts
16363
- import fs12 from "fs";
16364
- import os10 from "os";
16365
- import path14 from "path";
16688
+ import fs13 from "fs";
16689
+ import os11 from "os";
16690
+ import path15 from "path";
16366
16691
  function jailStorePath() {
16367
- return path14.join(os10.homedir(), ".node9", "jail-paths.json");
16692
+ return path15.join(os11.homedir(), ".node9", "jail-paths.json");
16368
16693
  }
16369
16694
  function readJailPaths() {
16370
16695
  let text;
16371
16696
  try {
16372
- text = fs12.readFileSync(jailStorePath(), "utf8");
16697
+ text = fs13.readFileSync(jailStorePath(), "utf8");
16373
16698
  } catch (err2) {
16374
16699
  if (err2.code === "ENOENT") return [];
16375
16700
  throw err2;
@@ -16387,8 +16712,8 @@ function readJailPaths() {
16387
16712
  }
16388
16713
  function writeJailPaths(paths) {
16389
16714
  const p = jailStorePath();
16390
- fs12.mkdirSync(path14.dirname(p), { recursive: true });
16391
- fs12.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
16715
+ fs13.mkdirSync(path15.dirname(p), { recursive: true });
16716
+ fs13.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
16392
16717
  }
16393
16718
  function addJailPath(rawPath, verdict) {
16394
16719
  const norm = rawPath.trim();
@@ -16427,14 +16752,14 @@ function removeJailPath(rawPath) {
16427
16752
  return { removed, paths: after };
16428
16753
  }
16429
16754
  function regenerateUserJail(paths) {
16430
- const file2 = path14.join(USER_SHIELDS_DIR_PATH, `${USER_JAIL_SHIELD}.json`);
16755
+ const file2 = path15.join(USER_SHIELDS_DIR_PATH, `${USER_JAIL_SHIELD}.json`);
16431
16756
  if (paths.length === 0) {
16432
16757
  const active2 = readActiveShields();
16433
16758
  if (active2.includes(USER_JAIL_SHIELD)) {
16434
16759
  writeActiveShields(active2.filter((s) => s !== USER_JAIL_SHIELD));
16435
16760
  }
16436
16761
  try {
16437
- fs12.rmSync(file2, { force: true });
16762
+ fs13.rmSync(file2, { force: true });
16438
16763
  } catch {
16439
16764
  }
16440
16765
  return;
@@ -16461,12 +16786,12 @@ var init_jail = __esm({
16461
16786
  });
16462
16787
 
16463
16788
  // src/canary/registry.ts
16464
- import fs13 from "fs";
16465
- import os11 from "os";
16466
- import path15 from "path";
16789
+ import fs14 from "fs";
16790
+ import os12 from "os";
16791
+ import path16 from "path";
16467
16792
  import { createHash as createHash3, randomUUID as randomUUID2, randomBytes } from "crypto";
16468
16793
  function canaryStorePath() {
16469
- return path15.join(os11.homedir(), ".node9", "canaries.json");
16794
+ return path16.join(os12.homedir(), ".node9", "canaries.json");
16470
16795
  }
16471
16796
  function isRecord(x) {
16472
16797
  if (!x || typeof x !== "object") return false;
@@ -16477,7 +16802,7 @@ function loadCanaries(opts) {
16477
16802
  const p = canaryStorePath();
16478
16803
  let raw;
16479
16804
  try {
16480
- raw = fs13.readFileSync(p, "utf-8");
16805
+ raw = fs14.readFileSync(p, "utf-8");
16481
16806
  } catch (e) {
16482
16807
  if (e.code === "ENOENT") return [];
16483
16808
  throw e;
@@ -16499,19 +16824,19 @@ function saveCanaries(records) {
16499
16824
  const p = canaryStorePath();
16500
16825
  const paths = addJailPath(p, "block");
16501
16826
  regenerateUserJail(paths);
16502
- fs13.mkdirSync(path15.dirname(p), { recursive: true });
16827
+ fs14.mkdirSync(path16.dirname(p), { recursive: true });
16503
16828
  const tmp = `${p}.${randomBytes(8).toString("hex")}.tmp`;
16504
16829
  try {
16505
- fs13.writeFileSync(tmp, JSON.stringify({ version: 1, records }, null, 2) + "\n", {
16830
+ fs14.writeFileSync(tmp, JSON.stringify({ version: 1, records }, null, 2) + "\n", {
16506
16831
  mode: 384,
16507
16832
  flag: "wx"
16508
16833
  });
16509
- fs13.chmodSync(tmp, 384);
16510
- fs13.renameSync(tmp, p);
16834
+ fs14.chmodSync(tmp, 384);
16835
+ fs14.renameSync(tmp, p);
16511
16836
  } finally {
16512
- if (fs13.existsSync(tmp)) {
16837
+ if (fs14.existsSync(tmp)) {
16513
16838
  try {
16514
- fs13.unlinkSync(tmp);
16839
+ fs14.unlinkSync(tmp);
16515
16840
  } catch {
16516
16841
  }
16517
16842
  }
@@ -16561,21 +16886,21 @@ var init_registry = __esm({
16561
16886
  });
16562
16887
 
16563
16888
  // src/utils/git-remotes.ts
16564
- import fs14 from "fs";
16565
- import path16 from "path";
16889
+ import fs15 from "fs";
16890
+ import path17 from "path";
16566
16891
  function readSmallFile(p, max) {
16567
16892
  let fd;
16568
16893
  try {
16569
- fd = fs14.openSync(p, fs14.constants.O_RDONLY | (fs14.constants.O_NOFOLLOW ?? 0));
16570
- const st = fs14.fstatSync(fd);
16894
+ fd = fs15.openSync(p, fs15.constants.O_RDONLY | (fs15.constants.O_NOFOLLOW ?? 0));
16895
+ const st = fs15.fstatSync(fd);
16571
16896
  if (!st.isFile() || st.size > max) return null;
16572
16897
  const buf = Buffer.alloc(st.size);
16573
- const n = fs14.readSync(fd, buf, 0, st.size, 0);
16898
+ const n = fs15.readSync(fd, buf, 0, st.size, 0);
16574
16899
  return buf.subarray(0, n).toString("utf8");
16575
16900
  } catch {
16576
16901
  return null;
16577
16902
  } finally {
16578
- if (fd !== void 0) fs14.closeSync(fd);
16903
+ if (fd !== void 0) fs15.closeSync(fd);
16579
16904
  }
16580
16905
  }
16581
16906
  function gitRemoteHost(url) {
@@ -16594,20 +16919,20 @@ function gitRemoteHost(url) {
16594
16919
  return null;
16595
16920
  }
16596
16921
  function findGitConfig(dir2, walkUp = true) {
16597
- let cur = path16.resolve(dir2);
16922
+ let cur = path17.resolve(dir2);
16598
16923
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
16599
- const dotGit = path16.join(cur, ".git");
16600
- const direct = readSmallFile(path16.join(dotGit, "config"), 1e6);
16924
+ const dotGit = path17.join(cur, ".git");
16925
+ const direct = readSmallFile(path17.join(dotGit, "config"), 1e6);
16601
16926
  if (direct !== null) return direct;
16602
16927
  const pointer = readSmallFile(dotGit, 4096);
16603
16928
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
16604
16929
  if (m) {
16605
- let gitDir = path16.resolve(cur, m[1]);
16606
- const common = readSmallFile(path16.join(gitDir, "commondir"), 4096);
16607
- if (common !== null) gitDir = path16.resolve(gitDir, common.trim());
16608
- return readSmallFile(path16.join(gitDir, "config"), 1e6);
16930
+ let gitDir = path17.resolve(cur, m[1]);
16931
+ const common = readSmallFile(path17.join(gitDir, "commondir"), 4096);
16932
+ if (common !== null) gitDir = path17.resolve(gitDir, common.trim());
16933
+ return readSmallFile(path17.join(gitDir, "config"), 1e6);
16609
16934
  }
16610
- const parent = path16.dirname(cur);
16935
+ const parent = path17.dirname(cur);
16611
16936
  if (parent === cur) return null;
16612
16937
  cur = parent;
16613
16938
  }
@@ -16616,12 +16941,12 @@ function findGitConfig(dir2, walkUp = true) {
16616
16941
  function readGitRemotes(dir2, opts = {}) {
16617
16942
  if (!dir2) return [];
16618
16943
  const walkUp = opts.walkUp !== false;
16619
- const key = `${walkUp ? "up" : "at"}:${path16.resolve(dir2)}`;
16620
- const hit = cache.get(key);
16944
+ const key = `${walkUp ? "up" : "at"}:${path17.resolve(dir2)}`;
16945
+ const hit = cache2.get(key);
16621
16946
  if (hit) return hit;
16622
16947
  const out = [];
16623
16948
  try {
16624
- const cfg = findGitConfig(path16.resolve(dir2), walkUp);
16949
+ const cfg = findGitConfig(path17.resolve(dir2), walkUp);
16625
16950
  if (cfg !== null) {
16626
16951
  let current = null;
16627
16952
  for (const line of cfg.split(/\r?\n/)) {
@@ -16643,31 +16968,31 @@ function readGitRemotes(dir2, opts = {}) {
16643
16968
  }
16644
16969
  } catch {
16645
16970
  }
16646
- cache.set(key, out);
16971
+ cache2.set(key, out);
16647
16972
  return out;
16648
16973
  }
16649
- var cache;
16974
+ var cache2;
16650
16975
  var init_git_remotes = __esm({
16651
16976
  "src/utils/git-remotes.ts"() {
16652
16977
  "use strict";
16653
16978
  init_dist();
16654
- cache = /* @__PURE__ */ new Map();
16979
+ cache2 = /* @__PURE__ */ new Map();
16655
16980
  }
16656
16981
  });
16657
16982
 
16658
16983
  // src/exposure/known-hosts.ts
16659
- import fs15 from "fs";
16660
- import path17 from "path";
16984
+ import fs16 from "fs";
16985
+ import path18 from "path";
16661
16986
  function manifestRoot(dir2) {
16662
- let cur = path17.resolve(dir2);
16987
+ let cur = path18.resolve(dir2);
16663
16988
  for (let i = 0; i < 64; i++) {
16664
16989
  for (const [m] of ECOSYSTEMS) {
16665
16990
  try {
16666
- if (fs15.existsSync(path17.join(cur, m))) return cur;
16991
+ if (fs16.existsSync(path18.join(cur, m))) return cur;
16667
16992
  } catch {
16668
16993
  }
16669
16994
  }
16670
- const parent = path17.dirname(cur);
16995
+ const parent = path18.dirname(cur);
16671
16996
  if (parent === cur) return null;
16672
16997
  cur = parent;
16673
16998
  }
@@ -16676,8 +17001,8 @@ function manifestRoot(dir2) {
16676
17001
  function knownProjectHosts(dir2) {
16677
17002
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
16678
17003
  if (!dir2) return empty;
16679
- const key = path17.resolve(dir2);
16680
- const hit = cache2.get(key);
17004
+ const key = path18.resolve(dir2);
17005
+ const hit = cache3.get(key);
16681
17006
  if (hit) return hit;
16682
17007
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
16683
17008
  try {
@@ -16689,20 +17014,20 @@ function knownProjectHosts(dir2) {
16689
17014
  const root = manifestRoot(key);
16690
17015
  if (root !== null) {
16691
17016
  for (const [m, registry] of ECOSYSTEMS) {
16692
- if (fs15.existsSync(path17.join(root, m))) out.hosts.add(registry);
17017
+ if (fs16.existsSync(path18.join(root, m))) out.hosts.add(registry);
16693
17018
  }
16694
17019
  try {
16695
- if (fs15.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17020
+ if (fs16.readdirSync(root).some((f) => f.endsWith(".gemspec")))
16696
17021
  out.hosts.add("host:rubygems.org");
16697
17022
  } catch {
16698
17023
  }
16699
17024
  }
16700
17025
  } catch {
16701
17026
  }
16702
- cache2.set(key, out);
17027
+ cache3.set(key, out);
16703
17028
  return out;
16704
17029
  }
16705
- var ECOSYSTEMS, cache2;
17030
+ var ECOSYSTEMS, cache3;
16706
17031
  var init_known_hosts = __esm({
16707
17032
  "src/exposure/known-hosts.ts"() {
16708
17033
  "use strict";
@@ -16714,14 +17039,14 @@ var init_known_hosts = __esm({
16714
17039
  ["Cargo.toml", "host:crates.io"],
16715
17040
  ["Gemfile", "host:rubygems.org"]
16716
17041
  ];
16717
- cache2 = /* @__PURE__ */ new Map();
17042
+ cache3 = /* @__PURE__ */ new Map();
16718
17043
  }
16719
17044
  });
16720
17045
 
16721
17046
  // src/exposure/writes.ts
16722
- import fs16 from "fs";
16723
- import os12 from "os";
16724
- import path18 from "path";
17047
+ import fs17 from "fs";
17048
+ import os13 from "os";
17049
+ import path19 from "path";
16725
17050
  function writtenContent(args) {
16726
17051
  if (!args || typeof args !== "object" || Array.isArray(args)) return [];
16727
17052
  const a = args;
@@ -16748,7 +17073,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16748
17073
  if (typeof a.content === "string") {
16749
17074
  try {
16750
17075
  const next = JSON.parse(a.content);
16751
- const prev = JSON.parse(fs16.readFileSync(absPath, "utf8"));
17076
+ const prev = JSON.parse(fs17.readFileSync(absPath, "utf8"));
16752
17077
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
16753
17078
  } catch {
16754
17079
  return LIFECYCLE_RE.test(a.content);
@@ -16758,8 +17083,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16758
17083
  }
16759
17084
  function judgeWritesAfterExposure(opts) {
16760
17085
  const holds = [];
16761
- const home = opts.home ?? os12.homedir();
16762
- const tmp = normalizeFilePath(os12.tmpdir());
17086
+ const home = opts.home ?? os13.homedir();
17087
+ const tmp = normalizeFilePath(os13.tmpdir());
16763
17088
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
16764
17089
  const seen = /* @__PURE__ */ new Set();
16765
17090
  for (const access of opts.accesses) {
@@ -16769,7 +17094,7 @@ function judgeWritesAfterExposure(opts) {
16769
17094
  seen.add(p);
16770
17095
  if (p === "/dev" || p.startsWith("/dev/")) continue;
16771
17096
  if (root !== null && isInside(root, p)) {
16772
- if (isAgentInstructionPath(p, root) && (path18.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17097
+ if (isAgentInstructionPath(p, root) && (path19.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
16773
17098
  holds.push({ path: p, why: "agent-instruction" });
16774
17099
  continue;
16775
17100
  }
@@ -16869,6 +17194,7 @@ var init_exposure_store = __esm({
16869
17194
  tool: source.tool,
16870
17195
  server: source.server,
16871
17196
  ...source.word ? { word: source.word } : {},
17197
+ ...source.eid ? { eid: source.eid } : {},
16872
17198
  at: source.at || Date.now()
16873
17199
  });
16874
17200
  if (e.sources.length > MAX_SOURCES) e.sources.splice(0, e.sources.length - MAX_SOURCES);
@@ -16916,33 +17242,33 @@ var init_exposure_store = __esm({
16916
17242
 
16917
17243
  // src/exposure/marker.ts
16918
17244
  import crypto4 from "crypto";
16919
- import fs17 from "fs";
16920
- import os13 from "os";
16921
- import path19 from "path";
17245
+ import fs18 from "fs";
17246
+ import os14 from "os";
17247
+ import path20 from "path";
16922
17248
  function dir() {
16923
- return path19.join(os13.homedir(), ".node9", "exposure-sessions");
17249
+ return path20.join(os14.homedir(), ".node9", "exposure-sessions");
16924
17250
  }
16925
17251
  function file(sessionId) {
16926
17252
  const id = crypto4.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
16927
- return path19.join(dir(), id);
17253
+ return path20.join(dir(), id);
16928
17254
  }
16929
17255
  function markSessionExposed(sessionId) {
16930
17256
  if (!sessionId) return;
16931
17257
  try {
16932
- fs17.mkdirSync(dir(), { recursive: true, mode: 448 });
17258
+ fs18.mkdirSync(dir(), { recursive: true, mode: 448 });
16933
17259
  const f = file(sessionId);
16934
17260
  const now = /* @__PURE__ */ new Date();
16935
17261
  try {
16936
- fs17.utimesSync(f, now, now);
17262
+ fs18.utimesSync(f, now, now);
16937
17263
  } catch {
16938
- fs17.writeFileSync(f, "", { mode: 384 });
17264
+ fs18.writeFileSync(f, "", { mode: 384 });
16939
17265
  }
16940
17266
  if (Math.random() < 0.05) {
16941
17267
  const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
16942
- for (const name of fs17.readdirSync(dir())) {
17268
+ for (const name of fs18.readdirSync(dir())) {
16943
17269
  try {
16944
- const p = path19.join(dir(), name);
16945
- if (fs17.statSync(p).mtimeMs < cutoff) fs17.unlinkSync(p);
17270
+ const p = path20.join(dir(), name);
17271
+ if (fs18.statSync(p).mtimeMs < cutoff) fs18.unlinkSync(p);
16946
17272
  } catch {
16947
17273
  }
16948
17274
  }
@@ -16953,7 +17279,7 @@ function markSessionExposed(sessionId) {
16953
17279
  function maybeSessionExposed(sessionId) {
16954
17280
  if (!sessionId) return false;
16955
17281
  try {
16956
- const st = fs17.statSync(file(sessionId));
17282
+ const st = fs18.statSync(file(sessionId));
16957
17283
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
16958
17284
  } catch (e) {
16959
17285
  return e.code !== "ENOENT";
@@ -16961,8 +17287,8 @@ function maybeSessionExposed(sessionId) {
16961
17287
  }
16962
17288
  function clearSessionMarkers(sessionId) {
16963
17289
  try {
16964
- if (sessionId) fs17.unlinkSync(file(sessionId));
16965
- else fs17.rmSync(dir(), { recursive: true, force: true });
17290
+ if (sessionId) fs18.unlinkSync(file(sessionId));
17291
+ else fs18.rmSync(dir(), { recursive: true, force: true });
16966
17292
  } catch {
16967
17293
  }
16968
17294
  }
@@ -17017,9 +17343,9 @@ var init_mcp_cmd = __esm({
17017
17343
  });
17018
17344
 
17019
17345
  // src/daemon/mcp-tools.ts
17020
- import fs18 from "fs";
17021
- import path20 from "path";
17022
- import os14 from "os";
17346
+ import fs19 from "fs";
17347
+ import path21 from "path";
17348
+ import os15 from "os";
17023
17349
  function sanitizeToolAnnotations(raw) {
17024
17350
  if (!raw || typeof raw !== "object") return void 0;
17025
17351
  const a = raw;
@@ -17053,13 +17379,13 @@ function deriveServerName(cmd) {
17053
17379
  return strip(base2) || "MCP Server";
17054
17380
  }
17055
17381
  function getMcpToolsFile() {
17056
- return path20.join(os14.homedir(), ".node9", "mcp-tools.json");
17382
+ return path21.join(os15.homedir(), ".node9", "mcp-tools.json");
17057
17383
  }
17058
17384
  function readMcpToolsConfig() {
17059
17385
  try {
17060
17386
  const file2 = getMcpToolsFile();
17061
- if (!fs18.existsSync(file2)) return {};
17062
- const raw = fs18.readFileSync(file2, "utf-8");
17387
+ if (!fs19.existsSync(file2)) return {};
17388
+ const raw = fs19.readFileSync(file2, "utf-8");
17063
17389
  return JSON.parse(raw);
17064
17390
  } catch {
17065
17391
  return {};
@@ -17068,11 +17394,11 @@ function readMcpToolsConfig() {
17068
17394
  function writeMcpToolsConfig(config) {
17069
17395
  try {
17070
17396
  const file2 = getMcpToolsFile();
17071
- const dir2 = path20.dirname(file2);
17072
- if (!fs18.existsSync(dir2)) fs18.mkdirSync(dir2, { recursive: true });
17073
- const tmpPath = `${file2}.${os14.hostname()}.${process.pid}.tmp`;
17074
- fs18.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
17075
- fs18.renameSync(tmpPath, file2);
17397
+ const dir2 = path21.dirname(file2);
17398
+ if (!fs19.existsSync(dir2)) fs19.mkdirSync(dir2, { recursive: true });
17399
+ const tmpPath = `${file2}.${os15.hostname()}.${process.pid}.tmp`;
17400
+ fs19.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
17401
+ fs19.renameSync(tmpPath, file2);
17076
17402
  } catch (e) {
17077
17403
  console.error("Failed to write mcp-tools.json", e);
17078
17404
  }
@@ -17135,13 +17461,13 @@ var init_mcp_tools = __esm({
17135
17461
  });
17136
17462
 
17137
17463
  // src/exposure/roles.ts
17138
- import fs19 from "fs";
17464
+ import fs20 from "fs";
17139
17465
  function foldServerName(name) {
17140
17466
  return name.replace(/[^A-Za-z0-9_-]/g, "_").toLowerCase();
17141
17467
  }
17142
17468
  function inventoryFileStamp() {
17143
17469
  try {
17144
- const st = fs19.statSync(getMcpToolsFile());
17470
+ const st = fs20.statSync(getMcpToolsFile());
17145
17471
  return `${st.mtimeMs}:${st.size}`;
17146
17472
  } catch {
17147
17473
  return "";
@@ -17236,82 +17562,6 @@ var init_roles = __esm({
17236
17562
  }
17237
17563
  });
17238
17564
 
17239
- // src/auth/trusted-recipients.ts
17240
- import fs20 from "fs";
17241
- import path21 from "path";
17242
- import os15 from "os";
17243
- function getTrustedRecipientsPath() {
17244
- return path21.join(os15.homedir(), ".node9", "trusted-recipients.json");
17245
- }
17246
- function readTrustedRecipients() {
17247
- try {
17248
- const raw = fs20.readFileSync(getTrustedRecipientsPath(), "utf8");
17249
- const parsed = JSON.parse(raw);
17250
- return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
17251
- } catch {
17252
- return [];
17253
- }
17254
- }
17255
- function writeTrustedRecipients(entries) {
17256
- const filePath = getTrustedRecipientsPath();
17257
- fs20.mkdirSync(path21.dirname(filePath), { recursive: true });
17258
- const tmp = filePath + ".node9-tmp";
17259
- fs20.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
17260
- fs20.renameSync(tmp, filePath);
17261
- }
17262
- function normalizeRecipient(raw) {
17263
- const v = raw.trim();
17264
- if (!v) return null;
17265
- if (/^(email|host|iban):/i.test(v)) {
17266
- const i = v.indexOf(":");
17267
- const kind = v.slice(0, i).toLowerCase();
17268
- const inner = normalizeRecipient(v.slice(i + 1));
17269
- return inner && inner.startsWith(`${kind}:`) ? inner : null;
17270
- }
17271
- const found = extractArgDestinations({ to: v });
17272
- return found.length === 1 ? found[0] : null;
17273
- }
17274
- function addTrustedRecipient(raw, addedBy = "user") {
17275
- const value = normalizeRecipient(raw);
17276
- if (!value) return null;
17277
- const entries = readTrustedRecipients();
17278
- if (!entries.some((e) => e.value === value)) {
17279
- entries.push({ value, addedAt: Date.now(), addedBy });
17280
- writeTrustedRecipients(entries);
17281
- }
17282
- return value;
17283
- }
17284
- function removeTrustedRecipient(raw) {
17285
- const value = normalizeRecipient(raw) ?? raw.trim();
17286
- const entries = readTrustedRecipients();
17287
- const next = entries.filter((e) => e.value !== value);
17288
- if (next.length === entries.length) return false;
17289
- writeTrustedRecipients(next);
17290
- return true;
17291
- }
17292
- function mtimeOf() {
17293
- try {
17294
- return fs20.statSync(getTrustedRecipientsPath()).mtimeMs;
17295
- } catch {
17296
- return 0;
17297
- }
17298
- }
17299
- function isTrustedRecipient(value) {
17300
- const mtime = mtimeOf();
17301
- if (!cache3 || cache3.mtime !== mtime) {
17302
- cache3 = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
17303
- }
17304
- return cache3.values.has(value);
17305
- }
17306
- var cache3;
17307
- var init_trusted_recipients = __esm({
17308
- "src/auth/trusted-recipients.ts"() {
17309
- "use strict";
17310
- init_dist();
17311
- cache3 = null;
17312
- }
17313
- });
17314
-
17315
17565
  // src/utils/read-capped.ts
17316
17566
  import fs21 from "fs";
17317
17567
  function readCapped(file2, maxBytes) {
@@ -17379,7 +17629,8 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
17379
17629
  const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
17380
17630
  const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
17381
17631
  Object.entries(riskMetadata).filter(
17382
- ([, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v)
17632
+ ([k, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v) || // fw#318 2b: the one array the SaaS schema declares.
17633
+ k === "exposureDestinations" && Array.isArray(v) && v.length > 0
17383
17634
  )
17384
17635
  ) : void 0;
17385
17636
  const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
@@ -17459,11 +17710,15 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
17459
17710
  cwd: process.cwd(),
17460
17711
  platform: os16.platform()
17461
17712
  },
17462
- // Local-only fields (fw#318 exposureDestinations) stay off the wire:
17463
- // the SaaS validates riskMetadata strictly and would drop it whole.
17713
+ // The SaaS validates riskMetadata strictly and would drop it whole on
17714
+ // an unknown key, so only fields its schema declares go on the wire.
17715
+ // exposureDestinations ships since the dashboard's 2b contract
17716
+ // (fw#318): "Approve and trust" needs them.
17464
17717
  ...riskMetadata && {
17465
17718
  riskMetadata: Object.fromEntries(
17466
- Object.entries(riskMetadata).filter(([, v]) => !Array.isArray(v))
17719
+ Object.entries(riskMetadata).filter(
17720
+ ([k, v]) => !Array.isArray(v) || k === "exposureDestinations"
17721
+ )
17467
17722
  )
17468
17723
  },
17469
17724
  ...ciContext && { ciContext },
@@ -18323,7 +18578,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18323
18578
  let fileAccess;
18324
18579
  let fileFinding;
18325
18580
  try {
18326
- fileAccess = prepareFileAccesses(toolName, args, options?.cwd, config.policy.toolInspection);
18581
+ fileAccess = prepareFileAccesses(toolName, args, options?.cwd, inspectionLayers(config.policy));
18327
18582
  fileFinding = isManual ? null : evaluateProtectedFileAccess(
18328
18583
  fileAccess.accesses,
18329
18584
  fileAccess,
@@ -18342,7 +18597,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18342
18597
  fileAccess = { cwd: options?.cwd, homeDirs: [], accesses: [], handledPaths: /* @__PURE__ */ new Set() };
18343
18598
  fileFinding = isManual ? null : incompleteFileVerdict();
18344
18599
  }
18345
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
18600
+ const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
18346
18601
  const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
18347
18602
  const shellReview = !isManual && shellInput.kind === "uninspectable";
18348
18603
  if (shellReview) {
@@ -18452,6 +18707,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18452
18707
  let exposureReview = null;
18453
18708
  let exposureKeys = [];
18454
18709
  let exposureDestinations = [];
18710
+ let exposureFromEid;
18455
18711
  const ucPolicy = config.policy.untrustedContent;
18456
18712
  if (ucPolicy?.enabled && !taintWarning && meta?.sessionId && !isManual) {
18457
18713
  const role = toolRole(toolName, ucPolicy);
@@ -18459,7 +18715,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18459
18715
  const shellExp = shellInput.kind === "shell" ? classifyShellExposure(shellInput.command) : null;
18460
18716
  const bareName = splitMcpToolName(toolName)?.bare;
18461
18717
  const mcpFileWrite = bareName !== void 0 && fileToolOperation(bareName, args) === "write";
18462
- const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, config.policy.toolInspection).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
18718
+ const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, inspectionLayers(config.policy)).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
18463
18719
  const sendsSomething = acts && !mcpFileWrite || !!shellExp?.sends;
18464
18720
  const mayBeExposed = daemonSaysExposed ?? (maybeSessionExposed(meta.sessionId) || writes.length > 0 && judgeWritesAfterExposure({
18465
18721
  accesses: writes,
@@ -18503,7 +18759,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18503
18759
  const isKnown = (d) => {
18504
18760
  const domain = destinationDomain(d);
18505
18761
  const value = destinationValue(d);
18506
- return domain !== null && ucPolicy.orgDomains.includes(domain) || mentioned.has(d) || isTrustedRecipient(d) || destinationKind(d) === "host" && (isLoopbackHost(value) || isTrustedHost(value) || config.policy.trustedHostsManaged && matchesTrustedHost(value, config.policy.trustedHosts) || config.policy.egress.allow.some((pat) => hostMatches(value, pat)) || writeProjectHosts.hosts.has(d));
18762
+ return domain !== null && ucPolicy.orgDomains.includes(domain) || mentioned.has(d) || isTrustedRecipient(d) || ucPolicy.trustedRecipients.includes(d) || destinationKind(d) === "host" && (isLoopbackHost(value) || isTrustedHost(value) || config.policy.trustedHostsManaged && matchesTrustedHost(value, config.policy.trustedHosts) || config.policy.egress.allow.some((pat) => hostMatches(value, pat)) || writeProjectHosts.hosts.has(d));
18507
18763
  };
18508
18764
  const approvedOnce = (key) => ucPolicy.afterExposure === "ask-once" && approved.has(key);
18509
18765
  const repos = (shellExp?.sends ? shellExp.repos : []).filter(
@@ -18567,6 +18823,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18567
18823
  );
18568
18824
  }
18569
18825
  exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
18826
+ exposureFromEid = last?.eid;
18827
+ if (exposureFromEid) meta = { ...meta, exposureFromEid };
18570
18828
  exposureKeys = [
18571
18829
  ...held.filter((h) => h.dest || h.why === "unresolved" || h.why === "repository").map(
18572
18830
  (h) => h.why === "repository" ? `${toolName}|repo:${h.repo}` : `${toolName}|${h.dest || "unresolved"}`
@@ -18580,6 +18838,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18580
18838
  exposureReview += ` To trust ${exposureDestinations.length === 1 ? "this address" : "these addresses"} for good, the user runs: ` + exposureDestinations.map((d) => `node9 trust recipient add ${destinationValue(d)}`).join("; ") + ".";
18581
18839
  }
18582
18840
  } else if (held.length > 0 || writeHolds.length > 0 || logged.length > 0) {
18841
+ meta = {
18842
+ ...meta,
18843
+ exposure: { kind: "logged", destinations: logged.length },
18844
+ ...last?.eid ? { exposureFromEid: last.eid } : {}
18845
+ };
18583
18846
  appendToLog(HOOK_DEBUG_LOG, {
18584
18847
  ts: (/* @__PURE__ */ new Date()).toISOString(),
18585
18848
  event: "exposure-logged",
@@ -19133,6 +19396,7 @@ ${packageReview}`;
19133
19396
  // deferReview and serverKey never co-occur in production — see above.)
19134
19397
  reason: taintWarning || packageReview || exposureReview || explainableLabel || "Node9 flagged this action for review.",
19135
19398
  ...exposureReview && exposureKeys.length > 0 ? { exposureKeys } : {},
19399
+ ...exposureReview && exposureFromEid ? { exposureFromEid } : {},
19136
19400
  ruleDescription: policyRuleDescription,
19137
19401
  blockedByLabel: explainableLabel
19138
19402
  };
@@ -30896,7 +31160,7 @@ var init_ship = __esm({
30896
31160
  });
30897
31161
 
30898
31162
  // src/policy-snapshot/build.ts
30899
- function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = [], syncHealth, identity) {
31163
+ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = [], syncHealth, identity, trustedRecipients = []) {
30900
31164
  const p = config.policy;
30901
31165
  const statusByKey = /* @__PURE__ */ new Map();
30902
31166
  for (const s of statusEntries) {
@@ -30906,7 +31170,7 @@ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, st
30906
31170
  const connectedRows = Object.entries(mcpTools).slice(0, MAX_MCP_SERVERS).map(([key, cfg]) => ({
30907
31171
  key,
30908
31172
  ...cfg.name && { name: cfg.name },
30909
- tools: cfg.tools.map((t) => t.name).slice(0, MAX_MCP_TOOLS),
31173
+ tools: cfg.tools.slice(0, MAX_MCP_TOOLS).map((t) => ({ name: t.name, ...t.annotations ? { annotations: t.annotations } : {} })),
30910
31174
  toolCount: cfg.tools.length,
30911
31175
  status: cfg.status,
30912
31176
  // In mcp-tools.json ⇒ it launched ⇒ connected, unless the resolver says stale.
@@ -30956,6 +31220,15 @@ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, st
30956
31220
  engineVersion: ENGINE_VERSION,
30957
31221
  // Connected rows first so they win the cap; non-connected SEE rows fill the rest.
30958
31222
  mcpServers: [...connectedRows, ...extraRows].slice(0, MAX_MCP_SERVERS),
31223
+ // fw#318 2b: the developer's personal trusted recipients (A1: read-only
31224
+ // on the dashboard). Omitted when empty, so an old SaaS sees no change.
31225
+ ...trustedRecipients.length > 0 && {
31226
+ trustedRecipients: trustedRecipients.slice(0, 200).map((e) => ({
31227
+ value: e.value,
31228
+ addedBy: e.addedBy,
31229
+ ...e.addedAt ? { addedAt: e.addedAt } : {}
31230
+ }))
31231
+ },
30959
31232
  // fleet-ship Step 2: ship the proxy's own sync health so the dashboard can
30960
31233
  // derive a "sync failing" badge. Only included when the caller passes it.
30961
31234
  ...syncHealth && {
@@ -31476,6 +31749,10 @@ function extractManagedConfig(body) {
31476
31749
  out3.afterExposure = u.afterExposure;
31477
31750
  if (Array.isArray(u.orgDomains))
31478
31751
  out3.orgDomains = u.orgDomains.filter((x) => typeof x === "string");
31752
+ if (Array.isArray(u.trustedRecipients))
31753
+ out3.trustedRecipients = u.trustedRecipients.filter(
31754
+ (x) => typeof x === "string"
31755
+ );
31479
31756
  if (u.roles && typeof u.roles === "object" && !Array.isArray(u.roles)) {
31480
31757
  const roles = {};
31481
31758
  for (const [server, tools] of Object.entries(u.roles)) {
@@ -31652,6 +31929,17 @@ async function pushPostureSnapshot(creds) {
31652
31929
  } catch {
31653
31930
  }
31654
31931
  }
31932
+ function personalTrustedRecipients() {
31933
+ try {
31934
+ return readTrustedRecipients().map((e) => ({
31935
+ value: e.value,
31936
+ addedBy: e.addedBy === "approval" ? "approval" : "user",
31937
+ ...Number.isFinite(e.addedAt) ? { addedAt: new Date(e.addedAt).toISOString() } : {}
31938
+ }));
31939
+ } catch {
31940
+ return [];
31941
+ }
31942
+ }
31655
31943
  async function pushPolicySnapshot(creds) {
31656
31944
  try {
31657
31945
  const cfg = getConfig();
@@ -31674,7 +31962,9 @@ async function pushPolicySnapshot(creds) {
31674
31962
  readSyncHealth(),
31675
31963
  // Machine identity — heals legacy machineId-less keys on the SaaS side
31676
31964
  // (login-v2 duplicate-key fix). getMachineId mints on first need.
31677
- { machineId: getMachineId(), platform: process.platform }
31965
+ { machineId: getMachineId(), platform: process.platform },
31966
+ // fw#318 2b: the developer's personal trusted recipients.
31967
+ personalTrustedRecipients()
31678
31968
  );
31679
31969
  await shipPolicySnapshot(body, creds);
31680
31970
  } catch {
@@ -31705,7 +31995,9 @@ async function runPolicyPush() {
31705
31995
  // (daemon here, user shell in runPolicyPush); see the P2 design's two-env note.
31706
31996
  resolveMcpStatus(),
31707
31997
  readSyncHealth(),
31708
- { machineId: getMachineId(), platform: process.platform }
31998
+ { machineId: getMachineId(), platform: process.platform },
31999
+ // fw#318 2b: the developer's personal trusted recipients.
32000
+ personalTrustedRecipients()
31709
32001
  );
31710
32002
  const sent = await shipPolicySnapshot(body, creds);
31711
32003
  return sent ? { ok: true } : { ok: false, reason: "Push failed (network or server error)" };
@@ -31883,6 +32175,7 @@ var init_sync = __esm({
31883
32175
  init_ship();
31884
32176
  init_build2();
31885
32177
  init_mcp_tools();
32178
+ init_trusted_recipients();
31886
32179
  init_state2();
31887
32180
  init_mcp_status();
31888
32181
  init_ship2();
@@ -31939,6 +32232,13 @@ import fs56 from "fs";
31939
32232
  import path55 from "path";
31940
32233
  import os47 from "os";
31941
32234
  import crypto7 from "crypto";
32235
+ function exposureOf(v) {
32236
+ if (!v || typeof v !== "object") return null;
32237
+ const o = v;
32238
+ if (o.kind !== "source" && o.kind !== "logged") return null;
32239
+ const n = typeof o.destinations === "number" && Number.isFinite(o.destinations) ? o.destinations : 0;
32240
+ return { kind: o.kind, destinations: Math.max(0, Math.min(1e3, Math.floor(n))) };
32241
+ }
31942
32242
  function fileSignature(filePath) {
31943
32243
  const fd = fs56.openSync(filePath, "r");
31944
32244
  try {
@@ -32014,7 +32314,9 @@ function buildWireRows(chunk2) {
32014
32314
  ...typeof parsed.loopCount === "number" ? { loopCount: parsed.loopCount } : {},
32015
32315
  ...typeof parsed.transcriptPath === "string" ? { transcriptPath: parsed.transcriptPath } : {},
32016
32316
  ...typeof parsed.taintFromEid === "string" ? { taintFromEid: parsed.taintFromEid } : {},
32017
- ...typeof parsed.taintSource === "string" ? { taintSource: parsed.taintSource } : {}
32317
+ ...typeof parsed.taintSource === "string" ? { taintSource: parsed.taintSource } : {},
32318
+ ...exposureOf(parsed.exposure) ? { exposure: exposureOf(parsed.exposure) } : {},
32319
+ ...typeof parsed.exposureFromEid === "string" ? { exposureFromEid: parsed.exposureFromEid } : {}
32018
32320
  });
32019
32321
  }
32020
32322
  return { rows, consumed: lastNl + 1 };
@@ -34474,6 +34776,7 @@ data: ${JSON.stringify(item.data)}
34474
34776
  tool: body.source.tool.slice(0, 200),
34475
34777
  server: typeof body.source.server === "string" ? body.source.server.slice(0, 80) : void 0,
34476
34778
  word: typeof body.source.word === "string" ? body.source.word.slice(0, 40) : void 0,
34779
+ eid: typeof body.source.eid === "string" ? body.source.eid.slice(0, 64) : void 0,
34477
34780
  at: Date.now()
34478
34781
  },
34479
34782
  strings(body.destinations)
@@ -64301,7 +64604,8 @@ RAW: ${raw}
64301
64604
  sessionId: sid,
64302
64605
  ts: Date.now(),
64303
64606
  label: result2.blockedByLabel,
64304
- ...result2.exposureKeys?.length ? { exposureKeys: result2.exposureKeys } : {}
64607
+ ...result2.exposureKeys?.length ? { exposureKeys: result2.exposureKeys } : {},
64608
+ ...result2.exposureFromEid ? { exposureFromEid: result2.exposureFromEid } : {}
64305
64609
  });
64306
64610
  }
64307
64611
  } catch {
@@ -64764,9 +65068,14 @@ function buildArgsField(rawInput) {
64764
65068
  const hit = scanArgs(rawInput);
64765
65069
  if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
64766
65070
  if (!scanCoveredEverything(rawInput)) return { ...hashed, argsUnscanned: true };
64767
- const gateHit = inspectArgumentSecrets(rawInput).match;
64768
- if (gateHit)
64769
- return { ...hashed, dlpPattern: gateHit.patternName, dlpSample: gateHit.redactedSample };
65071
+ const inspected = inspectArgumentSecrets(rawInput);
65072
+ if (inspected.match)
65073
+ return {
65074
+ ...hashed,
65075
+ dlpPattern: inspected.match.patternName,
65076
+ dlpSample: inspected.match.redactedSample
65077
+ };
65078
+ if (inspected.coverage === "incomplete") return { ...hashed, argsUnscanned: true };
64770
65079
  return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
64771
65080
  } catch {
64772
65081
  return hashed;
@@ -64877,6 +65186,7 @@ function registerLogCommand(program2) {
64877
65186
  {
64878
65187
  agent,
64879
65188
  ruleName: reviewLabel,
65189
+ ...resolvedReview.exposureFromEid ? { exposureFromEid: resolvedReview.exposureFromEid } : {},
64880
65190
  ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
64881
65191
  ...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
64882
65192
  ...safeCwd ? { workingDir: safeCwd } : {}
@@ -64914,17 +65224,21 @@ function registerLogCommand(program2) {
64914
65224
  if (redactOutputMode) {
64915
65225
  const { result, found } = redactText(toolOutput);
64916
65226
  let out = result;
65227
+ const ucA = config.policy.untrustedContent;
65228
+ const shellA = ucA?.enabled ? shellFetch(tool, rawInput) : null;
65229
+ const sourceA = !!ucA?.enabled && (toolRole(tool, ucA).source || !!shellA?.fetches);
64917
65230
  let injection = null;
64918
65231
  if (injectionOn) {
64919
- const m = scanInjection(result, { tool: rawToolName });
65232
+ const m = scanInjection(result, {
65233
+ tool: rawToolName,
65234
+ ...sourceA ? { untrusted: true } : {}
65235
+ });
64920
65236
  if (m && atLeastConfidence(m.confidence, inj.minConfidence)) {
64921
65237
  injection = m;
64922
65238
  out = frameUntrusted(result);
64923
65239
  }
64924
65240
  }
64925
- const ucA = config.policy.untrustedContent;
64926
- const shellA = ucA?.enabled ? shellFetch(tool, rawInput) : null;
64927
- if (ucA?.enabled && (toolRole(tool, ucA).source || shellA?.fetches)) {
65241
+ if (sourceA) {
64928
65242
  const wordA = shellA?.fetches ? shellSourceWord(shellA.fetchKinds) : sourceWord(tool);
64929
65243
  markSessionExposed(payloadSessionId ?? "");
64930
65244
  await notifySessionExposure(
@@ -64966,15 +65280,16 @@ function registerLogCommand(program2) {
64966
65280
  );
64967
65281
  }
64968
65282
  if (injectionOn) {
64969
- const m = scanInjection(toolOutput, { tool: rawToolName });
65283
+ const injCtx = { tool: rawToolName, ...sourceRead ? { untrusted: true } : {} };
65284
+ const m = scanInjection(toolOutput, injCtx);
64970
65285
  if (m && atLeastConfidence(m.confidence, inj.minConfidence)) {
64971
65286
  if (spotlightApplies) {
64972
- injectedLeaf = (leaf) => scanInjection(leaf, { tool: rawToolName }) !== null;
65287
+ injectedLeaf = (leaf) => scanInjection(leaf, injCtx) !== null;
64973
65288
  }
64974
65289
  if (agent === "Claude Code" && !spotlightApplies) {
64975
65290
  updatedToolOutput = frameToolOutputLeaves(
64976
65291
  updatedToolOutput ?? payload.tool_response,
64977
- (leaf) => scanInjection(leaf, { tool: rawToolName }) !== null,
65292
+ (leaf) => scanInjection(leaf, injCtx) !== null,
64978
65293
  newUntrustedFrame,
64979
65294
  neutralizeMarkers
64980
65295
  );
@@ -64991,22 +65306,29 @@ function registerLogCommand(program2) {
64991
65306
  if (sourceRead) {
64992
65307
  const destinations = extractContentDestinations(toolOutput);
64993
65308
  markSessionExposed(payloadSessionId ?? "");
65309
+ const sourceEid = appendLocalAudit(
65310
+ tool,
65311
+ rawInput,
65312
+ "allow",
65313
+ "exposure-source",
65314
+ {
65315
+ agent,
65316
+ ...payloadSessionId ? { sessionId: payloadSessionId } : {},
65317
+ ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
65318
+ exposure: { kind: "source", destinations: destinations.length }
65319
+ },
65320
+ config.settings.auditHashArgs === true
65321
+ );
64994
65322
  await notifySessionExposure(
64995
65323
  payloadSessionId ?? "",
64996
65324
  {
64997
65325
  tool,
64998
65326
  server: splitMcpToolName(tool)?.server,
64999
- ...shellExposure?.fetches ? { word: shellSourceWord(shellExposure.fetchKinds) } : {}
65327
+ ...shellExposure?.fetches ? { word: shellSourceWord(shellExposure.fetchKinds) } : {},
65328
+ eid: sourceEid
65000
65329
  },
65001
65330
  destinations
65002
65331
  );
65003
- appendToLog(LOCAL_AUDIT_LOG, {
65004
- ts: (/* @__PURE__ */ new Date()).toISOString(),
65005
- tool,
65006
- source: "exposure-source",
65007
- destinations: destinations.length,
65008
- ...payloadSessionId ? { sessionId: payloadSessionId } : {}
65009
- });
65010
65332
  if (spotlightApplies) {
65011
65333
  const word = shellExposure?.fetches ? shellSourceWord(shellExposure.fetchKinds) : sourceWord(tool);
65012
65334
  if (agent === "Claude Code") {
@@ -66536,7 +66858,9 @@ function dimensionOfBlock(checkedBy, ruleName) {
66536
66858
  if (checkedBy.includes("egress")) return "network";
66537
66859
  if (ruleName.startsWith("ssrf:")) return "network";
66538
66860
  if (checkedBy.includes("pii") || checkedBy.includes("dlp")) return "data";
66539
- if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection"))
66861
+ if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection") || // fw#318: a send held after the session read outside content, and the
66862
+ // read rows themselves. Mirrors node9Firewall canon-taxonomy.ts.
66863
+ ruleName.startsWith("exposure") || checkedBy.startsWith("exposure"))
66540
66864
  return "detection";
66541
66865
  if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "apps";
66542
66866
  if (ruleName.startsWith("shield:project-jail")) return "files";
@@ -67860,7 +68184,10 @@ function scanToolResult(line, parsed, tool, cfg) {
67860
68184
  let injection = null;
67861
68185
  const inj = cfg.injection;
67862
68186
  if (inj.enabled && !inj.allow.includes(tool) && texts.length > 0) {
67863
- const m = scanInjection(texts.join("\n"), { tool });
68187
+ const m = scanInjection(texts.join("\n"), {
68188
+ tool,
68189
+ ...cfg.untrusted ? { untrusted: true } : {}
68190
+ });
67864
68191
  if (m && CONFIDENCE_RANK2[m.confidence] >= CONFIDENCE_RANK2[inj.minConfidence]) {
67865
68192
  injection = m;
67866
68193
  mutated = true;
@@ -68376,7 +68703,8 @@ async function runMcpGateway(upstreamCommand, configName) {
68376
68703
  const scan = scanToolResult(line, parsed, exec.toolName, {
68377
68704
  dlpEnabled: cfg.policy.dlp.enabled,
68378
68705
  injection: cfg.policy.injectionScan,
68379
- spotlight: isSource && uc.spotlight ? sourceWord(nsName) : void 0
68706
+ spotlight: isSource && uc.spotlight ? sourceWord(nsName) : void 0,
68707
+ untrusted: isSource
68380
68708
  });
68381
68709
  if (scan.changed) outLine = scan.line;
68382
68710
  if (isSource) {