@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.js CHANGED
@@ -2168,32 +2168,109 @@ function evaluateSmartConditions(args, rule) {
2168
2168
  function isBashTool(tool) {
2169
2169
  return BASH_TOOL_NAMES.has(tool.toLowerCase());
2170
2170
  }
2171
- function inspectionEntry(tool, inspection = {}) {
2171
+ function inspectionLayers(policy) {
2172
+ return { trusted: policy.toolInspection, project: policy.projectToolInspection };
2173
+ }
2174
+ function isShellEntry(value) {
2175
+ return typeof value === "object" && value !== null && value.kind === "shell" && typeof value.field === "string" && Object.keys(value).length === 2;
2176
+ }
2177
+ function isInspectionLayers(input) {
2178
+ const trusted = input.trusted;
2179
+ return typeof trusted === "object" && trusted !== null && !isShellEntry(trusted);
2180
+ }
2181
+ function layersOf(input) {
2182
+ return isInspectionLayers(input) ? { trusted: input.trusted, project: input.project ?? {} } : { trusted: input, project: {} };
2183
+ }
2184
+ function entryOf(tool, inspection) {
2172
2185
  const pattern = Object.keys(inspection).find((p) => matchesPattern(tool, p));
2173
2186
  return pattern === void 0 ? void 0 : inspection[pattern];
2174
2187
  }
2188
+ function inspectionEntry(tool, inspection = {}) {
2189
+ const { trusted, project } = layersOf(inspection);
2190
+ return entryOf(tool, trusted) ?? entryOf(tool, project);
2191
+ }
2192
+ function executorName(tool) {
2193
+ const name = tool.toLowerCase();
2194
+ if (!name.startsWith("mcp__")) return name;
2195
+ const at = name.lastIndexOf("__");
2196
+ return at >= "mcp__".length ? name.slice(at + 2) : name;
2197
+ }
2198
+ function entryFields(entry) {
2199
+ if (typeof entry === "object") return [entry.field];
2200
+ return entry === "command" ? ["command"] : null;
2201
+ }
2175
2202
  function shellFields(tool, inspection) {
2176
2203
  if (isBashTool(tool)) return ["command"];
2177
- const entry = inspectionEntry(tool, inspection);
2178
- if (typeof entry === "object") return [entry.field];
2179
- if (entry !== void 0) return entry === "command" ? [entry] : null;
2180
- const name = tool.toLowerCase().replace(/^mcp__[a-z0-9_-]+?__/, "");
2181
- return MCP_EXECUTORS.has(name) ? ["command", "cmd", "script"] : null;
2204
+ const { trusted, project } = layersOf(inspection);
2205
+ const trustedEntry = entryOf(tool, trusted);
2206
+ if (trustedEntry !== void 0) return entryFields(trustedEntry);
2207
+ const builtin = MCP_EXECUTORS.has(executorName(tool)) ? BUILTIN_FIELDS : null;
2208
+ const projectEntry = entryOf(tool, project);
2209
+ if (projectEntry === void 0) return builtin;
2210
+ const added = entryFields(projectEntry);
2211
+ if (!builtin) return added;
2212
+ if (!added) return builtin;
2213
+ return [.../* @__PURE__ */ new Set([...builtin, ...added])];
2182
2214
  }
2183
2215
  function isShellShapedTool(tool, inspection = {}) {
2184
2216
  return shellFields(tool, inspection) !== null;
2185
2217
  }
2218
+ function shellWord(word) {
2219
+ if (BARE_WORD.test(word)) return word;
2220
+ return word === "" ? "''" : `'${word.replace(/'/g, `'\\''`)}'`;
2221
+ }
2222
+ function isStringArray(value) {
2223
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
2224
+ }
2225
+ function commandLine(args, fieldPath, value, native) {
2226
+ const missing = { reason: "Shell command field is missing or is not a string" };
2227
+ const unreadable = { reason: "Shell command arguments could not be read" };
2228
+ if (typeof value !== "string" && (native || !Array.isArray(value))) return missing;
2229
+ const dot = fieldPath.lastIndexOf(".");
2230
+ const parent = dot === -1 ? args : getNestedValue(args, fieldPath.slice(0, dot));
2231
+ const siblings = ARGV_FIELDS.map((key) => ({
2232
+ key,
2233
+ value: parent && typeof parent === "object" ? parent[key] : void 0
2234
+ })).filter(({ value: value2 }) => value2 !== void 0 && value2 !== null);
2235
+ let argv = siblings[0];
2236
+ if (native) argv = siblings.find(({ value: v }) => isStringArray(v));
2237
+ else if (siblings.length === 2 && JSON.stringify(siblings[0].value) !== JSON.stringify(siblings[1].value))
2238
+ return { reason: "Shell command arguments disagree" };
2239
+ let command;
2240
+ let source = fieldPath;
2241
+ if (Array.isArray(value)) {
2242
+ if (argv || !value.length || !isStringArray(value)) return unreadable;
2243
+ command = value.map(shellWord).join(" ");
2244
+ } else if (!argv) {
2245
+ return { command: value, fieldPath };
2246
+ } else {
2247
+ if (!isStringArray(argv.value)) return unreadable;
2248
+ if (!argv.value.length) return { command: value, fieldPath };
2249
+ command = `${value} ${argv.value.map(shellWord).join(" ")}`;
2250
+ source = `${fieldPath}+${argv.key}`;
2251
+ }
2252
+ if (command.length > MAX_ARGV_CHARACTERS)
2253
+ return { reason: "Shell command arguments exceed the inspectable size" };
2254
+ return { command, fieldPath: source };
2255
+ }
2186
2256
  function resolveShellInput(tool, args, inspection = {}) {
2187
2257
  const fields = shellFields(tool, inspection);
2188
2258
  if (!fields) return { kind: "not-shell" };
2189
2259
  const values = fields.map((fieldPath) => ({ fieldPath, value: getNestedValue(args, fieldPath) })).filter(({ value }) => value !== void 0 && value !== null && value !== "");
2190
- if (!values.length || values.some(({ value }) => typeof value !== "string")) {
2260
+ if (!values.length) {
2191
2261
  return { kind: "uninspectable", reason: "Shell command field is missing or is not a string" };
2192
2262
  }
2193
- if (new Set(values.map(({ value }) => value)).size !== 1) {
2263
+ const native = isBashTool(tool);
2264
+ const lines = values.map(({ fieldPath, value }) => commandLine(args, fieldPath, value, native));
2265
+ const failed = lines.find((line) => "reason" in line);
2266
+ if (failed) return { kind: "uninspectable", reason: failed.reason };
2267
+ const commands = lines.filter(
2268
+ (line) => "command" in line
2269
+ );
2270
+ if (new Set(commands.map(({ command }) => command)).size !== 1) {
2194
2271
  return { kind: "uninspectable", reason: "Multiple shell command fields disagree" };
2195
2272
  }
2196
- return { kind: "shell", command: values[0].value, fieldPath: values[0].fieldPath };
2273
+ return { kind: "shell", ...commands[0] };
2197
2274
  }
2198
2275
  function shellRuleArgs(tool, args, ruleTool, inspection = {}) {
2199
2276
  if (!ruleTool || !matchesPattern("bash", ruleTool) || matchesPattern(tool, ruleTool)) return args;
@@ -2934,20 +3011,25 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2934
3011
  }
2935
3012
  return best;
2936
3013
  }
2937
- function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ new Set() }) {
2938
- const words = [...text.matchAll(/[a-zA-Z]+/g)];
3014
+ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new Set() }) {
2939
3015
  let run = [];
3016
+ let runIndex = [];
2940
3017
  let previousEnd = 0;
2941
3018
  const rejected = /* @__PURE__ */ new Set();
2942
3019
  const inspect = () => {
3020
+ if (run.length < 12) return false;
3021
+ const ascending = new Array(run.length).fill(0);
3022
+ for (let i = 1; i < run.length; i++)
3023
+ ascending[i] = ascending[i - 1] + (runIndex[i] > runIndex[i - 1] ? 1 : 0);
2943
3024
  for (let start = 0; start + 12 <= run.length; start++) {
2944
3025
  for (const count of lengths) {
2945
3026
  if (start + count > run.length) continue;
3027
+ if (ascending[start + count - 1] - ascending[start] === count - 1) continue;
2946
3028
  const candidate = run.slice(start, start + count);
2947
3029
  const sentence2 = candidate.join(" ");
2948
3030
  if (rejected.has(sentence2)) continue;
2949
- if (++budget.candidates > 1024) {
2950
- budget.reasons.add("mnemonic-candidates");
3031
+ if (++budget.hashes > MAX_MNEMONIC_HASHES) {
3032
+ budget.reasons.add("mnemonic-budget");
2951
3033
  return false;
2952
3034
  }
2953
3035
  if (BIP39_PUBLIC_VECTORS.has(hash(sentence2).toString("hex"))) {
@@ -2965,13 +3047,18 @@ function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ n
2965
3047
  }
2966
3048
  return false;
2967
3049
  };
2968
- for (const word of words) {
2969
- if (!indices.has(word[0]) || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
3050
+ for (const word of text.matchAll(/[a-zA-Z]+/g)) {
3051
+ const index = indices.get(word[0]);
3052
+ if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
2970
3053
  if (inspect()) return true;
2971
- if (budget.reasons.has("mnemonic-candidates")) return false;
3054
+ if (budget.reasons.has("mnemonic-budget")) return false;
2972
3055
  run = [];
3056
+ runIndex = [];
3057
+ }
3058
+ if (index !== void 0) {
3059
+ run.push(word[0]);
3060
+ runIndex.push(index);
2973
3061
  }
2974
- if (indices.has(word[0])) run.push(word[0]);
2975
3062
  previousEnd = word.index + word[0].length;
2976
3063
  }
2977
3064
  return inspect();
@@ -3102,6 +3189,9 @@ function validateCask(raw) {
3102
3189
  const second = b64urlIndex(s[tsAt + 5]);
3103
3190
  return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
3104
3191
  }
3192
+ function isInvisibleChar(char) {
3193
+ return INVISIBLE_ONE.test(char);
3194
+ }
3105
3195
  function stripInvisible(text) {
3106
3196
  return text.replace(INVISIBLE_RE, "");
3107
3197
  }
@@ -3167,7 +3257,7 @@ function scanInjection(text, ctx = {}) {
3167
3257
  }
3168
3258
  }
3169
3259
  if (matched.length === 0) return null;
3170
- const untrusted = !!ctx.tool && UNTRUSTED_TOOLS.test(ctx.tool);
3260
+ const untrusted = ctx.untrusted ?? (!!ctx.tool && UNTRUSTED_TOOLS.test(ctx.tool));
3171
3261
  const score = matched.length + (untrusted ? 1 : 0);
3172
3262
  const confidence = score >= 3 ? "high" : score === 2 ? "medium" : "low";
3173
3263
  return { signals: untrusted ? [...matched, "untrusted-origin"] : matched, confidence };
@@ -3355,6 +3445,45 @@ function matchArgumentText(text, fieldPath, budget) {
3355
3445
  }
3356
3446
  return best;
3357
3447
  }
3448
+ function matchArgumentBatch(fragments, fieldPath, budget) {
3449
+ if (!fragments.length) return null;
3450
+ const text = fragments.join(BATCH_SEPARATOR);
3451
+ const lower2 = text.toLowerCase();
3452
+ let best = null;
3453
+ for (const pattern of DLP_PATTERNS) {
3454
+ if (pattern.keywords && !pattern.keywords.some((kw) => lower2.includes(kw.toLowerCase())))
3455
+ continue;
3456
+ const flags = pattern.regex.flags.replace(/[gmy]/g, "");
3457
+ if (!new RegExp(pattern.regex.source, flags + "m").test(text)) continue;
3458
+ for (const fragment of fragments) {
3459
+ if (firstAcceptedMatch(pattern, fragment) === null) continue;
3460
+ const severity = pattern.contextBoost && isAssignmentContext(fragment) ? "block" : pattern.severity;
3461
+ if (severity === "block")
3462
+ return {
3463
+ patternName: pattern.name,
3464
+ fieldPath,
3465
+ redactedSample: "[encoded secret]",
3466
+ severity
3467
+ };
3468
+ best ??= {
3469
+ patternName: pattern.name,
3470
+ fieldPath,
3471
+ redactedSample: "[encoded secret]",
3472
+ severity
3473
+ };
3474
+ if (!pattern.contextBoost) break;
3475
+ }
3476
+ }
3477
+ if (findMnemonic(text, budget)) {
3478
+ return {
3479
+ patternName: "BIP-39 Recovery Phrase",
3480
+ fieldPath,
3481
+ redactedSample: "[recovery phrase]",
3482
+ severity: "block"
3483
+ };
3484
+ }
3485
+ return best;
3486
+ }
3358
3487
  function scanArgs(args, depth = 0, fieldPath = "args") {
3359
3488
  if (depth > MAX_DEPTH2 || args === null || args === void 0) return null;
3360
3489
  if (Array.isArray(args)) {
@@ -4323,6 +4452,9 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
4323
4452
  return null;
4324
4453
  }
4325
4454
  }
4455
+ function newSecretViewBudget() {
4456
+ return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
4457
+ }
4326
4458
  function textBytes(buf) {
4327
4459
  if (buf.length < 16) return null;
4328
4460
  const text = buf.toString("utf8");
@@ -4341,13 +4473,41 @@ function percentOnce(text) {
4341
4473
  }
4342
4474
  });
4343
4475
  }
4476
+ function* wrappedBase64(text) {
4477
+ if (!text.includes("\n")) return;
4478
+ const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
4479
+ let run = [];
4480
+ let width = 0;
4481
+ const flush = function* () {
4482
+ if (run.length >= 2) yield run.join("");
4483
+ run = [];
4484
+ width = 0;
4485
+ };
4486
+ for (const raw of text.split("\n")) {
4487
+ const line = raw.trim();
4488
+ const full = line.length >= 16 && word.test(line) && !line.includes("=");
4489
+ if (full && (!run.length || line.length === width)) {
4490
+ run.push(line);
4491
+ width = line.length;
4492
+ continue;
4493
+ }
4494
+ if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
4495
+ run.push(line);
4496
+ yield* flush();
4497
+ continue;
4498
+ }
4499
+ yield* flush();
4500
+ if (full) {
4501
+ run.push(line);
4502
+ width = line.length;
4503
+ }
4504
+ }
4505
+ yield* flush();
4506
+ }
4344
4507
  function* encodedRegions(text, budget) {
4345
4508
  yield [text, false];
4509
+ for (const block of wrappedBase64(text)) yield [block, false];
4346
4510
  for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
4347
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4348
- budget.reasons.add("decode-candidates");
4349
- return;
4350
- }
4351
4511
  try {
4352
4512
  new URL(match[0]);
4353
4513
  } catch {
@@ -4367,6 +4527,8 @@ function* encodedRegions(text, budget) {
4367
4527
  ];
4368
4528
  for (const region of regions) if (region) yield [region, false];
4369
4529
  const labels = host2.split(".");
4530
+ 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))
4531
+ continue;
4370
4532
  for (let start = 0; start < labels.length; start++) {
4371
4533
  let joined = labels[start];
4372
4534
  for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
@@ -4374,11 +4536,13 @@ function* encodedRegions(text, budget) {
4374
4536
  if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
4375
4537
  break;
4376
4538
  joined += labels[end];
4377
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4378
- budget.reasons.add("decode-candidates");
4539
+ if (joined.length < 16) continue;
4540
+ budget.joinedLabelBytes += joined.length;
4541
+ if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
4542
+ budget.reasons.add("decode-labels");
4379
4543
  return;
4380
4544
  }
4381
- if (joined.length >= 16 && joined.length <= 253) yield [joined, true];
4545
+ yield [joined, true];
4382
4546
  }
4383
4547
  }
4384
4548
  }
@@ -4415,10 +4579,6 @@ function* decodedCandidates(text, budget) {
4415
4579
  if (joined) yield ["dns-labels", region];
4416
4580
  for (const [kind, re] of families) {
4417
4581
  for (const match of region.matchAll(re)) {
4418
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4419
- budget.reasons.add("decode-candidates");
4420
- return;
4421
- }
4422
4582
  const raw = match[0];
4423
4583
  let buf;
4424
4584
  if (kind === "base64") {
@@ -4441,42 +4601,76 @@ function* decodedCandidates(text, budget) {
4441
4601
  }
4442
4602
  }
4443
4603
  }
4444
- function* secretViews(text, budget) {
4604
+ function secretViews(text, budget) {
4445
4605
  const seen = /* @__PURE__ */ new Set([text]);
4446
- const queue = [{ text, transforms: [] }];
4447
- for (let i = 0; i < queue.length; i++) {
4448
- const view = queue[i];
4449
- yield view;
4450
- for (const [kind, decoded] of decodedCandidates(view.text, budget)) {
4451
- if (seen.has(decoded)) continue;
4452
- if (view.transforms.length >= SECRET_VIEW_LIMITS.maxDepth) {
4453
- budget.reasons.add("decode-depth");
4454
- continue;
4455
- }
4456
- const bytes = Buffer.byteLength(decoded);
4457
- if (queue.length >= SECRET_VIEW_LIMITS.maxViewsPerLeaf || budget.views >= SECRET_VIEW_LIMITS.maxViews || budget.bytes + bytes > SECRET_VIEW_LIMITS.maxDerivedBytes) {
4458
- budget.reasons.add("decode-budget");
4459
- return;
4460
- }
4461
- seen.add(decoded);
4462
- budget.views++;
4463
- budget.bytes += bytes;
4464
- queue.push({ text: decoded, transforms: [...view.transforms, kind] });
4606
+ const out = [];
4607
+ let frontier = [{ text, transforms: [] }];
4608
+ for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
4609
+ const next = [];
4610
+ for (const parent of frontier) {
4611
+ let perLeaf = 0;
4612
+ for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
4613
+ if (seen.has(decoded)) continue;
4614
+ seen.add(decoded);
4615
+ const normalizing = kind === "invisibles" || kind === "percent";
4616
+ if (depth >= 2 && !normalizing) {
4617
+ budget.nestedBytes += Buffer.byteLength(decoded);
4618
+ if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
4619
+ budget.reasons.add("decode-nested");
4620
+ return out;
4621
+ }
4622
+ }
4623
+ const view = { text: decoded, transforms: [...parent.transforms, kind] };
4624
+ out.push(view);
4625
+ next.push(view);
4626
+ }
4627
+ }
4628
+ frontier = next;
4629
+ if (!frontier.length) return out;
4630
+ }
4631
+ for (const view of frontier) {
4632
+ if (!decodedCandidates(view.text, budget).next().done) {
4633
+ budget.reasons.add("decode-nested");
4634
+ break;
4635
+ }
4636
+ }
4637
+ return out;
4638
+ }
4639
+ function batches(views) {
4640
+ const out = [];
4641
+ let current = [];
4642
+ let size = 0;
4643
+ for (const { text } of views) {
4644
+ if (current.length && size + text.length > SECRET_VIEW_LIMITS.batchCharacters) {
4645
+ out.push(current);
4646
+ current = [];
4647
+ size = 0;
4465
4648
  }
4466
- if (budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) return;
4649
+ current.push(text);
4650
+ size += text.length;
4467
4651
  }
4652
+ if (current.length) out.push(current);
4653
+ return out;
4468
4654
  }
4469
4655
  function inspectArgumentSecrets(args) {
4470
- const budget = { views: 0, bytes: 0, candidates: 0, reasons: /* @__PURE__ */ new Set() };
4656
+ const budget = newSecretViewBudget();
4471
4657
  const ancestors = /* @__PURE__ */ new Set();
4472
4658
  let nodes = 0;
4473
4659
  let sourceBytes = 0;
4474
4660
  let tailBytes = 0;
4475
4661
  let best = null;
4662
+ const blocked = () => best?.severity === "block";
4476
4663
  const accept = (match) => {
4477
4664
  if (match && (!best || best.severity !== "block" && match.severity === "block")) best = match;
4478
4665
  };
4666
+ const decodeSegment = (segment, fieldPath) => {
4667
+ for (const batch of batches(secretViews(segment, budget))) {
4668
+ accept(matchArgumentBatch(batch, fieldPath, budget));
4669
+ if (blocked()) return;
4670
+ }
4671
+ };
4479
4672
  const visit2 = (value, depth, fieldPath) => {
4673
+ if (blocked()) return;
4480
4674
  if (++nodes > ARGUMENT_SCAN_LIMITS.maxNodes) {
4481
4675
  budget.reasons.add("argument-nodes");
4482
4676
  return;
@@ -4518,14 +4712,11 @@ function inspectArgumentSecrets(args) {
4518
4712
  }
4519
4713
  sourceBytes += Math.min(bytes, Math.max(0, remaining));
4520
4714
  if (!text) return;
4521
- for (const view of secretViews(text, budget)) {
4522
- const hit = matchArgumentText(view.text, fieldPath, budget);
4523
- if (hit)
4524
- accept(view.transforms.length ? { ...hit, redactedSample: "[encoded secret]" } : hit);
4525
- }
4715
+ accept(matchArgumentText(text, fieldPath, budget));
4716
+ let scannedEnd = text.length;
4526
4717
  if (!truncated && value.length > window) {
4527
- budget.reasons.add("decode-window");
4528
4718
  for (let start = window - overlap; start < value.length; start += window - overlap) {
4719
+ if (blocked()) return;
4529
4720
  const chunk2 = value.slice(start, start + window);
4530
4721
  const chunkBytes = Buffer.byteLength(chunk2);
4531
4722
  if (tailBytes + chunkBytes > ARGUMENT_SCAN_LIMITS.maxTailBytes) {
@@ -4534,9 +4725,16 @@ function inspectArgumentSecrets(args) {
4534
4725
  }
4535
4726
  tailBytes += chunkBytes;
4536
4727
  accept(matchArgumentText(chunk2, fieldPath, budget));
4728
+ scannedEnd = start + chunk2.length;
4537
4729
  if (start + window >= value.length) break;
4538
4730
  }
4539
4731
  }
4732
+ for (let start = 0; start < scannedEnd && !blocked(); ) {
4733
+ let end = Math.min(start + window, scannedEnd);
4734
+ while (end < scannedEnd && runChar(value, end - 1) && runChar(value, end)) end++;
4735
+ decodeSegment(value.slice(start, end), fieldPath);
4736
+ start = end;
4737
+ }
4540
4738
  const trimmed = text.trim();
4541
4739
  if (value.length < ARGUMENT_SCAN_LIMITS.maxJsonCharacters && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
4542
4740
  try {
@@ -4555,7 +4753,7 @@ function inspectArgumentSecrets(args) {
4555
4753
  function scanArgumentSecrets(args) {
4556
4754
  const result = inspectArgumentSecrets(args);
4557
4755
  if (result.match) return result.match;
4558
- return result.reasons.some((reason) => reason.startsWith("argument-")) ? {
4756
+ return result.coverage === "incomplete" ? {
4559
4757
  patternName: "Argument inspection incomplete",
4560
4758
  fieldPath: "args",
4561
4759
  redactedSample: "[uninspected content]",
@@ -4583,18 +4781,13 @@ function tokenize2(toolName) {
4583
4781
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
4584
4782
  }
4585
4783
  function extractShellCommand(toolName, args, toolInspection) {
4586
- const patterns = Object.keys(toolInspection);
4587
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4588
- if (!matchingPattern) return null;
4589
- const fieldPath = toolInspection[matchingPattern];
4784
+ const fieldPath = inspectionEntry(toolName, toolInspection);
4785
+ if (fieldPath === void 0) return null;
4590
4786
  const value = getNestedValue(args, typeof fieldPath === "string" ? fieldPath : fieldPath.field);
4591
4787
  return typeof value === "string" ? value : null;
4592
4788
  }
4593
4789
  function isSqlTool(toolName, toolInspection) {
4594
- const patterns = Object.keys(toolInspection);
4595
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4596
- if (!matchingPattern) return false;
4597
- const fieldName = toolInspection[matchingPattern];
4790
+ const fieldName = inspectionEntry(toolName, toolInspection);
4598
4791
  return fieldName === "sql" || fieldName === "query";
4599
4792
  }
4600
4793
  function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
@@ -4661,9 +4854,10 @@ function egressPolicyVerdict(eg) {
4661
4854
  }
4662
4855
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
4663
4856
  let protectedVerdict = null;
4857
+ const layers = inspectionLayers(config.policy);
4664
4858
  if (context.agent !== "Terminal") {
4665
4859
  try {
4666
- const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, config.policy.toolInspection);
4860
+ const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, layers);
4667
4861
  protectedVerdict = evaluateProtectedFileAccess(accesses, context, catalogSettings(config));
4668
4862
  } catch {
4669
4863
  protectedVerdict = incompleteFileVerdict();
@@ -4698,7 +4892,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4698
4892
  hooks
4699
4893
  );
4700
4894
  if (result.decision === "block") return result;
4701
- if (context.agent !== "Terminal" && resolveShellInput(toolName, args, config.policy.toolInspection).kind === "uninspectable") {
4895
+ if (context.agent !== "Terminal" && resolveShellInput(toolName, args, layers).kind === "uninspectable") {
4702
4896
  return {
4703
4897
  decision: "review",
4704
4898
  ruleName: "shell-inspection-incomplete",
@@ -4714,13 +4908,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4714
4908
  const { agent, cwd, activeEnvironment } = context;
4715
4909
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
4716
4910
  const settingsView = catalogSettings(config);
4911
+ const layers = inspectionLayers(config.policy);
4717
4912
  const cv = (id) => checkValue(settingsView, id);
4718
4913
  let loggedFinding;
4719
4914
  const noteLogged = (v) => {
4720
4915
  if (v?.logged && !loggedFinding) loggedFinding = v;
4721
4916
  };
4722
4917
  const allowed = () => loggedFinding ?? { decision: "allow" };
4723
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
4918
+ const shellInput = resolveShellInput(toolName, args, layers);
4724
4919
  const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
4725
4920
  {
4726
4921
  const dest = ssrfDestinationFloor(toolName, args, {
@@ -4825,13 +5020,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4825
5020
  return { id, value: cv(id) };
4826
5021
  };
4827
5022
  const matches = config.policy.smartRules.filter((rule) => {
4828
- if (!toolMatchesRule(toolName, rule.tool, config.policy.toolInspection)) return false;
5023
+ if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
4829
5024
  if (astSuppressed(rule)) return false;
4830
5025
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
4831
- if (!evaluateSmartConditions(
4832
- shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
4833
- rule
4834
- ))
5026
+ if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
4835
5027
  return false;
4836
5028
  const check = ruleCheck(rule);
4837
5029
  if (check?.value === "off") return false;
@@ -4878,7 +5070,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4878
5070
  let allTokens = [];
4879
5071
  let pathTokens = [];
4880
5072
  if (pendingToolEgress) return pendingToolEgress;
4881
- const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, config.policy.toolInspection) : null;
5073
+ const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, layers) : null;
4882
5074
  if (shellCommand) {
4883
5075
  const analyzed = analyzeShellCommand(shellCommand);
4884
5076
  allTokens = analyzed.allTokens;
@@ -4955,7 +5147,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4955
5147
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
4956
5148
  const rawTokens = shellCommand.trim().split(/\s+/);
4957
5149
  const sshHosts = extractAllSshHosts(rawTokens.slice(1));
4958
- allTokens.push(...sshHosts);
5150
+ allTokens = allTokens.concat(sshHosts);
4959
5151
  }
4960
5152
  if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
4961
5153
  const prov = checkProvenance2(firstToken, cwd);
@@ -4984,7 +5176,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4984
5176
  };
4985
5177
  }
4986
5178
  }
4987
- if (isSqlTool(toolName, config.policy.toolInspection)) {
5179
+ if (isSqlTool(toolName, layers)) {
4988
5180
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
4989
5181
  }
4990
5182
  } else {
@@ -4992,7 +5184,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4992
5184
  if (args && typeof args === "object") {
4993
5185
  const flattenedArgs = JSON.stringify(args).toLowerCase();
4994
5186
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
4995
- allTokens.push(...extraTokens);
5187
+ allTokens = allTokens.concat(extraTokens);
4996
5188
  }
4997
5189
  }
4998
5190
  const isManual = agent === "Terminal";
@@ -6287,7 +6479,7 @@ function* stringValues(obj, depth = 0) {
6287
6479
  }
6288
6480
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
6289
6481
  }
6290
- var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_mvdan_sh4, import_crypto4, 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;
6482
+ var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_mvdan_sh4, import_crypto4, 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;
6291
6483
  var init_dist = __esm({
6292
6484
  "packages/policy-engine/dist/index.mjs"() {
6293
6485
  "use strict";
@@ -8672,7 +8864,23 @@ var init_dist = __esm({
8672
8864
  "shell",
8673
8865
  "exec_command"
8674
8866
  ]);
8675
- MCP_EXECUTORS = /* @__PURE__ */ new Set([...BASH_TOOL_NAMES, "exec", "run_command", "terminal"]);
8867
+ MCP_EXECUTORS = /* @__PURE__ */ new Set([
8868
+ ...BASH_TOOL_NAMES,
8869
+ "exec",
8870
+ "run_command",
8871
+ "terminal",
8872
+ "execute_command",
8873
+ "execute_shell",
8874
+ "execute_shell_command",
8875
+ "shell_execute",
8876
+ "run_shell",
8877
+ "run_terminal_command",
8878
+ "start_process"
8879
+ ]);
8880
+ BUILTIN_FIELDS = ["command", "cmd", "script"];
8881
+ ARGV_FIELDS = ["args", "argv"];
8882
+ BARE_WORD = /^[A-Za-z0-9_./+:@%,-]+$/;
8883
+ MAX_ARGV_CHARACTERS = 262144;
8676
8884
  ({ syntax: syntax2 } = import_mvdan_sh2.default);
8677
8885
  MAX_ALTERNATIVES = 64;
8678
8886
  MAX_DEPTH = 64;
@@ -10798,6 +11006,7 @@ zoo`.split("\n");
10798
11006
  indices = new Map(BIP39_ENGLISH.map((word, i) => [word, i]));
10799
11007
  lengths = [24, 21, 18, 15, 12];
10800
11008
  hash = (value) => (0, import_crypto2.createHash)("sha256").update(value).digest();
11009
+ MAX_MNEMONIC_HASHES = 25e3;
10801
11010
  IBAN_LENGTH = {
10802
11011
  AD: 24,
10803
11012
  AE: 23,
@@ -10905,6 +11114,7 @@ zoo`.split("\n");
10905
11114
  CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
10906
11115
  CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
10907
11116
  INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
11117
+ INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
10908
11118
  HOMOGLYPHS = {
10909
11119
  // Cyrillic lowercase
10910
11120
  \u0430: "a",
@@ -11611,6 +11821,7 @@ zoo`.split("\n");
11611
11821
  /** Max nesting depth walked; anything deeper is NOT scanned. */
11612
11822
  maxDepth: MAX_DEPTH2
11613
11823
  };
11824
+ BATCH_SEPARATOR = "\n";
11614
11825
  ({ syntax: syntax3 } = import_mvdan_sh3.default);
11615
11826
  NPM_INSTALL_VERBS = /* @__PURE__ */ new Set([
11616
11827
  "install",
@@ -12080,16 +12291,24 @@ zoo`.split("\n");
12080
12291
  ["browser_batch", ["actions[].input.url"]]
12081
12292
  ]);
12082
12293
  SECRET_VIEW_LIMITS = {
12294
+ /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
12083
12295
  maxDepth: 4,
12084
- maxViewsPerLeaf: 64,
12085
- maxViews: 256,
12086
- maxDerivedBytes: 1048576,
12087
- maxCandidates: 4096
12296
+ /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
12297
+ maxNestedViewsPerLeaf: 64,
12298
+ maxNestedBytes: 1048576,
12299
+ /** Joined hostname-label text per call; past it the call is reviewed. */
12300
+ maxJoinedLabelBytes: 1048576,
12301
+ /** Decoded fragments are matched in batches of about this many characters. */
12302
+ batchCharacters: 65536,
12303
+ /** Only a valid DNS name has its labels joined. */
12304
+ maxHostLength: 253,
12305
+ maxLabelLength: 63,
12306
+ maxLabels: 127
12088
12307
  };
12089
12308
  ARGUMENT_SCAN_LIMITS = {
12090
12309
  maxDepth: 12,
12091
12310
  maxNodes: 16384,
12092
- /** Raw text is matched in windows of this size; decoding views use the first window. */
12311
+ /** Text is matched and decoded in windows of this size. */
12093
12312
  maxStringCharacters: 1e5,
12094
12313
  /** Windows overlap by this much so a token on a window edge is still seen whole. */
12095
12314
  windowOverlap: 4096,
@@ -12099,6 +12318,11 @@ zoo`.split("\n");
12099
12318
  maxTailBytes: 4194304,
12100
12319
  maxJsonCharacters: 1e4
12101
12320
  };
12321
+ RUN_CHAR = /[A-Za-z0-9+/=_%\\.-]/;
12322
+ runChar = (value, index) => {
12323
+ const char = value[index] ?? "";
12324
+ return RUN_CHAR.test(char) || char !== "" && isInvisibleChar(char);
12325
+ };
12102
12326
  VERDICT_RANK = {
12103
12327
  allow: 0,
12104
12328
  review: 1,
@@ -12358,7 +12582,7 @@ zoo`.split("\n");
12358
12582
  { view: "separators-stripped", decoder: "separators", stripped: true }
12359
12583
  ];
12360
12584
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
12361
- CANONICAL_EXTRACTOR_VERSION = "canonical-v25";
12585
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v26";
12362
12586
  DEDUPE_PREVIEW_LEN = 120;
12363
12587
  ENGINE_VERSION = "1.4.0";
12364
12588
  }
@@ -12484,6 +12708,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
12484
12708
  taintFromEid: meta.taintFromEid,
12485
12709
  ...meta.taintSource && { taintSource: meta.taintSource }
12486
12710
  } : {};
12711
+ const exposureFields = {
12712
+ ...meta?.exposure ? { exposure: meta.exposure } : {},
12713
+ ...meta?.exposureFromEid ? { exposureFromEid: meta.exposureFromEid } : {}
12714
+ };
12487
12715
  const eid = generateEventId();
12488
12716
  appendToLog(LOCAL_AUDIT_LOG, {
12489
12717
  // eid first: the outbox shipper dedups on it, and a fixed leading field
@@ -12506,6 +12734,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
12506
12734
  ...loopCountField,
12507
12735
  ...transcriptPathField,
12508
12736
  ...taintFields,
12737
+ ...exposureFields,
12509
12738
  ...testRun,
12510
12739
  agent: meta?.agent,
12511
12740
  mcpServer: meta?.mcpServer,
@@ -12538,6 +12767,82 @@ var init_audit = __esm({
12538
12767
  }
12539
12768
  });
12540
12769
 
12770
+ // src/auth/trusted-recipients.ts
12771
+ function getTrustedRecipientsPath() {
12772
+ return import_path2.default.join(import_os2.default.homedir(), ".node9", "trusted-recipients.json");
12773
+ }
12774
+ function readTrustedRecipients() {
12775
+ try {
12776
+ const raw = import_fs2.default.readFileSync(getTrustedRecipientsPath(), "utf8");
12777
+ const parsed = JSON.parse(raw);
12778
+ return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
12779
+ } catch {
12780
+ return [];
12781
+ }
12782
+ }
12783
+ function writeTrustedRecipients(entries) {
12784
+ const filePath = getTrustedRecipientsPath();
12785
+ import_fs2.default.mkdirSync(import_path2.default.dirname(filePath), { recursive: true });
12786
+ const tmp = filePath + ".node9-tmp";
12787
+ import_fs2.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
12788
+ import_fs2.default.renameSync(tmp, filePath);
12789
+ }
12790
+ function normalizeRecipient(raw) {
12791
+ const v = raw.trim();
12792
+ if (!v) return null;
12793
+ if (/^(email|host|iban):/i.test(v)) {
12794
+ const i = v.indexOf(":");
12795
+ const kind = v.slice(0, i).toLowerCase();
12796
+ const inner = normalizeRecipient(v.slice(i + 1));
12797
+ return inner && inner.startsWith(`${kind}:`) ? inner : null;
12798
+ }
12799
+ const found = extractArgDestinations({ to: v });
12800
+ return found.length === 1 ? found[0] : null;
12801
+ }
12802
+ function addTrustedRecipient(raw, addedBy = "user") {
12803
+ const value = normalizeRecipient(raw);
12804
+ if (!value) return null;
12805
+ const entries = readTrustedRecipients();
12806
+ if (!entries.some((e) => e.value === value)) {
12807
+ entries.push({ value, addedAt: Date.now(), addedBy });
12808
+ writeTrustedRecipients(entries);
12809
+ }
12810
+ return value;
12811
+ }
12812
+ function removeTrustedRecipient(raw) {
12813
+ const value = normalizeRecipient(raw) ?? raw.trim();
12814
+ const entries = readTrustedRecipients();
12815
+ const next = entries.filter((e) => e.value !== value);
12816
+ if (next.length === entries.length) return false;
12817
+ writeTrustedRecipients(next);
12818
+ return true;
12819
+ }
12820
+ function mtimeOf() {
12821
+ try {
12822
+ return import_fs2.default.statSync(getTrustedRecipientsPath()).mtimeMs;
12823
+ } catch {
12824
+ return 0;
12825
+ }
12826
+ }
12827
+ function isTrustedRecipient(value) {
12828
+ const mtime = mtimeOf();
12829
+ if (!cache || cache.mtime !== mtime) {
12830
+ cache = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
12831
+ }
12832
+ return cache.values.has(value);
12833
+ }
12834
+ var import_fs2, import_path2, import_os2, cache;
12835
+ var init_trusted_recipients = __esm({
12836
+ "src/auth/trusted-recipients.ts"() {
12837
+ "use strict";
12838
+ import_fs2 = __toESM(require("fs"));
12839
+ import_path2 = __toESM(require("path"));
12840
+ import_os2 = __toESM(require("os"));
12841
+ init_dist();
12842
+ cache = null;
12843
+ }
12844
+ });
12845
+
12541
12846
  // src/config-schema.ts
12542
12847
  function formatIssues(issues) {
12543
12848
  const lines = issues.map((issue) => {
@@ -12815,7 +13120,7 @@ function loadUserShields() {
12815
13120
  const result = {};
12816
13121
  let entries;
12817
13122
  try {
12818
- entries = import_fs2.default.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
13123
+ entries = import_fs3.default.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
12819
13124
  } catch (err2) {
12820
13125
  if (err2.code !== "ENOENT") {
12821
13126
  process.stderr.write(
@@ -12826,9 +13131,9 @@ function loadUserShields() {
12826
13131
  return result;
12827
13132
  }
12828
13133
  for (const file2 of entries) {
12829
- const filePath = import_path2.default.join(USER_SHIELDS_DIR, file2);
13134
+ const filePath = import_path3.default.join(USER_SHIELDS_DIR, file2);
12830
13135
  try {
12831
- const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
13136
+ const raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
12832
13137
  const v = validateShieldDefinition(raw);
12833
13138
  if ("error" in v) {
12834
13139
  process.stderr.write(`[node9] ${v.error}: ${filePath}
@@ -12863,7 +13168,7 @@ function listShields() {
12863
13168
  }
12864
13169
  function readShieldsFile() {
12865
13170
  try {
12866
- const raw = import_fs2.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
13171
+ const raw = import_fs3.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
12867
13172
  if (!raw.trim()) return { active: [] };
12868
13173
  const parsed = JSON.parse(raw);
12869
13174
  const active = Array.isArray(parsed.active) ? parsed.active.filter(
@@ -12884,12 +13189,12 @@ function readShieldsFile() {
12884
13189
  }
12885
13190
  }
12886
13191
  function writeShieldsFile(data) {
12887
- import_fs2.default.mkdirSync(import_path2.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
13192
+ import_fs3.default.mkdirSync(import_path3.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
12888
13193
  const tmp = `${SHIELDS_STATE_FILE}.${import_crypto6.default.randomBytes(6).toString("hex")}.tmp`;
12889
13194
  const toWrite = { active: data.active };
12890
13195
  if (data.overrides && Object.keys(data.overrides).length > 0) toWrite.overrides = data.overrides;
12891
- import_fs2.default.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
12892
- import_fs2.default.renameSync(tmp, SHIELDS_STATE_FILE);
13196
+ import_fs3.default.writeFileSync(tmp, JSON.stringify(toWrite, null, 2), { mode: 384 });
13197
+ import_fs3.default.renameSync(tmp, SHIELDS_STATE_FILE);
12893
13198
  }
12894
13199
  function readActiveShields() {
12895
13200
  return readShieldsFile().active;
@@ -12973,25 +13278,25 @@ function installShield(name, shieldJson) {
12973
13278
  if (v.ok.name !== name) {
12974
13279
  throw new Error(`Shield name mismatch: file declares '${v.ok.name}' but expected '${name}'`);
12975
13280
  }
12976
- import_fs2.default.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
12977
- const filePath = import_path2.default.join(USER_SHIELDS_DIR, `${name}.json`);
13281
+ import_fs3.default.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
13282
+ const filePath = import_path3.default.join(USER_SHIELDS_DIR, `${name}.json`);
12978
13283
  const tmp = `${filePath}.${import_crypto6.default.randomBytes(6).toString("hex")}.tmp`;
12979
- import_fs2.default.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
12980
- import_fs2.default.renameSync(tmp, filePath);
13284
+ import_fs3.default.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
13285
+ import_fs3.default.renameSync(tmp, filePath);
12981
13286
  }
12982
- var import_fs2, import_path2, import_os2, import_crypto6, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS, USER_SHIELDS_DIR_PATH;
13287
+ var import_fs3, import_path3, import_os3, import_crypto6, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS, USER_SHIELDS_DIR_PATH;
12983
13288
  var init_shields = __esm({
12984
13289
  "src/shields.ts"() {
12985
13290
  "use strict";
12986
- import_fs2 = __toESM(require("fs"));
12987
- import_path2 = __toESM(require("path"));
12988
- import_os2 = __toESM(require("os"));
13291
+ import_fs3 = __toESM(require("fs"));
13292
+ import_path3 = __toESM(require("path"));
13293
+ import_os3 = __toESM(require("os"));
12989
13294
  import_crypto6 = __toESM(require("crypto"));
12990
13295
  init_dist();
12991
13296
  init_dist();
12992
- USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
13297
+ USER_SHIELDS_DIR = import_path3.default.join(import_os3.default.homedir(), ".node9", "shields");
12993
13298
  SHIELDS = buildSHIELDS();
12994
- SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
13299
+ SHIELDS_STATE_FILE = import_path3.default.join(import_os3.default.homedir(), ".node9", "shields.json");
12995
13300
  RULE_KEY_MIGRATIONS = [
12996
13301
  // 2026-05-21 — project-jail .env reads promoted review → block. Renamed
12997
13302
  // so the key honestly describes the verdict. Credential-file rules
@@ -13226,11 +13531,11 @@ var init_build = __esm({
13226
13531
 
13227
13532
  // src/auth/trusted-hosts.ts
13228
13533
  function getTrustedHostsPath() {
13229
- return import_path3.default.join(import_os3.default.homedir(), ".node9", "trusted-hosts.json");
13534
+ return import_path4.default.join(import_os4.default.homedir(), ".node9", "trusted-hosts.json");
13230
13535
  }
13231
13536
  function readTrustedHosts() {
13232
13537
  try {
13233
- const raw = import_fs3.default.readFileSync(getTrustedHostsPath(), "utf8");
13538
+ const raw = import_fs4.default.readFileSync(getTrustedHostsPath(), "utf8");
13234
13539
  const parsed = JSON.parse(raw);
13235
13540
  return Array.isArray(parsed.hosts) ? parsed.hosts : [];
13236
13541
  } catch {
@@ -13239,7 +13544,7 @@ function readTrustedHosts() {
13239
13544
  }
13240
13545
  function getFileMtime() {
13241
13546
  try {
13242
- return import_fs3.default.statSync(getTrustedHostsPath()).mtimeMs;
13547
+ return import_fs4.default.statSync(getTrustedHostsPath()).mtimeMs;
13243
13548
  } catch {
13244
13549
  return 0;
13245
13550
  }
@@ -13256,10 +13561,10 @@ function getCachedHosts() {
13256
13561
  }
13257
13562
  function writeTrustedHosts(hosts) {
13258
13563
  const filePath = getTrustedHostsPath();
13259
- import_fs3.default.mkdirSync(import_path3.default.dirname(filePath), { recursive: true });
13564
+ import_fs4.default.mkdirSync(import_path4.default.dirname(filePath), { recursive: true });
13260
13565
  const tmp = filePath + ".node9-tmp";
13261
- import_fs3.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
13262
- import_fs3.default.renameSync(tmp, filePath);
13566
+ import_fs4.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
13567
+ import_fs4.default.renameSync(tmp, filePath);
13263
13568
  _cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
13264
13569
  }
13265
13570
  function addTrustedHost(host2) {
@@ -13304,13 +13609,13 @@ function isTrustedHost(host2) {
13304
13609
  getCachedHosts().map((entry) => entry.host)
13305
13610
  );
13306
13611
  }
13307
- var import_fs3, import_path3, import_os3, _cache, CACHE_TTL_MS;
13612
+ var import_fs4, import_path4, import_os4, _cache, CACHE_TTL_MS;
13308
13613
  var init_trusted_hosts = __esm({
13309
13614
  "src/auth/trusted-hosts.ts"() {
13310
13615
  "use strict";
13311
- import_fs3 = __toESM(require("fs"));
13312
- import_path3 = __toESM(require("path"));
13313
- import_os3 = __toESM(require("os"));
13616
+ import_fs4 = __toESM(require("fs"));
13617
+ import_path4 = __toESM(require("path"));
13618
+ import_os4 = __toESM(require("os"));
13314
13619
  _cache = null;
13315
13620
  CACHE_TTL_MS = 5e3;
13316
13621
  }
@@ -13589,9 +13894,9 @@ function _resetConfigCache() {
13589
13894
  }
13590
13895
  function getGlobalSettings() {
13591
13896
  try {
13592
- const globalConfigPath2 = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
13593
- if (import_fs4.default.existsSync(globalConfigPath2)) {
13594
- const raw = JSON.parse(import_fs4.default.readFileSync(globalConfigPath2, "utf-8"));
13897
+ const globalConfigPath2 = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
13898
+ if (import_fs5.default.existsSync(globalConfigPath2)) {
13899
+ const raw = JSON.parse(import_fs5.default.readFileSync(globalConfigPath2, "utf-8"));
13595
13900
  const parsed = isV2File(raw) ? v2ToLegacy(raw).legacy : raw;
13596
13901
  const settings = parsed.settings || {};
13597
13902
  return {
@@ -13620,8 +13925,8 @@ function noteRejectedApiUrl(raw) {
13620
13925
  if (rejectedApiUrlsSeen.has(key)) return;
13621
13926
  rejectedApiUrlsSeen.add(key);
13622
13927
  try {
13623
- import_fs4.default.appendFileSync(
13624
- import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
13928
+ import_fs5.default.appendFileSync(
13929
+ import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log"),
13625
13930
  `[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
13626
13931
  `
13627
13932
  );
@@ -13638,9 +13943,9 @@ function getCredentials() {
13638
13943
  };
13639
13944
  }
13640
13945
  try {
13641
- const credPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "credentials.json");
13642
- if (import_fs4.default.existsSync(credPath)) {
13643
- const creds = JSON.parse(import_fs4.default.readFileSync(credPath, "utf-8"));
13946
+ const credPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
13947
+ if (import_fs5.default.existsSync(credPath)) {
13948
+ const creds = JSON.parse(import_fs5.default.readFileSync(credPath, "utf-8"));
13644
13949
  const profileName = process.env.NODE9_PROFILE || "default";
13645
13950
  const profile = creds[profileName];
13646
13951
  if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
@@ -13672,7 +13977,7 @@ function readRulesCacheResilient(cacheFile) {
13672
13977
  for (let attempt = 0; attempt < 3; attempt++) {
13673
13978
  let content;
13674
13979
  try {
13675
- content = import_fs4.default.readFileSync(cacheFile, "utf-8");
13980
+ content = import_fs5.default.readFileSync(cacheFile, "utf-8");
13676
13981
  existed = true;
13677
13982
  } catch (err2) {
13678
13983
  if (err2.code === "ENOENT") return {};
@@ -13687,10 +13992,10 @@ function readRulesCacheResilient(cacheFile) {
13687
13992
  }
13688
13993
  }
13689
13994
  if (existed || sawReadError) {
13690
- const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
13995
+ const backup = import_path5.default.join(import_path5.default.dirname(cacheFile), "rules-cache.last-good.json");
13691
13996
  if (backup !== cacheFile) {
13692
13997
  try {
13693
- const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
13998
+ const raw = JSON.parse(import_fs5.default.readFileSync(backup, "utf-8"));
13694
13999
  logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
13695
14000
  lastParsedRulesCache = raw;
13696
14001
  return raw;
@@ -13710,8 +14015,8 @@ function logCacheReadIssue(cacheFile, kind) {
13710
14015
  if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
13711
14016
  cacheReadLastLoggedAt = now;
13712
14017
  try {
13713
- import_fs4.default.appendFileSync(
13714
- import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
14018
+ import_fs5.default.appendFileSync(
14019
+ import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log"),
13715
14020
  `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
13716
14021
  `
13717
14022
  );
@@ -13723,8 +14028,8 @@ function catalogSettingsFrom(settings, policy) {
13723
14028
  }
13724
14029
  function getConfig(cwd) {
13725
14030
  if (!cwd && cachedConfig) return cachedConfig;
13726
- const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
13727
- const projectPath = import_path4.default.join(cwd ?? process.cwd(), "node9.config.json");
14031
+ const globalPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
14032
+ const projectPath = import_path5.default.join(cwd ?? process.cwd(), "node9.config.json");
13728
14033
  const globalConfig = tryLoadConfig(globalPath);
13729
14034
  const projectConfig = tryLoadConfig(projectPath, { project: true });
13730
14035
  const mergedSettings = {
@@ -13737,6 +14042,7 @@ function getConfig(cwd) {
13737
14042
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
13738
14043
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
13739
14044
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
14045
+ projectToolInspection: {},
13740
14046
  // Shipped rules carry `builtin`: the catalog's per-check value governs
13741
14047
  // them and only them (a user rule that reuses a name keeps its verdict).
13742
14048
  smartRules: DEFAULT_CONFIG.policy.smartRules.map((r) => ({ ...r, builtin: true })),
@@ -13763,7 +14069,8 @@ function getConfig(cwd) {
13763
14069
  untrustedContent: {
13764
14070
  ...DEFAULT_CONFIG.policy.untrustedContent,
13765
14071
  orgDomains: [...DEFAULT_CONFIG.policy.untrustedContent.orgDomains],
13766
- roles: {}
14072
+ roles: {},
14073
+ trustedRecipients: []
13767
14074
  },
13768
14075
  // Left empty on purpose: the local file is read fresh at policy-eval time
13769
14076
  // (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
@@ -13809,8 +14116,14 @@ function getConfig(cwd) {
13809
14116
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
13810
14117
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
13811
14118
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
13812
- if (p.toolInspection)
13813
- mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
14119
+ if (p.toolInspection) {
14120
+ if (isProject)
14121
+ mergedPolicy.projectToolInspection = {
14122
+ ...mergedPolicy.projectToolInspection,
14123
+ ...p.toolInspection
14124
+ };
14125
+ else mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
14126
+ }
13814
14127
  if (p.smartRules) {
13815
14128
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
13816
14129
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
@@ -13958,7 +14271,7 @@ function getConfig(cwd) {
13958
14271
  let cloudMandatesEnforcement = false;
13959
14272
  let cloudMandatesAppPerm = false;
13960
14273
  {
13961
- const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
14274
+ const cacheFile = import_path5.default.join(import_os5.default.homedir(), ".node9", "rules-cache.json");
13962
14275
  try {
13963
14276
  const raw = readRulesCacheResilient(cacheFile);
13964
14277
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
@@ -14351,10 +14664,10 @@ function getConfig(cwd) {
14351
14664
  }
14352
14665
  function tryLoadConfig(filePath, options = {}) {
14353
14666
  v2ChecksByPath.delete(filePath);
14354
- if (!import_fs4.default.existsSync(filePath)) return null;
14667
+ if (!import_fs5.default.existsSync(filePath)) return null;
14355
14668
  let raw;
14356
14669
  try {
14357
- raw = JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
14670
+ raw = JSON.parse(import_fs5.default.readFileSync(filePath, "utf-8"));
14358
14671
  } catch (err2) {
14359
14672
  const msg = err2 instanceof Error ? err2.message : String(err2);
14360
14673
  process.stderr.write(
@@ -14429,6 +14742,13 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
14429
14742
  if (replaceLists) cur.orgDomains = domains;
14430
14743
  else for (const d of domains) if (!cur.orgDomains.includes(d)) cur.orgDomains.push(d);
14431
14744
  }
14745
+ if (replaceLists && Array.isArray(uc.trustedRecipients)) {
14746
+ cur.trustedRecipients = [
14747
+ ...new Set(
14748
+ uc.trustedRecipients.filter((v) => typeof v === "string").map((v) => normalizeRecipient(v)).filter((v) => v !== null)
14749
+ )
14750
+ ].slice(0, 200);
14751
+ }
14432
14752
  if (uc.roles && typeof uc.roles === "object" && !Array.isArray(uc.roles)) {
14433
14753
  const next = replaceLists ? {} : cur.roles;
14434
14754
  for (const [server, tools] of Object.entries(uc.roles)) {
@@ -14445,13 +14765,14 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
14445
14765
  cur.roles = next;
14446
14766
  }
14447
14767
  }
14448
- var import_fs4, import_path4, import_os4, DANGEROUS_WORDS, RUNTIME_ONLY_CONFIG_KEYS, DEFAULT_CONFIG, RM_SAFE_PATH_PATTERN, VERDICT_ORDER, ADVISORY_SMART_RULES, cachedConfig, rejectedApiUrlsSeen, lastParsedRulesCache, CACHE_LOG_REARM_MS, cacheReadLastLoggedAt, v2ChecksByPath, KNOB_TO_CHECK;
14768
+ var import_fs5, import_path5, import_os5, DANGEROUS_WORDS, RUNTIME_ONLY_CONFIG_KEYS, DEFAULT_CONFIG, RM_SAFE_PATH_PATTERN, VERDICT_ORDER, ADVISORY_SMART_RULES, cachedConfig, rejectedApiUrlsSeen, lastParsedRulesCache, CACHE_LOG_REARM_MS, cacheReadLastLoggedAt, v2ChecksByPath, KNOB_TO_CHECK;
14449
14769
  var init_config = __esm({
14450
14770
  "src/config/index.ts"() {
14451
14771
  "use strict";
14452
- import_fs4 = __toESM(require("fs"));
14453
- import_path4 = __toESM(require("path"));
14454
- import_os4 = __toESM(require("os"));
14772
+ init_trusted_recipients();
14773
+ import_fs5 = __toESM(require("fs"));
14774
+ import_path5 = __toESM(require("path"));
14775
+ import_os5 = __toESM(require("os"));
14455
14776
  init_config_schema();
14456
14777
  init_shields();
14457
14778
  init_managed();
@@ -14706,7 +15027,8 @@ var init_config = __esm({
14706
15027
  spotlight: true,
14707
15028
  afterExposure: "ask-once",
14708
15029
  orgDomains: [],
14709
- roles: {}
15030
+ roles: {},
15031
+ trustedRecipients: []
14710
15032
  },
14711
15033
  trustedHosts: [],
14712
15034
  trustedHostsManaged: false,
@@ -14803,24 +15125,24 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
14803
15125
  if (!filePath) return null;
14804
15126
  let resolved;
14805
15127
  try {
14806
- const absolute = import_path5.default.resolve(cwd, filePath);
14807
- resolved = import_fs5.default.realpathSync.native(absolute);
15128
+ const absolute = import_path6.default.resolve(cwd, filePath);
15129
+ resolved = import_fs6.default.realpathSync.native(absolute);
14808
15130
  } catch (err2) {
14809
15131
  const code2 = err2.code;
14810
15132
  if (code2 === "ENOENT" || code2 === "ENOTDIR") {
14811
- resolved = import_path5.default.resolve(cwd, filePath);
15133
+ resolved = import_path6.default.resolve(cwd, filePath);
14812
15134
  } else {
14813
15135
  return sensitivePathMatch(filePath);
14814
15136
  }
14815
15137
  }
14816
15138
  return matchSensitivePath2(resolved, filePath, options);
14817
15139
  }
14818
- var import_fs5, import_path5;
15140
+ var import_fs6, import_path6;
14819
15141
  var init_dlp = __esm({
14820
15142
  "src/dlp.ts"() {
14821
15143
  "use strict";
14822
- import_fs5 = __toESM(require("fs"));
14823
- import_path5 = __toESM(require("path"));
15144
+ import_fs6 = __toESM(require("fs"));
15145
+ import_path6 = __toESM(require("path"));
14824
15146
  init_dist();
14825
15147
  init_dist();
14826
15148
  }
@@ -14829,24 +15151,24 @@ var init_dlp = __esm({
14829
15151
  // src/policy/file-access.ts
14830
15152
  function absoluteSpelling(p, cwd, home) {
14831
15153
  if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
14832
- return import_path6.default.isAbsolute(p) ? p : `${cwd}/${p}`;
15154
+ return import_path7.default.isAbsolute(p) ? p : `${cwd}/${p}`;
14833
15155
  }
14834
15156
  function physical(p, followFinal, links = 0) {
14835
15157
  if (links > 40) throw new Error("Too many symbolic links");
14836
15158
  if (!followFinal) {
14837
- const parent = physical(import_path6.default.dirname(p), true, links);
14838
- return { path: `${parent.path}/${import_path6.default.basename(p)}`, missing: parent.missing };
15159
+ const parent = physical(import_path7.default.dirname(p), true, links);
15160
+ return { path: `${parent.path}/${import_path7.default.basename(p)}`, missing: parent.missing };
14839
15161
  }
14840
15162
  try {
14841
- return { path: import_fs6.default.realpathSync.native(p), missing: false };
15163
+ return { path: import_fs7.default.realpathSync.native(p), missing: false };
14842
15164
  } catch (error) {
14843
15165
  const code2 = error.code;
14844
15166
  if (!["ENOENT", "ENOTDIR"].includes(code2 ?? "")) throw error;
14845
15167
  try {
14846
- if (import_fs6.default.lstatSync(p).isSymbolicLink()) {
14847
- const target = import_fs6.default.readlinkSync(p);
15168
+ if (import_fs7.default.lstatSync(p).isSymbolicLink()) {
15169
+ const target = import_fs7.default.readlinkSync(p);
14848
15170
  return physical(
14849
- import_path6.default.isAbsolute(target) ? target : `${import_path6.default.dirname(p)}/${target}`,
15171
+ import_path7.default.isAbsolute(target) ? target : `${import_path7.default.dirname(p)}/${target}`,
14850
15172
  true,
14851
15173
  links + 1
14852
15174
  );
@@ -14855,13 +15177,13 @@ function physical(p, followFinal, links = 0) {
14855
15177
  if (!["ENOENT", "ENOTDIR"].includes(linkError.code ?? ""))
14856
15178
  throw linkError;
14857
15179
  }
14858
- const parent = import_path6.default.dirname(p);
15180
+ const parent = import_path7.default.dirname(p);
14859
15181
  if (parent === p) return { path: p, missing: true };
14860
15182
  const resolved = physical(parent, true, links);
14861
- return { path: `${resolved.path}/${import_path6.default.basename(p)}`, missing: true };
15183
+ return { path: `${resolved.path}/${import_path7.default.basename(p)}`, missing: true };
14862
15184
  }
14863
15185
  }
14864
- function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = import_os5.default.homedir()) {
15186
+ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = import_os6.default.homedir()) {
14865
15187
  const context = { cwd, homeDirs: [home, "/root"] };
14866
15188
  const accesses = collectFileAccesses(tool, args, context, inspection);
14867
15189
  const handledPaths = /* @__PURE__ */ new Set();
@@ -14884,14 +15206,14 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
14884
15206
  const raw = absoluteSpelling(access.path, access.cwd ?? cwd, home);
14885
15207
  if (access.directoryTarget) {
14886
15208
  try {
14887
- if (!import_fs6.default.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
15209
+ if (!import_fs7.default.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
14888
15210
  continue;
14889
15211
  } catch {
14890
15212
  }
14891
15213
  }
14892
15214
  if (access.operation === "create") {
14893
15215
  try {
14894
- import_fs6.default.statSync(raw);
15216
+ import_fs7.default.statSync(raw);
14895
15217
  continue;
14896
15218
  } catch {
14897
15219
  }
@@ -14938,26 +15260,26 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
14938
15260
  }
14939
15261
  return { ...context, accesses: prepared, handledPaths };
14940
15262
  }
14941
- var import_fs6, import_os5, import_path6;
15263
+ var import_fs7, import_os6, import_path7;
14942
15264
  var init_file_access = __esm({
14943
15265
  "src/policy/file-access.ts"() {
14944
15266
  "use strict";
14945
- import_fs6 = __toESM(require("fs"));
14946
- import_os5 = __toESM(require("os"));
14947
- import_path6 = __toESM(require("path"));
15267
+ import_fs7 = __toESM(require("fs"));
15268
+ import_os6 = __toESM(require("os"));
15269
+ import_path7 = __toESM(require("path"));
14948
15270
  init_dist();
14949
15271
  }
14950
15272
  });
14951
15273
 
14952
15274
  // src/utils/provenance.ts
14953
15275
  function findInPath(cmd) {
14954
- if (import_path7.default.posix.isAbsolute(cmd)) return cmd;
15276
+ if (import_path8.default.posix.isAbsolute(cmd)) return cmd;
14955
15277
  const pathEnv = process.env.PATH ?? "";
14956
- for (const dir2 of pathEnv.split(import_path7.default.delimiter)) {
15278
+ for (const dir2 of pathEnv.split(import_path8.default.delimiter)) {
14957
15279
  if (!dir2) continue;
14958
- const full = import_path7.default.join(dir2, cmd);
15280
+ const full = import_path8.default.join(dir2, cmd);
14959
15281
  try {
14960
- import_fs7.default.accessSync(full, import_fs7.default.constants.X_OK);
15282
+ import_fs8.default.accessSync(full, import_fs8.default.constants.X_OK);
14961
15283
  return full;
14962
15284
  } catch {
14963
15285
  }
@@ -14968,7 +15290,7 @@ function _classifyPath(resolved, cwd) {
14968
15290
  if (cwd && resolved.startsWith(cwd + "/")) {
14969
15291
  return { trustLevel: "user", reason: "binary in project directory" };
14970
15292
  }
14971
- const osTmp = import_os6.default.tmpdir();
15293
+ const osTmp = import_os7.default.tmpdir();
14972
15294
  const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
14973
15295
  if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
14974
15296
  return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
@@ -14986,7 +15308,7 @@ function _classifyPath(resolved, cwd) {
14986
15308
  }
14987
15309
  function checkProvenance(cmd, cwd) {
14988
15310
  const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
14989
- if (import_path7.default.posix.isAbsolute(bare)) {
15311
+ if (import_path8.default.posix.isAbsolute(bare)) {
14990
15312
  const early = _classifyPath(bare, cwd);
14991
15313
  if (early.trustLevel === "suspect") {
14992
15314
  return { resolvedPath: bare, ...early };
@@ -15002,7 +15324,7 @@ function checkProvenance(cmd, cwd) {
15002
15324
  reason: "binary not found in PATH"
15003
15325
  };
15004
15326
  }
15005
- resolved = import_fs7.default.realpathSync(found);
15327
+ resolved = import_fs8.default.realpathSync(found);
15006
15328
  } catch {
15007
15329
  return {
15008
15330
  resolvedPath: cmd,
@@ -15011,7 +15333,7 @@ function checkProvenance(cmd, cwd) {
15011
15333
  };
15012
15334
  }
15013
15335
  try {
15014
- const stat = import_fs7.default.statSync(resolved);
15336
+ const stat = import_fs8.default.statSync(resolved);
15015
15337
  if (stat.mode & 2) {
15016
15338
  return {
15017
15339
  resolvedPath: resolved,
@@ -15029,21 +15351,21 @@ function checkProvenance(cmd, cwd) {
15029
15351
  const classify = _classifyPath(resolved, cwd);
15030
15352
  return { resolvedPath: resolved, ...classify };
15031
15353
  }
15032
- var import_fs7, import_path7, import_os6, SYSTEM_PREFIXES, MANAGED_PREFIXES, USER_PREFIXES, SUSPECT_PREFIXES;
15354
+ var import_fs8, import_path8, import_os7, SYSTEM_PREFIXES, MANAGED_PREFIXES, USER_PREFIXES, SUSPECT_PREFIXES;
15033
15355
  var init_provenance = __esm({
15034
15356
  "src/utils/provenance.ts"() {
15035
15357
  "use strict";
15036
- import_fs7 = __toESM(require("fs"));
15037
- import_path7 = __toESM(require("path"));
15038
- import_os6 = __toESM(require("os"));
15358
+ import_fs8 = __toESM(require("fs"));
15359
+ import_path8 = __toESM(require("path"));
15360
+ import_os7 = __toESM(require("os"));
15039
15361
  SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
15040
15362
  MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
15041
15363
  USER_PREFIXES = [
15042
- import_path7.default.join(import_os6.default.homedir(), "bin"),
15043
- import_path7.default.join(import_os6.default.homedir(), ".local", "bin"),
15044
- import_path7.default.join(import_os6.default.homedir(), ".cargo", "bin"),
15045
- import_path7.default.join(import_os6.default.homedir(), ".npm-global", "bin"),
15046
- import_path7.default.join(import_os6.default.homedir(), ".volta", "bin")
15364
+ import_path8.default.join(import_os7.default.homedir(), "bin"),
15365
+ import_path8.default.join(import_os7.default.homedir(), ".local", "bin"),
15366
+ import_path8.default.join(import_os7.default.homedir(), ".cargo", "bin"),
15367
+ import_path8.default.join(import_os7.default.homedir(), ".npm-global", "bin"),
15368
+ import_path8.default.join(import_os7.default.homedir(), ".volta", "bin")
15047
15369
  ];
15048
15370
  SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
15049
15371
  }
@@ -15052,7 +15374,7 @@ var init_provenance = __esm({
15052
15374
  // src/policy/index.ts
15053
15375
  async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
15054
15376
  const config = opts?.config ?? getConfig(cwd);
15055
- const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, config.policy.toolInspection);
15377
+ const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, inspectionLayers(config.policy));
15056
15378
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
15057
15379
  return evaluatePolicy(
15058
15380
  config,
@@ -15084,8 +15406,11 @@ function tokenize3(toolName) {
15084
15406
  }
15085
15407
  function explainExtractShellCommand(toolName, args, toolInspection) {
15086
15408
  const shell = resolveShellInput(toolName, args, toolInspection);
15087
- const matchingPattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
15088
- const entry = matchingPattern === void 0 ? void 0 : toolInspection[matchingPattern];
15409
+ const matchingPattern = [
15410
+ ...Object.keys(toolInspection.trusted),
15411
+ ...Object.keys(toolInspection.project ?? {})
15412
+ ].find((p) => matchesPattern(toolName, p));
15413
+ const entry = inspectionEntry(toolName, toolInspection);
15089
15414
  const legacyValue = shell.kind === "not-shell" && typeof entry === "string" ? getNestedValue(args, entry) : null;
15090
15415
  return {
15091
15416
  command: shell.kind === "shell" ? shell.command : typeof legacyValue === "string" ? legacyValue : null,
@@ -15094,15 +15419,17 @@ function explainExtractShellCommand(toolName, args, toolInspection) {
15094
15419
  };
15095
15420
  }
15096
15421
  function explainIsSqlTool(toolName, toolInspection) {
15097
- const patterns = Object.keys(toolInspection);
15098
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
15099
- if (!matchingPattern) return false;
15100
- const fieldName = toolInspection[matchingPattern];
15422
+ const fieldName = inspectionEntry(toolName, toolInspection);
15101
15423
  return fieldName === "sql" || fieldName === "query";
15102
15424
  }
15103
15425
  async function explainPolicy(toolName, args, agent = EXPLAIN_AGENT) {
15104
15426
  const config = getConfig();
15105
- const prepared = prepareFileAccesses(toolName, args, process.cwd(), config.policy.toolInspection);
15427
+ const prepared = prepareFileAccesses(
15428
+ toolName,
15429
+ args,
15430
+ process.cwd(),
15431
+ inspectionLayers(config.policy)
15432
+ );
15106
15433
  const derived = await deriveExplainTrace(toolName, args, prepared);
15107
15434
  const engine = await evaluatePolicy2(toolName, args, agent, prepared.cwd, {
15108
15435
  fileAccess: prepared,
@@ -15131,10 +15458,11 @@ async function explainPolicy(toolName, args, agent = EXPLAIN_AGENT) {
15131
15458
  }
15132
15459
  async function deriveExplainTrace(toolName, args, prepared) {
15133
15460
  const steps = [];
15134
- const globalPath = import_path8.default.join(import_os7.default.homedir(), ".node9", "config.json");
15135
- const projectPath = import_path8.default.join(process.cwd(), "node9.config.json");
15136
- const credsPath = import_path8.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
15461
+ const globalPath = import_path9.default.join(import_os8.default.homedir(), ".node9", "config.json");
15462
+ const projectPath = import_path9.default.join(process.cwd(), "node9.config.json");
15463
+ const credsPath = import_path9.default.join(import_os8.default.homedir(), ".node9", "credentials.json");
15137
15464
  const config = getConfig();
15465
+ const layers = inspectionLayers(config.policy);
15138
15466
  const wsGoverned = config.policySource === "workspace";
15139
15467
  const waterfall = [
15140
15468
  {
@@ -15146,22 +15474,22 @@ async function deriveExplainTrace(toolName, args, prepared) {
15146
15474
  {
15147
15475
  tier: 2,
15148
15476
  label: wsGoverned ? "Workspace config" : "Cloud policy",
15149
- status: import_fs8.default.existsSync(credsPath) ? "active" : "missing",
15150
- note: import_fs8.default.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"
15477
+ status: import_fs9.default.existsSync(credsPath) ? "active" : "missing",
15478
+ note: import_fs9.default.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"
15151
15479
  },
15152
15480
  {
15153
15481
  tier: 3,
15154
15482
  label: "Project config",
15155
- status: import_fs8.default.existsSync(projectPath) ? "active" : "missing",
15483
+ status: import_fs9.default.existsSync(projectPath) ? "active" : "missing",
15156
15484
  path: projectPath,
15157
- ...wsGoverned && import_fs8.default.existsSync(projectPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15485
+ ...wsGoverned && import_fs9.default.existsSync(projectPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15158
15486
  },
15159
15487
  {
15160
15488
  tier: 4,
15161
15489
  label: "Global config",
15162
- status: import_fs8.default.existsSync(globalPath) ? "active" : "missing",
15490
+ status: import_fs9.default.existsSync(globalPath) ? "active" : "missing",
15163
15491
  path: globalPath,
15164
- ...wsGoverned && import_fs8.default.existsSync(globalPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15492
+ ...wsGoverned && import_fs9.default.existsSync(globalPath) ? { note: "present \u2014 ignored (workspace config governs policy)" } : {}
15165
15493
  },
15166
15494
  {
15167
15495
  tier: 5,
@@ -15212,10 +15540,7 @@ async function deriveExplainTrace(toolName, args, prepared) {
15212
15540
  });
15213
15541
  if (config.policy.smartRules.length > 0) {
15214
15542
  const matchedRule = config.policy.smartRules.find(
15215
- (rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && evaluateSmartConditions(
15216
- shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
15217
- rule
15218
- )
15543
+ (rule) => toolMatchesRule(toolName, rule.tool, layers) && evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule)
15219
15544
  );
15220
15545
  if (matchedRule) {
15221
15546
  const label3 = `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`;
@@ -15257,7 +15582,7 @@ async function deriveExplainTrace(toolName, args, prepared) {
15257
15582
  command: shellCommand,
15258
15583
  matchingPattern,
15259
15584
  fieldName
15260
- } = explainExtractShellCommand(toolName, args, config.policy.toolInspection);
15585
+ } = explainExtractShellCommand(toolName, args, layers);
15261
15586
  if (shellCommand) {
15262
15587
  const analyzed = analyzeShellCommand(shellCommand);
15263
15588
  allTokens = analyzed.allTokens;
@@ -15308,7 +15633,7 @@ async function deriveExplainTrace(toolName, args, prepared) {
15308
15633
  outcome: "checked",
15309
15634
  detail: "No dangerous eval detected"
15310
15635
  });
15311
- if (explainIsSqlTool(toolName, config.policy.toolInspection)) {
15636
+ if (explainIsSqlTool(toolName, layers)) {
15312
15637
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS2.has(t.toLowerCase()));
15313
15638
  steps.push({
15314
15639
  name: "SQL token stripping",
@@ -15419,13 +15744,13 @@ async function deriveExplainTrace(toolName, args, prepared) {
15419
15744
  });
15420
15745
  return { tool: toolName, args, waterfall, steps, decision: "allow" };
15421
15746
  }
15422
- var import_fs8, import_path8, import_os7, SQL_DML_KEYWORDS2, EXPLAIN_AGENT;
15747
+ var import_fs9, import_path9, import_os8, SQL_DML_KEYWORDS2, EXPLAIN_AGENT;
15423
15748
  var init_policy = __esm({
15424
15749
  "src/policy/index.ts"() {
15425
15750
  "use strict";
15426
- import_fs8 = __toESM(require("fs"));
15427
- import_path8 = __toESM(require("path"));
15428
- import_os7 = __toESM(require("os"));
15751
+ import_fs9 = __toESM(require("fs"));
15752
+ import_path9 = __toESM(require("path"));
15753
+ import_os8 = __toESM(require("os"));
15429
15754
  init_dlp();
15430
15755
  init_file_access();
15431
15756
  init_config();
@@ -15441,34 +15766,34 @@ var init_policy = __esm({
15441
15766
 
15442
15767
  // src/utils/atomic-write.ts
15443
15768
  function atomicWriteSync(filePath, data, options) {
15444
- const dir2 = import_path9.default.dirname(filePath);
15445
- if (!import_fs9.default.existsSync(dir2)) import_fs9.default.mkdirSync(dir2, { recursive: true });
15769
+ const dir2 = import_path10.default.dirname(filePath);
15770
+ if (!import_fs10.default.existsSync(dir2)) import_fs10.default.mkdirSync(dir2, { recursive: true });
15446
15771
  const tmpPath = `${filePath}.${(0, import_crypto7.randomUUID)()}.tmp`;
15447
15772
  try {
15448
- import_fs9.default.writeFileSync(tmpPath, data, options);
15773
+ import_fs10.default.writeFileSync(tmpPath, data, options);
15449
15774
  } catch (err2) {
15450
15775
  try {
15451
- import_fs9.default.unlinkSync(tmpPath);
15776
+ import_fs10.default.unlinkSync(tmpPath);
15452
15777
  } catch {
15453
15778
  }
15454
15779
  throw err2;
15455
15780
  }
15456
15781
  try {
15457
- import_fs9.default.renameSync(tmpPath, filePath);
15782
+ import_fs10.default.renameSync(tmpPath, filePath);
15458
15783
  } catch (err2) {
15459
15784
  try {
15460
- import_fs9.default.unlinkSync(tmpPath);
15785
+ import_fs10.default.unlinkSync(tmpPath);
15461
15786
  } catch {
15462
15787
  }
15463
15788
  throw err2;
15464
15789
  }
15465
15790
  }
15466
- var import_fs9, import_path9, import_crypto7;
15791
+ var import_fs10, import_path10, import_crypto7;
15467
15792
  var init_atomic_write = __esm({
15468
15793
  "src/utils/atomic-write.ts"() {
15469
15794
  "use strict";
15470
- import_fs9 = __toESM(require("fs"));
15471
- import_path9 = __toESM(require("path"));
15795
+ import_fs10 = __toESM(require("fs"));
15796
+ import_path10 = __toESM(require("path"));
15472
15797
  import_crypto7 = require("crypto");
15473
15798
  }
15474
15799
  });
@@ -15485,11 +15810,11 @@ function extractCommandPattern(toolName, args) {
15485
15810
  }
15486
15811
  function checkPause() {
15487
15812
  try {
15488
- if (!import_fs10.default.existsSync(PAUSED_FILE)) return { paused: false };
15489
- const state = JSON.parse(import_fs10.default.readFileSync(PAUSED_FILE, "utf-8"));
15813
+ if (!import_fs11.default.existsSync(PAUSED_FILE)) return { paused: false };
15814
+ const state = JSON.parse(import_fs11.default.readFileSync(PAUSED_FILE, "utf-8"));
15490
15815
  if (state.expiry > 0 && Date.now() >= state.expiry) {
15491
15816
  try {
15492
- import_fs10.default.unlinkSync(PAUSED_FILE);
15817
+ import_fs11.default.unlinkSync(PAUSED_FILE);
15493
15818
  } catch {
15494
15819
  }
15495
15820
  return { paused: false };
@@ -15505,14 +15830,14 @@ function pauseNode9(durationMs, durationStr) {
15505
15830
  }
15506
15831
  function resumeNode9() {
15507
15832
  try {
15508
- if (import_fs10.default.existsSync(PAUSED_FILE)) import_fs10.default.unlinkSync(PAUSED_FILE);
15833
+ if (import_fs11.default.existsSync(PAUSED_FILE)) import_fs11.default.unlinkSync(PAUSED_FILE);
15509
15834
  } catch {
15510
15835
  }
15511
15836
  }
15512
15837
  function getActiveTrustSession(toolName, args) {
15513
15838
  try {
15514
- if (!import_fs10.default.existsSync(TRUST_FILE)) return false;
15515
- const trust = JSON.parse(import_fs10.default.readFileSync(TRUST_FILE, "utf-8"));
15839
+ if (!import_fs11.default.existsSync(TRUST_FILE)) return false;
15840
+ const trust = JSON.parse(import_fs11.default.readFileSync(TRUST_FILE, "utf-8"));
15516
15841
  const now = Date.now();
15517
15842
  const active = trust.entries.filter((e) => e.expiry > now);
15518
15843
  if (active.length !== trust.entries.length) {
@@ -15535,8 +15860,8 @@ function writeTrustSession(toolName, durationMs, args) {
15535
15860
  try {
15536
15861
  let trust = { entries: [] };
15537
15862
  try {
15538
- if (import_fs10.default.existsSync(TRUST_FILE)) {
15539
- trust = JSON.parse(import_fs10.default.readFileSync(TRUST_FILE, "utf-8"));
15863
+ if (import_fs11.default.existsSync(TRUST_FILE)) {
15864
+ trust = JSON.parse(import_fs11.default.readFileSync(TRUST_FILE, "utf-8"));
15540
15865
  }
15541
15866
  } catch {
15542
15867
  }
@@ -15558,26 +15883,26 @@ function writeTrustSession(toolName, durationMs, args) {
15558
15883
  }
15559
15884
  function getPersistentDecision(toolName) {
15560
15885
  try {
15561
- const file2 = import_path10.default.join(import_os8.default.homedir(), ".node9", "decisions.json");
15562
- if (!import_fs10.default.existsSync(file2)) return null;
15563
- const decisions = JSON.parse(import_fs10.default.readFileSync(file2, "utf-8"));
15886
+ const file2 = import_path11.default.join(import_os9.default.homedir(), ".node9", "decisions.json");
15887
+ if (!import_fs11.default.existsSync(file2)) return null;
15888
+ const decisions = JSON.parse(import_fs11.default.readFileSync(file2, "utf-8"));
15564
15889
  const d = decisions[toolName];
15565
15890
  if (d === "allow" || d === "deny") return d;
15566
15891
  } catch {
15567
15892
  }
15568
15893
  return null;
15569
15894
  }
15570
- var import_fs10, import_path10, import_os8, PAUSED_FILE, TRUST_FILE;
15895
+ var import_fs11, import_path11, import_os9, PAUSED_FILE, TRUST_FILE;
15571
15896
  var init_state = __esm({
15572
15897
  "src/auth/state.ts"() {
15573
15898
  "use strict";
15574
- import_fs10 = __toESM(require("fs"));
15575
- import_path10 = __toESM(require("path"));
15576
- import_os8 = __toESM(require("os"));
15899
+ import_fs11 = __toESM(require("fs"));
15900
+ import_path11 = __toESM(require("path"));
15901
+ import_os9 = __toESM(require("os"));
15577
15902
  init_policy();
15578
15903
  init_atomic_write();
15579
- PAUSED_FILE = import_path10.default.join(import_os8.default.homedir(), ".node9", "PAUSED");
15580
- TRUST_FILE = import_path10.default.join(import_os8.default.homedir(), ".node9", "trust.json");
15904
+ PAUSED_FILE = import_path11.default.join(import_os9.default.homedir(), ".node9", "PAUSED");
15905
+ TRUST_FILE = import_path11.default.join(import_os9.default.homedir(), ".node9", "trust.json");
15581
15906
  }
15582
15907
  });
15583
15908
 
@@ -15620,9 +15945,9 @@ function resolveDaemonPort(env = process.env) {
15620
15945
  }
15621
15946
  function getInternalToken() {
15622
15947
  try {
15623
- const pidFile = import_path11.default.join(import_os9.default.homedir(), ".node9", "daemon.pid");
15624
- if (!import_fs11.default.existsSync(pidFile)) return null;
15625
- const data = JSON.parse(import_fs11.default.readFileSync(pidFile, "utf-8"));
15948
+ const pidFile = import_path12.default.join(import_os10.default.homedir(), ".node9", "daemon.pid");
15949
+ if (!import_fs12.default.existsSync(pidFile)) return null;
15950
+ const data = JSON.parse(import_fs12.default.readFileSync(pidFile, "utf-8"));
15626
15951
  process.kill(data.pid, 0);
15627
15952
  return data.internalToken ?? null;
15628
15953
  } catch {
@@ -15630,12 +15955,12 @@ function getInternalToken() {
15630
15955
  }
15631
15956
  }
15632
15957
  function isDaemonRunning() {
15633
- const pidFile = import_path11.default.join(import_os9.default.homedir(), ".node9", "daemon.pid");
15634
- if (import_fs11.default.existsSync(pidFile)) {
15958
+ const pidFile = import_path12.default.join(import_os10.default.homedir(), ".node9", "daemon.pid");
15959
+ if (import_fs12.default.existsSync(pidFile)) {
15635
15960
  let pid;
15636
15961
  let port;
15637
15962
  try {
15638
- const data = JSON.parse(import_fs11.default.readFileSync(pidFile, "utf-8"));
15963
+ const data = JSON.parse(import_fs12.default.readFileSync(pidFile, "utf-8"));
15639
15964
  pid = data.pid;
15640
15965
  port = data.port;
15641
15966
  } catch {
@@ -15653,7 +15978,7 @@ function isDaemonRunning() {
15653
15978
  } catch (err2) {
15654
15979
  if (err2 instanceof Error && "code" in err2 && err2.code === "ESRCH") {
15655
15980
  try {
15656
- import_fs11.default.unlinkSync(pidFile);
15981
+ import_fs12.default.unlinkSync(pidFile);
15657
15982
  } catch {
15658
15983
  }
15659
15984
  }
@@ -15967,15 +16292,15 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
15967
16292
  signal: AbortSignal.timeout(3e3)
15968
16293
  });
15969
16294
  }
15970
- var import_fs11, import_net, import_path11, import_os9, ACTIVITY_SOCKET_PATH, DEFAULT_DAEMON_PORT, DAEMON_PORT, DAEMON_HOST;
16295
+ var import_fs12, import_net, import_path12, import_os10, ACTIVITY_SOCKET_PATH, DEFAULT_DAEMON_PORT, DAEMON_PORT, DAEMON_HOST;
15971
16296
  var init_daemon = __esm({
15972
16297
  "src/auth/daemon.ts"() {
15973
16298
  "use strict";
15974
- import_fs11 = __toESM(require("fs"));
16299
+ import_fs12 = __toESM(require("fs"));
15975
16300
  import_net = __toESM(require("net"));
15976
- import_path11 = __toESM(require("path"));
15977
- import_os9 = __toESM(require("os"));
15978
- ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path11.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
16301
+ import_path12 = __toESM(require("path"));
16302
+ import_os10 = __toESM(require("os"));
16303
+ ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path12.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
15979
16304
  DEFAULT_DAEMON_PORT = 7391;
15980
16305
  DAEMON_PORT = resolveDaemonPort();
15981
16306
  DAEMON_HOST = "127.0.0.1";
@@ -16034,7 +16359,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
16034
16359
  intent = "EDIT";
16035
16360
  if (obj.file_path) {
16036
16361
  editFilePath = String(obj.file_path);
16037
- editFileName = import_path12.default.basename(editFilePath);
16362
+ editFileName = import_path13.default.basename(editFilePath);
16038
16363
  }
16039
16364
  const result = extractContext(String(obj.new_string), matchedWord);
16040
16365
  contextSnippet = result.snippet;
@@ -16066,11 +16391,11 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
16066
16391
  ...ruleName && { ruleName }
16067
16392
  };
16068
16393
  }
16069
- var import_path12, CODE_KEYS;
16394
+ var import_path13, CODE_KEYS;
16070
16395
  var init_context_sniper = __esm({
16071
16396
  "src/context-sniper.ts"() {
16072
16397
  "use strict";
16073
- import_path12 = __toESM(require("path"));
16398
+ import_path13 = __toESM(require("path"));
16074
16399
  CODE_KEYS = [
16075
16400
  "command",
16076
16401
  "cmd",
@@ -16126,7 +16451,7 @@ function formatArgs(args, matchedField, matchedWord) {
16126
16451
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
16127
16452
  const obj = parsed;
16128
16453
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
16129
- const file2 = obj.file_path ? import_path13.default.basename(String(obj.file_path)) : "file";
16454
+ const file2 = obj.file_path ? import_path14.default.basename(String(obj.file_path)) : "file";
16130
16455
  const oldPreview = smartTruncate(String(obj.old_string), 120);
16131
16456
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
16132
16457
  return {
@@ -16343,12 +16668,12 @@ end run`;
16343
16668
  }
16344
16669
  });
16345
16670
  }
16346
- var import_child_process, import_path13, isTestEnv, MIN_INTERACTION_MS;
16671
+ var import_child_process, import_path14, isTestEnv, MIN_INTERACTION_MS;
16347
16672
  var init_native = __esm({
16348
16673
  "src/ui/native.ts"() {
16349
16674
  "use strict";
16350
16675
  import_child_process = require("child_process");
16351
- import_path13 = __toESM(require("path"));
16676
+ import_path14 = __toESM(require("path"));
16352
16677
  init_context_sniper();
16353
16678
  init_safe_text();
16354
16679
  isTestEnv = () => {
@@ -16360,12 +16685,12 @@ var init_native = __esm({
16360
16685
 
16361
16686
  // src/shields/jail.ts
16362
16687
  function jailStorePath() {
16363
- return import_path14.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
16688
+ return import_path15.default.join(import_os11.default.homedir(), ".node9", "jail-paths.json");
16364
16689
  }
16365
16690
  function readJailPaths() {
16366
16691
  let text;
16367
16692
  try {
16368
- text = import_fs12.default.readFileSync(jailStorePath(), "utf8");
16693
+ text = import_fs13.default.readFileSync(jailStorePath(), "utf8");
16369
16694
  } catch (err2) {
16370
16695
  if (err2.code === "ENOENT") return [];
16371
16696
  throw err2;
@@ -16383,8 +16708,8 @@ function readJailPaths() {
16383
16708
  }
16384
16709
  function writeJailPaths(paths) {
16385
16710
  const p = jailStorePath();
16386
- import_fs12.default.mkdirSync(import_path14.default.dirname(p), { recursive: true });
16387
- import_fs12.default.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
16711
+ import_fs13.default.mkdirSync(import_path15.default.dirname(p), { recursive: true });
16712
+ import_fs13.default.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
16388
16713
  }
16389
16714
  function addJailPath(rawPath, verdict) {
16390
16715
  const norm = rawPath.trim();
@@ -16423,14 +16748,14 @@ function removeJailPath(rawPath) {
16423
16748
  return { removed, paths: after };
16424
16749
  }
16425
16750
  function regenerateUserJail(paths) {
16426
- const file2 = import_path14.default.join(USER_SHIELDS_DIR_PATH, `${USER_JAIL_SHIELD}.json`);
16751
+ const file2 = import_path15.default.join(USER_SHIELDS_DIR_PATH, `${USER_JAIL_SHIELD}.json`);
16427
16752
  if (paths.length === 0) {
16428
16753
  const active2 = readActiveShields();
16429
16754
  if (active2.includes(USER_JAIL_SHIELD)) {
16430
16755
  writeActiveShields(active2.filter((s) => s !== USER_JAIL_SHIELD));
16431
16756
  }
16432
16757
  try {
16433
- import_fs12.default.rmSync(file2, { force: true });
16758
+ import_fs13.default.rmSync(file2, { force: true });
16434
16759
  } catch {
16435
16760
  }
16436
16761
  return;
@@ -16445,13 +16770,13 @@ function regenerateUserJail(paths) {
16445
16770
  const active = readActiveShields();
16446
16771
  if (!active.includes(USER_JAIL_SHIELD)) writeActiveShields([...active, USER_JAIL_SHIELD]);
16447
16772
  }
16448
- var import_fs12, import_os10, import_path14, USER_JAIL_SHIELD;
16773
+ var import_fs13, import_os11, import_path15, USER_JAIL_SHIELD;
16449
16774
  var init_jail = __esm({
16450
16775
  "src/shields/jail.ts"() {
16451
16776
  "use strict";
16452
- import_fs12 = __toESM(require("fs"));
16453
- import_os10 = __toESM(require("os"));
16454
- import_path14 = __toESM(require("path"));
16777
+ import_fs13 = __toESM(require("fs"));
16778
+ import_os11 = __toESM(require("os"));
16779
+ import_path15 = __toESM(require("path"));
16455
16780
  init_build();
16456
16781
  init_dist();
16457
16782
  init_shields();
@@ -16461,7 +16786,7 @@ var init_jail = __esm({
16461
16786
 
16462
16787
  // src/canary/registry.ts
16463
16788
  function canaryStorePath() {
16464
- return import_path15.default.join(import_os11.default.homedir(), ".node9", "canaries.json");
16789
+ return import_path16.default.join(import_os12.default.homedir(), ".node9", "canaries.json");
16465
16790
  }
16466
16791
  function isRecord(x) {
16467
16792
  if (!x || typeof x !== "object") return false;
@@ -16472,7 +16797,7 @@ function loadCanaries(opts) {
16472
16797
  const p = canaryStorePath();
16473
16798
  let raw;
16474
16799
  try {
16475
- raw = import_fs13.default.readFileSync(p, "utf-8");
16800
+ raw = import_fs14.default.readFileSync(p, "utf-8");
16476
16801
  } catch (e) {
16477
16802
  if (e.code === "ENOENT") return [];
16478
16803
  throw e;
@@ -16494,19 +16819,19 @@ function saveCanaries(records) {
16494
16819
  const p = canaryStorePath();
16495
16820
  const paths = addJailPath(p, "block");
16496
16821
  regenerateUserJail(paths);
16497
- import_fs13.default.mkdirSync(import_path15.default.dirname(p), { recursive: true });
16822
+ import_fs14.default.mkdirSync(import_path16.default.dirname(p), { recursive: true });
16498
16823
  const tmp = `${p}.${(0, import_crypto8.randomBytes)(8).toString("hex")}.tmp`;
16499
16824
  try {
16500
- import_fs13.default.writeFileSync(tmp, JSON.stringify({ version: 1, records }, null, 2) + "\n", {
16825
+ import_fs14.default.writeFileSync(tmp, JSON.stringify({ version: 1, records }, null, 2) + "\n", {
16501
16826
  mode: 384,
16502
16827
  flag: "wx"
16503
16828
  });
16504
- import_fs13.default.chmodSync(tmp, 384);
16505
- import_fs13.default.renameSync(tmp, p);
16829
+ import_fs14.default.chmodSync(tmp, 384);
16830
+ import_fs14.default.renameSync(tmp, p);
16506
16831
  } finally {
16507
- if (import_fs13.default.existsSync(tmp)) {
16832
+ if (import_fs14.default.existsSync(tmp)) {
16508
16833
  try {
16509
- import_fs13.default.unlinkSync(tmp);
16834
+ import_fs14.default.unlinkSync(tmp);
16510
16835
  } catch {
16511
16836
  }
16512
16837
  }
@@ -16545,13 +16870,13 @@ function canaryCtxValues() {
16545
16870
  path: r.path
16546
16871
  }));
16547
16872
  }
16548
- var import_fs13, import_os11, import_path15, import_crypto8, sha256Hex;
16873
+ var import_fs14, import_os12, import_path16, import_crypto8, sha256Hex;
16549
16874
  var init_registry = __esm({
16550
16875
  "src/canary/registry.ts"() {
16551
16876
  "use strict";
16552
- import_fs13 = __toESM(require("fs"));
16553
- import_os11 = __toESM(require("os"));
16554
- import_path15 = __toESM(require("path"));
16877
+ import_fs14 = __toESM(require("fs"));
16878
+ import_os12 = __toESM(require("os"));
16879
+ import_path16 = __toESM(require("path"));
16555
16880
  import_crypto8 = require("crypto");
16556
16881
  init_dist();
16557
16882
  init_jail();
@@ -16563,16 +16888,16 @@ var init_registry = __esm({
16563
16888
  function readSmallFile(p, max) {
16564
16889
  let fd;
16565
16890
  try {
16566
- fd = import_fs14.default.openSync(p, import_fs14.default.constants.O_RDONLY | (import_fs14.default.constants.O_NOFOLLOW ?? 0));
16567
- const st = import_fs14.default.fstatSync(fd);
16891
+ fd = import_fs15.default.openSync(p, import_fs15.default.constants.O_RDONLY | (import_fs15.default.constants.O_NOFOLLOW ?? 0));
16892
+ const st = import_fs15.default.fstatSync(fd);
16568
16893
  if (!st.isFile() || st.size > max) return null;
16569
16894
  const buf = Buffer.alloc(st.size);
16570
- const n = import_fs14.default.readSync(fd, buf, 0, st.size, 0);
16895
+ const n = import_fs15.default.readSync(fd, buf, 0, st.size, 0);
16571
16896
  return buf.subarray(0, n).toString("utf8");
16572
16897
  } catch {
16573
16898
  return null;
16574
16899
  } finally {
16575
- if (fd !== void 0) import_fs14.default.closeSync(fd);
16900
+ if (fd !== void 0) import_fs15.default.closeSync(fd);
16576
16901
  }
16577
16902
  }
16578
16903
  function gitRemoteHost(url) {
@@ -16591,20 +16916,20 @@ function gitRemoteHost(url) {
16591
16916
  return null;
16592
16917
  }
16593
16918
  function findGitConfig(dir2, walkUp = true) {
16594
- let cur = import_path16.default.resolve(dir2);
16919
+ let cur = import_path17.default.resolve(dir2);
16595
16920
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
16596
- const dotGit = import_path16.default.join(cur, ".git");
16597
- const direct = readSmallFile(import_path16.default.join(dotGit, "config"), 1e6);
16921
+ const dotGit = import_path17.default.join(cur, ".git");
16922
+ const direct = readSmallFile(import_path17.default.join(dotGit, "config"), 1e6);
16598
16923
  if (direct !== null) return direct;
16599
16924
  const pointer = readSmallFile(dotGit, 4096);
16600
16925
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
16601
16926
  if (m) {
16602
- let gitDir = import_path16.default.resolve(cur, m[1]);
16603
- const common = readSmallFile(import_path16.default.join(gitDir, "commondir"), 4096);
16604
- if (common !== null) gitDir = import_path16.default.resolve(gitDir, common.trim());
16605
- return readSmallFile(import_path16.default.join(gitDir, "config"), 1e6);
16927
+ let gitDir = import_path17.default.resolve(cur, m[1]);
16928
+ const common = readSmallFile(import_path17.default.join(gitDir, "commondir"), 4096);
16929
+ if (common !== null) gitDir = import_path17.default.resolve(gitDir, common.trim());
16930
+ return readSmallFile(import_path17.default.join(gitDir, "config"), 1e6);
16606
16931
  }
16607
- const parent = import_path16.default.dirname(cur);
16932
+ const parent = import_path17.default.dirname(cur);
16608
16933
  if (parent === cur) return null;
16609
16934
  cur = parent;
16610
16935
  }
@@ -16613,12 +16938,12 @@ function findGitConfig(dir2, walkUp = true) {
16613
16938
  function readGitRemotes(dir2, opts = {}) {
16614
16939
  if (!dir2) return [];
16615
16940
  const walkUp = opts.walkUp !== false;
16616
- const key = `${walkUp ? "up" : "at"}:${import_path16.default.resolve(dir2)}`;
16617
- const hit = cache.get(key);
16941
+ const key = `${walkUp ? "up" : "at"}:${import_path17.default.resolve(dir2)}`;
16942
+ const hit = cache2.get(key);
16618
16943
  if (hit) return hit;
16619
16944
  const out = [];
16620
16945
  try {
16621
- const cfg = findGitConfig(import_path16.default.resolve(dir2), walkUp);
16946
+ const cfg = findGitConfig(import_path17.default.resolve(dir2), walkUp);
16622
16947
  if (cfg !== null) {
16623
16948
  let current = null;
16624
16949
  for (const line of cfg.split(/\r?\n/)) {
@@ -16640,31 +16965,31 @@ function readGitRemotes(dir2, opts = {}) {
16640
16965
  }
16641
16966
  } catch {
16642
16967
  }
16643
- cache.set(key, out);
16968
+ cache2.set(key, out);
16644
16969
  return out;
16645
16970
  }
16646
- var import_fs14, import_path16, cache;
16971
+ var import_fs15, import_path17, cache2;
16647
16972
  var init_git_remotes = __esm({
16648
16973
  "src/utils/git-remotes.ts"() {
16649
16974
  "use strict";
16650
- import_fs14 = __toESM(require("fs"));
16651
- import_path16 = __toESM(require("path"));
16975
+ import_fs15 = __toESM(require("fs"));
16976
+ import_path17 = __toESM(require("path"));
16652
16977
  init_dist();
16653
- cache = /* @__PURE__ */ new Map();
16978
+ cache2 = /* @__PURE__ */ new Map();
16654
16979
  }
16655
16980
  });
16656
16981
 
16657
16982
  // src/exposure/known-hosts.ts
16658
16983
  function manifestRoot(dir2) {
16659
- let cur = import_path17.default.resolve(dir2);
16984
+ let cur = import_path18.default.resolve(dir2);
16660
16985
  for (let i = 0; i < 64; i++) {
16661
16986
  for (const [m] of ECOSYSTEMS) {
16662
16987
  try {
16663
- if (import_fs15.default.existsSync(import_path17.default.join(cur, m))) return cur;
16988
+ if (import_fs16.default.existsSync(import_path18.default.join(cur, m))) return cur;
16664
16989
  } catch {
16665
16990
  }
16666
16991
  }
16667
- const parent = import_path17.default.dirname(cur);
16992
+ const parent = import_path18.default.dirname(cur);
16668
16993
  if (parent === cur) return null;
16669
16994
  cur = parent;
16670
16995
  }
@@ -16673,8 +16998,8 @@ function manifestRoot(dir2) {
16673
16998
  function knownProjectHosts(dir2) {
16674
16999
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
16675
17000
  if (!dir2) return empty;
16676
- const key = import_path17.default.resolve(dir2);
16677
- const hit = cache2.get(key);
17001
+ const key = import_path18.default.resolve(dir2);
17002
+ const hit = cache3.get(key);
16678
17003
  if (hit) return hit;
16679
17004
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
16680
17005
  try {
@@ -16686,25 +17011,25 @@ function knownProjectHosts(dir2) {
16686
17011
  const root = manifestRoot(key);
16687
17012
  if (root !== null) {
16688
17013
  for (const [m, registry] of ECOSYSTEMS) {
16689
- if (import_fs15.default.existsSync(import_path17.default.join(root, m))) out.hosts.add(registry);
17014
+ if (import_fs16.default.existsSync(import_path18.default.join(root, m))) out.hosts.add(registry);
16690
17015
  }
16691
17016
  try {
16692
- if (import_fs15.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17017
+ if (import_fs16.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
16693
17018
  out.hosts.add("host:rubygems.org");
16694
17019
  } catch {
16695
17020
  }
16696
17021
  }
16697
17022
  } catch {
16698
17023
  }
16699
- cache2.set(key, out);
17024
+ cache3.set(key, out);
16700
17025
  return out;
16701
17026
  }
16702
- var import_fs15, import_path17, ECOSYSTEMS, cache2;
17027
+ var import_fs16, import_path18, ECOSYSTEMS, cache3;
16703
17028
  var init_known_hosts = __esm({
16704
17029
  "src/exposure/known-hosts.ts"() {
16705
17030
  "use strict";
16706
- import_fs15 = __toESM(require("fs"));
16707
- import_path17 = __toESM(require("path"));
17031
+ import_fs16 = __toESM(require("fs"));
17032
+ import_path18 = __toESM(require("path"));
16708
17033
  init_git_remotes();
16709
17034
  ECOSYSTEMS = [
16710
17035
  ["package.json", "host:registry.npmjs.org"],
@@ -16713,7 +17038,7 @@ var init_known_hosts = __esm({
16713
17038
  ["Cargo.toml", "host:crates.io"],
16714
17039
  ["Gemfile", "host:rubygems.org"]
16715
17040
  ];
16716
- cache2 = /* @__PURE__ */ new Map();
17041
+ cache3 = /* @__PURE__ */ new Map();
16717
17042
  }
16718
17043
  });
16719
17044
 
@@ -16744,7 +17069,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16744
17069
  if (typeof a.content === "string") {
16745
17070
  try {
16746
17071
  const next = JSON.parse(a.content);
16747
- const prev = JSON.parse(import_fs16.default.readFileSync(absPath, "utf8"));
17072
+ const prev = JSON.parse(import_fs17.default.readFileSync(absPath, "utf8"));
16748
17073
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
16749
17074
  } catch {
16750
17075
  return LIFECYCLE_RE.test(a.content);
@@ -16754,8 +17079,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16754
17079
  }
16755
17080
  function judgeWritesAfterExposure(opts) {
16756
17081
  const holds = [];
16757
- const home = opts.home ?? import_os12.default.homedir();
16758
- const tmp = normalizeFilePath(import_os12.default.tmpdir());
17082
+ const home = opts.home ?? import_os13.default.homedir();
17083
+ const tmp = normalizeFilePath(import_os13.default.tmpdir());
16759
17084
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
16760
17085
  const seen = /* @__PURE__ */ new Set();
16761
17086
  for (const access of opts.accesses) {
@@ -16765,7 +17090,7 @@ function judgeWritesAfterExposure(opts) {
16765
17090
  seen.add(p);
16766
17091
  if (p === "/dev" || p.startsWith("/dev/")) continue;
16767
17092
  if (root !== null && isInside(root, p)) {
16768
- if (isAgentInstructionPath(p, root) && (import_path18.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17093
+ if (isAgentInstructionPath(p, root) && (import_path19.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
16769
17094
  holds.push({ path: p, why: "agent-instruction" });
16770
17095
  continue;
16771
17096
  }
@@ -16784,13 +17109,13 @@ function judgeWritesAfterExposure(opts) {
16784
17109
  }
16785
17110
  return holds;
16786
17111
  }
16787
- var import_fs16, import_os12, import_path18, WRITE_OPS, LIFECYCLE_RE;
17112
+ var import_fs17, import_os13, import_path19, WRITE_OPS, LIFECYCLE_RE;
16788
17113
  var init_writes = __esm({
16789
17114
  "src/exposure/writes.ts"() {
16790
17115
  "use strict";
16791
- import_fs16 = __toESM(require("fs"));
16792
- import_os12 = __toESM(require("os"));
16793
- import_path18 = __toESM(require("path"));
17116
+ import_fs17 = __toESM(require("fs"));
17117
+ import_os13 = __toESM(require("os"));
17118
+ import_path19 = __toESM(require("path"));
16794
17119
  init_dist();
16795
17120
  WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
16796
17121
  LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
@@ -16868,6 +17193,7 @@ var init_exposure_store = __esm({
16868
17193
  tool: source.tool,
16869
17194
  server: source.server,
16870
17195
  ...source.word ? { word: source.word } : {},
17196
+ ...source.eid ? { eid: source.eid } : {},
16871
17197
  at: source.at || Date.now()
16872
17198
  });
16873
17199
  if (e.sources.length > MAX_SOURCES) e.sources.splice(0, e.sources.length - MAX_SOURCES);
@@ -16915,29 +17241,29 @@ var init_exposure_store = __esm({
16915
17241
 
16916
17242
  // src/exposure/marker.ts
16917
17243
  function dir() {
16918
- return import_path19.default.join(import_os13.default.homedir(), ".node9", "exposure-sessions");
17244
+ return import_path20.default.join(import_os14.default.homedir(), ".node9", "exposure-sessions");
16919
17245
  }
16920
17246
  function file(sessionId) {
16921
17247
  const id = import_crypto9.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
16922
- return import_path19.default.join(dir(), id);
17248
+ return import_path20.default.join(dir(), id);
16923
17249
  }
16924
17250
  function markSessionExposed(sessionId) {
16925
17251
  if (!sessionId) return;
16926
17252
  try {
16927
- import_fs17.default.mkdirSync(dir(), { recursive: true, mode: 448 });
17253
+ import_fs18.default.mkdirSync(dir(), { recursive: true, mode: 448 });
16928
17254
  const f = file(sessionId);
16929
17255
  const now = /* @__PURE__ */ new Date();
16930
17256
  try {
16931
- import_fs17.default.utimesSync(f, now, now);
17257
+ import_fs18.default.utimesSync(f, now, now);
16932
17258
  } catch {
16933
- import_fs17.default.writeFileSync(f, "", { mode: 384 });
17259
+ import_fs18.default.writeFileSync(f, "", { mode: 384 });
16934
17260
  }
16935
17261
  if (Math.random() < 0.05) {
16936
17262
  const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
16937
- for (const name of import_fs17.default.readdirSync(dir())) {
17263
+ for (const name of import_fs18.default.readdirSync(dir())) {
16938
17264
  try {
16939
- const p = import_path19.default.join(dir(), name);
16940
- if (import_fs17.default.statSync(p).mtimeMs < cutoff) import_fs17.default.unlinkSync(p);
17265
+ const p = import_path20.default.join(dir(), name);
17266
+ if (import_fs18.default.statSync(p).mtimeMs < cutoff) import_fs18.default.unlinkSync(p);
16941
17267
  } catch {
16942
17268
  }
16943
17269
  }
@@ -16948,7 +17274,7 @@ function markSessionExposed(sessionId) {
16948
17274
  function maybeSessionExposed(sessionId) {
16949
17275
  if (!sessionId) return false;
16950
17276
  try {
16951
- const st = import_fs17.default.statSync(file(sessionId));
17277
+ const st = import_fs18.default.statSync(file(sessionId));
16952
17278
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
16953
17279
  } catch (e) {
16954
17280
  return e.code !== "ENOENT";
@@ -16956,19 +17282,19 @@ function maybeSessionExposed(sessionId) {
16956
17282
  }
16957
17283
  function clearSessionMarkers(sessionId) {
16958
17284
  try {
16959
- if (sessionId) import_fs17.default.unlinkSync(file(sessionId));
16960
- else import_fs17.default.rmSync(dir(), { recursive: true, force: true });
17285
+ if (sessionId) import_fs18.default.unlinkSync(file(sessionId));
17286
+ else import_fs18.default.rmSync(dir(), { recursive: true, force: true });
16961
17287
  } catch {
16962
17288
  }
16963
17289
  }
16964
- var import_crypto9, import_fs17, import_os13, import_path19;
17290
+ var import_crypto9, import_fs18, import_os14, import_path20;
16965
17291
  var init_marker = __esm({
16966
17292
  "src/exposure/marker.ts"() {
16967
17293
  "use strict";
16968
17294
  import_crypto9 = __toESM(require("crypto"));
16969
- import_fs17 = __toESM(require("fs"));
16970
- import_os13 = __toESM(require("os"));
16971
- import_path19 = __toESM(require("path"));
17295
+ import_fs18 = __toESM(require("fs"));
17296
+ import_os14 = __toESM(require("os"));
17297
+ import_path20 = __toESM(require("path"));
16972
17298
  init_exposure_store();
16973
17299
  }
16974
17300
  });
@@ -17050,13 +17376,13 @@ function deriveServerName(cmd) {
17050
17376
  return strip(base2) || "MCP Server";
17051
17377
  }
17052
17378
  function getMcpToolsFile() {
17053
- return import_path20.default.join(import_os14.default.homedir(), ".node9", "mcp-tools.json");
17379
+ return import_path21.default.join(import_os15.default.homedir(), ".node9", "mcp-tools.json");
17054
17380
  }
17055
17381
  function readMcpToolsConfig() {
17056
17382
  try {
17057
17383
  const file2 = getMcpToolsFile();
17058
- if (!import_fs18.default.existsSync(file2)) return {};
17059
- const raw = import_fs18.default.readFileSync(file2, "utf-8");
17384
+ if (!import_fs19.default.existsSync(file2)) return {};
17385
+ const raw = import_fs19.default.readFileSync(file2, "utf-8");
17060
17386
  return JSON.parse(raw);
17061
17387
  } catch {
17062
17388
  return {};
@@ -17065,11 +17391,11 @@ function readMcpToolsConfig() {
17065
17391
  function writeMcpToolsConfig(config) {
17066
17392
  try {
17067
17393
  const file2 = getMcpToolsFile();
17068
- const dir2 = import_path20.default.dirname(file2);
17069
- if (!import_fs18.default.existsSync(dir2)) import_fs18.default.mkdirSync(dir2, { recursive: true });
17070
- const tmpPath = `${file2}.${import_os14.default.hostname()}.${process.pid}.tmp`;
17071
- import_fs18.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
17072
- import_fs18.default.renameSync(tmpPath, file2);
17394
+ const dir2 = import_path21.default.dirname(file2);
17395
+ if (!import_fs19.default.existsSync(dir2)) import_fs19.default.mkdirSync(dir2, { recursive: true });
17396
+ const tmpPath = `${file2}.${import_os15.default.hostname()}.${process.pid}.tmp`;
17397
+ import_fs19.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
17398
+ import_fs19.default.renameSync(tmpPath, file2);
17073
17399
  } catch (e) {
17074
17400
  console.error("Failed to write mcp-tools.json", e);
17075
17401
  }
@@ -17124,13 +17450,13 @@ function approveServer(serverKey, disabledTools) {
17124
17450
  writeMcpToolsConfig(config);
17125
17451
  }
17126
17452
  }
17127
- var import_fs18, import_path20, import_os14;
17453
+ var import_fs19, import_path21, import_os15;
17128
17454
  var init_mcp_tools = __esm({
17129
17455
  "src/daemon/mcp-tools.ts"() {
17130
17456
  "use strict";
17131
- import_fs18 = __toESM(require("fs"));
17132
- import_path20 = __toESM(require("path"));
17133
- import_os14 = __toESM(require("os"));
17457
+ import_fs19 = __toESM(require("fs"));
17458
+ import_path21 = __toESM(require("path"));
17459
+ import_os15 = __toESM(require("os"));
17134
17460
  init_mcp_cmd();
17135
17461
  }
17136
17462
  });
@@ -17141,7 +17467,7 @@ function foldServerName(name) {
17141
17467
  }
17142
17468
  function inventoryFileStamp() {
17143
17469
  try {
17144
- const st = import_fs19.default.statSync(getMcpToolsFile());
17470
+ const st = import_fs20.default.statSync(getMcpToolsFile());
17145
17471
  return `${st.mtimeMs}:${st.size}`;
17146
17472
  } catch {
17147
17473
  return "";
@@ -17205,11 +17531,11 @@ function sourceWord(toolName) {
17205
17531
  if (BUILTIN_SOURCES.has(bare) || /browser|web|http|search/.test(server)) return "web page";
17206
17532
  return "tool result";
17207
17533
  }
17208
- var import_fs19, EXPOSURE_LABEL, AGENT_INTERNAL_PREFIXES, inventoryCache, inventoryStamp, BUILTIN_SOURCES, MCP_RE;
17534
+ var import_fs20, EXPOSURE_LABEL, AGENT_INTERNAL_PREFIXES, inventoryCache, inventoryStamp, BUILTIN_SOURCES, MCP_RE;
17209
17535
  var init_roles = __esm({
17210
17536
  "src/exposure/roles.ts"() {
17211
17537
  "use strict";
17212
- import_fs19 = __toESM(require("fs"));
17538
+ import_fs20 = __toESM(require("fs"));
17213
17539
  init_mcp_tools();
17214
17540
  EXPOSURE_LABEL = "\u{1F6E1}\uFE0F Node9 Exposure";
17215
17541
  AGENT_INTERNAL_PREFIXES = ["ccd_", "visualize"];
@@ -17237,82 +17563,6 @@ var init_roles = __esm({
17237
17563
  }
17238
17564
  });
17239
17565
 
17240
- // src/auth/trusted-recipients.ts
17241
- function getTrustedRecipientsPath() {
17242
- return import_path21.default.join(import_os15.default.homedir(), ".node9", "trusted-recipients.json");
17243
- }
17244
- function readTrustedRecipients() {
17245
- try {
17246
- const raw = import_fs20.default.readFileSync(getTrustedRecipientsPath(), "utf8");
17247
- const parsed = JSON.parse(raw);
17248
- return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
17249
- } catch {
17250
- return [];
17251
- }
17252
- }
17253
- function writeTrustedRecipients(entries) {
17254
- const filePath = getTrustedRecipientsPath();
17255
- import_fs20.default.mkdirSync(import_path21.default.dirname(filePath), { recursive: true });
17256
- const tmp = filePath + ".node9-tmp";
17257
- import_fs20.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
17258
- import_fs20.default.renameSync(tmp, filePath);
17259
- }
17260
- function normalizeRecipient(raw) {
17261
- const v = raw.trim();
17262
- if (!v) return null;
17263
- if (/^(email|host|iban):/i.test(v)) {
17264
- const i = v.indexOf(":");
17265
- const kind = v.slice(0, i).toLowerCase();
17266
- const inner = normalizeRecipient(v.slice(i + 1));
17267
- return inner && inner.startsWith(`${kind}:`) ? inner : null;
17268
- }
17269
- const found = extractArgDestinations({ to: v });
17270
- return found.length === 1 ? found[0] : null;
17271
- }
17272
- function addTrustedRecipient(raw, addedBy = "user") {
17273
- const value = normalizeRecipient(raw);
17274
- if (!value) return null;
17275
- const entries = readTrustedRecipients();
17276
- if (!entries.some((e) => e.value === value)) {
17277
- entries.push({ value, addedAt: Date.now(), addedBy });
17278
- writeTrustedRecipients(entries);
17279
- }
17280
- return value;
17281
- }
17282
- function removeTrustedRecipient(raw) {
17283
- const value = normalizeRecipient(raw) ?? raw.trim();
17284
- const entries = readTrustedRecipients();
17285
- const next = entries.filter((e) => e.value !== value);
17286
- if (next.length === entries.length) return false;
17287
- writeTrustedRecipients(next);
17288
- return true;
17289
- }
17290
- function mtimeOf() {
17291
- try {
17292
- return import_fs20.default.statSync(getTrustedRecipientsPath()).mtimeMs;
17293
- } catch {
17294
- return 0;
17295
- }
17296
- }
17297
- function isTrustedRecipient(value) {
17298
- const mtime = mtimeOf();
17299
- if (!cache3 || cache3.mtime !== mtime) {
17300
- cache3 = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
17301
- }
17302
- return cache3.values.has(value);
17303
- }
17304
- var import_fs20, import_path21, import_os15, cache3;
17305
- var init_trusted_recipients = __esm({
17306
- "src/auth/trusted-recipients.ts"() {
17307
- "use strict";
17308
- import_fs20 = __toESM(require("fs"));
17309
- import_path21 = __toESM(require("path"));
17310
- import_os15 = __toESM(require("os"));
17311
- init_dist();
17312
- cache3 = null;
17313
- }
17314
- });
17315
-
17316
17566
  // src/utils/read-capped.ts
17317
17567
  function readCapped(file2, maxBytes) {
17318
17568
  if (maxBytes < 0) return null;
@@ -17378,7 +17628,8 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
17378
17628
  const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
17379
17629
  const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
17380
17630
  Object.entries(riskMetadata).filter(
17381
- ([, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v)
17631
+ ([k, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v) || // fw#318 2b: the one array the SaaS schema declares.
17632
+ k === "exposureDestinations" && Array.isArray(v) && v.length > 0
17382
17633
  )
17383
17634
  ) : void 0;
17384
17635
  const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
@@ -17458,11 +17709,15 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
17458
17709
  cwd: process.cwd(),
17459
17710
  platform: import_os16.default.platform()
17460
17711
  },
17461
- // Local-only fields (fw#318 exposureDestinations) stay off the wire:
17462
- // the SaaS validates riskMetadata strictly and would drop it whole.
17712
+ // The SaaS validates riskMetadata strictly and would drop it whole on
17713
+ // an unknown key, so only fields its schema declares go on the wire.
17714
+ // exposureDestinations ships since the dashboard's 2b contract
17715
+ // (fw#318): "Approve and trust" needs them.
17463
17716
  ...riskMetadata && {
17464
17717
  riskMetadata: Object.fromEntries(
17465
- Object.entries(riskMetadata).filter(([, v]) => !Array.isArray(v))
17718
+ Object.entries(riskMetadata).filter(
17719
+ ([k, v]) => !Array.isArray(v) || k === "exposureDestinations"
17720
+ )
17466
17721
  )
17467
17722
  },
17468
17723
  ...ciContext && { ciContext },
@@ -18324,7 +18579,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18324
18579
  let fileAccess;
18325
18580
  let fileFinding;
18326
18581
  try {
18327
- fileAccess = prepareFileAccesses(toolName, args, options?.cwd, config.policy.toolInspection);
18582
+ fileAccess = prepareFileAccesses(toolName, args, options?.cwd, inspectionLayers(config.policy));
18328
18583
  fileFinding = isManual ? null : evaluateProtectedFileAccess(
18329
18584
  fileAccess.accesses,
18330
18585
  fileAccess,
@@ -18343,7 +18598,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18343
18598
  fileAccess = { cwd: options?.cwd, homeDirs: [], accesses: [], handledPaths: /* @__PURE__ */ new Set() };
18344
18599
  fileFinding = isManual ? null : incompleteFileVerdict();
18345
18600
  }
18346
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
18601
+ const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
18347
18602
  const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
18348
18603
  const shellReview = !isManual && shellInput.kind === "uninspectable";
18349
18604
  if (shellReview) {
@@ -18453,6 +18708,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18453
18708
  let exposureReview = null;
18454
18709
  let exposureKeys = [];
18455
18710
  let exposureDestinations = [];
18711
+ let exposureFromEid;
18456
18712
  const ucPolicy = config.policy.untrustedContent;
18457
18713
  if (ucPolicy?.enabled && !taintWarning && meta?.sessionId && !isManual) {
18458
18714
  const role = toolRole(toolName, ucPolicy);
@@ -18460,7 +18716,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18460
18716
  const shellExp = shellInput.kind === "shell" ? classifyShellExposure(shellInput.command) : null;
18461
18717
  const bareName = splitMcpToolName(toolName)?.bare;
18462
18718
  const mcpFileWrite = bareName !== void 0 && fileToolOperation(bareName, args) === "write";
18463
- const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, config.policy.toolInspection).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
18719
+ const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, inspectionLayers(config.policy)).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
18464
18720
  const sendsSomething = acts && !mcpFileWrite || !!shellExp?.sends;
18465
18721
  const mayBeExposed = daemonSaysExposed ?? (maybeSessionExposed(meta.sessionId) || writes.length > 0 && judgeWritesAfterExposure({
18466
18722
  accesses: writes,
@@ -18504,7 +18760,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18504
18760
  const isKnown = (d) => {
18505
18761
  const domain = destinationDomain(d);
18506
18762
  const value = destinationValue(d);
18507
- 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));
18763
+ 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));
18508
18764
  };
18509
18765
  const approvedOnce = (key) => ucPolicy.afterExposure === "ask-once" && approved.has(key);
18510
18766
  const repos = (shellExp?.sends ? shellExp.repos : []).filter(
@@ -18568,6 +18824,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18568
18824
  );
18569
18825
  }
18570
18826
  exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read2}. This call ${parts.join("; and ")}.`;
18827
+ exposureFromEid = last?.eid;
18828
+ if (exposureFromEid) meta = { ...meta, exposureFromEid };
18571
18829
  exposureKeys = [
18572
18830
  ...held.filter((h) => h.dest || h.why === "unresolved" || h.why === "repository").map(
18573
18831
  (h) => h.why === "repository" ? `${toolName}|repo:${h.repo}` : `${toolName}|${h.dest || "unresolved"}`
@@ -18581,6 +18839,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18581
18839
  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("; ") + ".";
18582
18840
  }
18583
18841
  } else if (held.length > 0 || writeHolds.length > 0 || logged.length > 0) {
18842
+ meta = {
18843
+ ...meta,
18844
+ exposure: { kind: "logged", destinations: logged.length },
18845
+ ...last?.eid ? { exposureFromEid: last.eid } : {}
18846
+ };
18584
18847
  appendToLog(HOOK_DEBUG_LOG, {
18585
18848
  ts: (/* @__PURE__ */ new Date()).toISOString(),
18586
18849
  event: "exposure-logged",
@@ -19134,6 +19397,7 @@ ${packageReview}`;
19134
19397
  // deferReview and serverKey never co-occur in production — see above.)
19135
19398
  reason: taintWarning || packageReview || exposureReview || explainableLabel || "Node9 flagged this action for review.",
19136
19399
  ...exposureReview && exposureKeys.length > 0 ? { exposureKeys } : {},
19400
+ ...exposureReview && exposureFromEid ? { exposureFromEid } : {},
19137
19401
  ruleDescription: policyRuleDescription,
19138
19402
  blockedByLabel: explainableLabel
19139
19403
  };
@@ -30909,7 +31173,7 @@ var init_ship = __esm({
30909
31173
  });
30910
31174
 
30911
31175
  // src/policy-snapshot/build.ts
30912
- function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = [], syncHealth, identity) {
31176
+ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, statusEntries = [], syncHealth, identity, trustedRecipients = []) {
30913
31177
  const p = config.policy;
30914
31178
  const statusByKey = /* @__PURE__ */ new Map();
30915
31179
  for (const s of statusEntries) {
@@ -30919,7 +31183,7 @@ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, st
30919
31183
  const connectedRows = Object.entries(mcpTools).slice(0, MAX_MCP_SERVERS).map(([key, cfg]) => ({
30920
31184
  key,
30921
31185
  ...cfg.name && { name: cfg.name },
30922
- tools: cfg.tools.map((t) => t.name).slice(0, MAX_MCP_TOOLS),
31186
+ tools: cfg.tools.slice(0, MAX_MCP_TOOLS).map((t) => ({ name: t.name, ...t.annotations ? { annotations: t.annotations } : {} })),
30923
31187
  toolCount: cfg.tools.length,
30924
31188
  status: cfg.status,
30925
31189
  // In mcp-tools.json ⇒ it launched ⇒ connected, unless the resolver says stale.
@@ -30969,6 +31233,15 @@ function buildPolicySnapshot(config, activeShields, overrides, mcpTools = {}, st
30969
31233
  engineVersion: ENGINE_VERSION,
30970
31234
  // Connected rows first so they win the cap; non-connected SEE rows fill the rest.
30971
31235
  mcpServers: [...connectedRows, ...extraRows].slice(0, MAX_MCP_SERVERS),
31236
+ // fw#318 2b: the developer's personal trusted recipients (A1: read-only
31237
+ // on the dashboard). Omitted when empty, so an old SaaS sees no change.
31238
+ ...trustedRecipients.length > 0 && {
31239
+ trustedRecipients: trustedRecipients.slice(0, 200).map((e) => ({
31240
+ value: e.value,
31241
+ addedBy: e.addedBy,
31242
+ ...e.addedAt ? { addedAt: e.addedAt } : {}
31243
+ }))
31244
+ },
30972
31245
  // fleet-ship Step 2: ship the proxy's own sync health so the dashboard can
30973
31246
  // derive a "sync failing" badge. Only included when the caller passes it.
30974
31247
  ...syncHealth && {
@@ -31486,6 +31759,10 @@ function extractManagedConfig(body) {
31486
31759
  out3.afterExposure = u.afterExposure;
31487
31760
  if (Array.isArray(u.orgDomains))
31488
31761
  out3.orgDomains = u.orgDomains.filter((x) => typeof x === "string");
31762
+ if (Array.isArray(u.trustedRecipients))
31763
+ out3.trustedRecipients = u.trustedRecipients.filter(
31764
+ (x) => typeof x === "string"
31765
+ );
31489
31766
  if (u.roles && typeof u.roles === "object" && !Array.isArray(u.roles)) {
31490
31767
  const roles = {};
31491
31768
  for (const [server, tools] of Object.entries(u.roles)) {
@@ -31662,6 +31939,17 @@ async function pushPostureSnapshot(creds) {
31662
31939
  } catch {
31663
31940
  }
31664
31941
  }
31942
+ function personalTrustedRecipients() {
31943
+ try {
31944
+ return readTrustedRecipients().map((e) => ({
31945
+ value: e.value,
31946
+ addedBy: e.addedBy === "approval" ? "approval" : "user",
31947
+ ...Number.isFinite(e.addedAt) ? { addedAt: new Date(e.addedAt).toISOString() } : {}
31948
+ }));
31949
+ } catch {
31950
+ return [];
31951
+ }
31952
+ }
31665
31953
  async function pushPolicySnapshot(creds) {
31666
31954
  try {
31667
31955
  const cfg = getConfig();
@@ -31684,7 +31972,9 @@ async function pushPolicySnapshot(creds) {
31684
31972
  readSyncHealth(),
31685
31973
  // Machine identity — heals legacy machineId-less keys on the SaaS side
31686
31974
  // (login-v2 duplicate-key fix). getMachineId mints on first need.
31687
- { machineId: getMachineId(), platform: process.platform }
31975
+ { machineId: getMachineId(), platform: process.platform },
31976
+ // fw#318 2b: the developer's personal trusted recipients.
31977
+ personalTrustedRecipients()
31688
31978
  );
31689
31979
  await shipPolicySnapshot(body, creds);
31690
31980
  } catch {
@@ -31715,7 +32005,9 @@ async function runPolicyPush() {
31715
32005
  // (daemon here, user shell in runPolicyPush); see the P2 design's two-env note.
31716
32006
  resolveMcpStatus(),
31717
32007
  readSyncHealth(),
31718
- { machineId: getMachineId(), platform: process.platform }
32008
+ { machineId: getMachineId(), platform: process.platform },
32009
+ // fw#318 2b: the developer's personal trusted recipients.
32010
+ personalTrustedRecipients()
31719
32011
  );
31720
32012
  const sent = await shipPolicySnapshot(body, creds);
31721
32013
  return sent ? { ok: true } : { ok: false, reason: "Push failed (network or server error)" };
@@ -31897,6 +32189,7 @@ var init_sync = __esm({
31897
32189
  init_ship();
31898
32190
  init_build2();
31899
32191
  init_mcp_tools();
32192
+ init_trusted_recipients();
31900
32193
  init_state2();
31901
32194
  init_mcp_status();
31902
32195
  init_ship2();
@@ -31949,6 +32242,13 @@ __export(audit_shipper_exports, {
31949
32242
  startAuditShipper: () => startAuditShipper,
31950
32243
  writeWatermark: () => writeWatermark
31951
32244
  });
32245
+ function exposureOf(v) {
32246
+ if (!v || typeof v !== "object") return null;
32247
+ const o = v;
32248
+ if (o.kind !== "source" && o.kind !== "logged") return null;
32249
+ const n = typeof o.destinations === "number" && Number.isFinite(o.destinations) ? o.destinations : 0;
32250
+ return { kind: o.kind, destinations: Math.max(0, Math.min(1e3, Math.floor(n))) };
32251
+ }
31952
32252
  function fileSignature(filePath) {
31953
32253
  const fd = import_fs53.default.openSync(filePath, "r");
31954
32254
  try {
@@ -32024,7 +32324,9 @@ function buildWireRows(chunk2) {
32024
32324
  ...typeof parsed.loopCount === "number" ? { loopCount: parsed.loopCount } : {},
32025
32325
  ...typeof parsed.transcriptPath === "string" ? { transcriptPath: parsed.transcriptPath } : {},
32026
32326
  ...typeof parsed.taintFromEid === "string" ? { taintFromEid: parsed.taintFromEid } : {},
32027
- ...typeof parsed.taintSource === "string" ? { taintSource: parsed.taintSource } : {}
32327
+ ...typeof parsed.taintSource === "string" ? { taintSource: parsed.taintSource } : {},
32328
+ ...exposureOf(parsed.exposure) ? { exposure: exposureOf(parsed.exposure) } : {},
32329
+ ...typeof parsed.exposureFromEid === "string" ? { exposureFromEid: parsed.exposureFromEid } : {}
32028
32330
  });
32029
32331
  }
32030
32332
  return { rows, consumed: lastNl + 1 };
@@ -34481,6 +34783,7 @@ data: ${JSON.stringify(item.data)}
34481
34783
  tool: body.source.tool.slice(0, 200),
34482
34784
  server: typeof body.source.server === "string" ? body.source.server.slice(0, 80) : void 0,
34483
34785
  word: typeof body.source.word === "string" ? body.source.word.slice(0, 40) : void 0,
34786
+ eid: typeof body.source.eid === "string" ? body.source.eid.slice(0, 64) : void 0,
34484
34787
  at: Date.now()
34485
34788
  },
34486
34789
  strings(body.destinations)
@@ -64321,7 +64624,8 @@ RAW: ${raw}
64321
64624
  sessionId: sid,
64322
64625
  ts: Date.now(),
64323
64626
  label: result2.blockedByLabel,
64324
- ...result2.exposureKeys?.length ? { exposureKeys: result2.exposureKeys } : {}
64627
+ ...result2.exposureKeys?.length ? { exposureKeys: result2.exposureKeys } : {},
64628
+ ...result2.exposureFromEid ? { exposureFromEid: result2.exposureFromEid } : {}
64325
64629
  });
64326
64630
  }
64327
64631
  } catch {
@@ -64784,9 +65088,14 @@ function buildArgsField(rawInput) {
64784
65088
  const hit = scanArgs(rawInput);
64785
65089
  if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
64786
65090
  if (!scanCoveredEverything(rawInput)) return { ...hashed, argsUnscanned: true };
64787
- const gateHit = inspectArgumentSecrets(rawInput).match;
64788
- if (gateHit)
64789
- return { ...hashed, dlpPattern: gateHit.patternName, dlpSample: gateHit.redactedSample };
65091
+ const inspected = inspectArgumentSecrets(rawInput);
65092
+ if (inspected.match)
65093
+ return {
65094
+ ...hashed,
65095
+ dlpPattern: inspected.match.patternName,
65096
+ dlpSample: inspected.match.redactedSample
65097
+ };
65098
+ if (inspected.coverage === "incomplete") return { ...hashed, argsUnscanned: true };
64790
65099
  return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
64791
65100
  } catch {
64792
65101
  return hashed;
@@ -64897,6 +65206,7 @@ function registerLogCommand(program2) {
64897
65206
  {
64898
65207
  agent,
64899
65208
  ruleName: reviewLabel,
65209
+ ...resolvedReview.exposureFromEid ? { exposureFromEid: resolvedReview.exposureFromEid } : {},
64900
65210
  ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
64901
65211
  ...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
64902
65212
  ...safeCwd ? { workingDir: safeCwd } : {}
@@ -64934,17 +65244,21 @@ function registerLogCommand(program2) {
64934
65244
  if (redactOutputMode) {
64935
65245
  const { result, found } = redactText(toolOutput);
64936
65246
  let out = result;
65247
+ const ucA = config.policy.untrustedContent;
65248
+ const shellA = ucA?.enabled ? shellFetch(tool, rawInput) : null;
65249
+ const sourceA = !!ucA?.enabled && (toolRole(tool, ucA).source || !!shellA?.fetches);
64937
65250
  let injection = null;
64938
65251
  if (injectionOn) {
64939
- const m = scanInjection(result, { tool: rawToolName });
65252
+ const m = scanInjection(result, {
65253
+ tool: rawToolName,
65254
+ ...sourceA ? { untrusted: true } : {}
65255
+ });
64940
65256
  if (m && atLeastConfidence(m.confidence, inj.minConfidence)) {
64941
65257
  injection = m;
64942
65258
  out = frameUntrusted(result);
64943
65259
  }
64944
65260
  }
64945
- const ucA = config.policy.untrustedContent;
64946
- const shellA = ucA?.enabled ? shellFetch(tool, rawInput) : null;
64947
- if (ucA?.enabled && (toolRole(tool, ucA).source || shellA?.fetches)) {
65261
+ if (sourceA) {
64948
65262
  const wordA = shellA?.fetches ? shellSourceWord(shellA.fetchKinds) : sourceWord(tool);
64949
65263
  markSessionExposed(payloadSessionId ?? "");
64950
65264
  await notifySessionExposure(
@@ -64986,15 +65300,16 @@ function registerLogCommand(program2) {
64986
65300
  );
64987
65301
  }
64988
65302
  if (injectionOn) {
64989
- const m = scanInjection(toolOutput, { tool: rawToolName });
65303
+ const injCtx = { tool: rawToolName, ...sourceRead ? { untrusted: true } : {} };
65304
+ const m = scanInjection(toolOutput, injCtx);
64990
65305
  if (m && atLeastConfidence(m.confidence, inj.minConfidence)) {
64991
65306
  if (spotlightApplies) {
64992
- injectedLeaf = (leaf) => scanInjection(leaf, { tool: rawToolName }) !== null;
65307
+ injectedLeaf = (leaf) => scanInjection(leaf, injCtx) !== null;
64993
65308
  }
64994
65309
  if (agent === "Claude Code" && !spotlightApplies) {
64995
65310
  updatedToolOutput = frameToolOutputLeaves(
64996
65311
  updatedToolOutput ?? payload.tool_response,
64997
- (leaf) => scanInjection(leaf, { tool: rawToolName }) !== null,
65312
+ (leaf) => scanInjection(leaf, injCtx) !== null,
64998
65313
  newUntrustedFrame,
64999
65314
  neutralizeMarkers
65000
65315
  );
@@ -65011,22 +65326,29 @@ function registerLogCommand(program2) {
65011
65326
  if (sourceRead) {
65012
65327
  const destinations = extractContentDestinations(toolOutput);
65013
65328
  markSessionExposed(payloadSessionId ?? "");
65329
+ const sourceEid = appendLocalAudit(
65330
+ tool,
65331
+ rawInput,
65332
+ "allow",
65333
+ "exposure-source",
65334
+ {
65335
+ agent,
65336
+ ...payloadSessionId ? { sessionId: payloadSessionId } : {},
65337
+ ...rawToolName !== tool ? { agentToolName: rawToolName } : {},
65338
+ exposure: { kind: "source", destinations: destinations.length }
65339
+ },
65340
+ config.settings.auditHashArgs === true
65341
+ );
65014
65342
  await notifySessionExposure(
65015
65343
  payloadSessionId ?? "",
65016
65344
  {
65017
65345
  tool,
65018
65346
  server: splitMcpToolName(tool)?.server,
65019
- ...shellExposure?.fetches ? { word: shellSourceWord(shellExposure.fetchKinds) } : {}
65347
+ ...shellExposure?.fetches ? { word: shellSourceWord(shellExposure.fetchKinds) } : {},
65348
+ eid: sourceEid
65020
65349
  },
65021
65350
  destinations
65022
65351
  );
65023
- appendToLog(LOCAL_AUDIT_LOG, {
65024
- ts: (/* @__PURE__ */ new Date()).toISOString(),
65025
- tool,
65026
- source: "exposure-source",
65027
- destinations: destinations.length,
65028
- ...payloadSessionId ? { sessionId: payloadSessionId } : {}
65029
- });
65030
65352
  if (spotlightApplies) {
65031
65353
  const word = shellExposure?.fetches ? shellSourceWord(shellExposure.fetchKinds) : sourceWord(tool);
65032
65354
  if (agent === "Claude Code") {
@@ -66556,7 +66878,9 @@ function dimensionOfBlock(checkedBy, ruleName) {
66556
66878
  if (checkedBy.includes("egress")) return "network";
66557
66879
  if (ruleName.startsWith("ssrf:")) return "network";
66558
66880
  if (checkedBy.includes("pii") || checkedBy.includes("dlp")) return "data";
66559
- if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection"))
66881
+ if (checkedBy === "loop-detected" || checkedBy === "mcp-pin-mismatch" || checkedBy.startsWith("injection") || // fw#318: a send held after the session read outside content, and the
66882
+ // read rows themselves. Mirrors node9Firewall canon-taxonomy.ts.
66883
+ ruleName.startsWith("exposure") || checkedBy.startsWith("exposure"))
66560
66884
  return "detection";
66561
66885
  if (checkedBy === "app-permission-block" || ruleName.startsWith("app-permission:")) return "apps";
66562
66886
  if (ruleName.startsWith("shield:project-jail")) return "files";
@@ -67880,7 +68204,10 @@ function scanToolResult(line, parsed, tool, cfg) {
67880
68204
  let injection = null;
67881
68205
  const inj = cfg.injection;
67882
68206
  if (inj.enabled && !inj.allow.includes(tool) && texts.length > 0) {
67883
- const m = scanInjection(texts.join("\n"), { tool });
68207
+ const m = scanInjection(texts.join("\n"), {
68208
+ tool,
68209
+ ...cfg.untrusted ? { untrusted: true } : {}
68210
+ });
67884
68211
  if (m && CONFIDENCE_RANK2[m.confidence] >= CONFIDENCE_RANK2[inj.minConfidence]) {
67885
68212
  injection = m;
67886
68213
  mutated = true;
@@ -68396,7 +68723,8 @@ async function runMcpGateway(upstreamCommand, configName) {
68396
68723
  const scan = scanToolResult(line, parsed, exec.toolName, {
68397
68724
  dlpEnabled: cfg.policy.dlp.enabled,
68398
68725
  injection: cfg.policy.injectionScan,
68399
- spotlight: isSource && uc.spotlight ? sourceWord(nsName) : void 0
68726
+ spotlight: isSource && uc.spotlight ? sourceWord(nsName) : void 0,
68727
+ untrusted: isSource
68400
68728
  });
68401
68729
  if (scan.changed) outLine = scan.line;
68402
68730
  if (isSource) {