@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/index.js CHANGED
@@ -2158,32 +2158,109 @@ function evaluateSmartConditions(args, rule) {
2158
2158
  function isBashTool(tool) {
2159
2159
  return BASH_TOOL_NAMES.has(tool.toLowerCase());
2160
2160
  }
2161
- function inspectionEntry(tool, inspection = {}) {
2161
+ function inspectionLayers(policy) {
2162
+ return { trusted: policy.toolInspection, project: policy.projectToolInspection };
2163
+ }
2164
+ function isShellEntry(value) {
2165
+ return typeof value === "object" && value !== null && value.kind === "shell" && typeof value.field === "string" && Object.keys(value).length === 2;
2166
+ }
2167
+ function isInspectionLayers(input) {
2168
+ const trusted = input.trusted;
2169
+ return typeof trusted === "object" && trusted !== null && !isShellEntry(trusted);
2170
+ }
2171
+ function layersOf(input) {
2172
+ return isInspectionLayers(input) ? { trusted: input.trusted, project: input.project ?? {} } : { trusted: input, project: {} };
2173
+ }
2174
+ function entryOf(tool, inspection) {
2162
2175
  const pattern = Object.keys(inspection).find((p) => matchesPattern(tool, p));
2163
2176
  return pattern === void 0 ? void 0 : inspection[pattern];
2164
2177
  }
2178
+ function inspectionEntry(tool, inspection = {}) {
2179
+ const { trusted, project } = layersOf(inspection);
2180
+ return entryOf(tool, trusted) ?? entryOf(tool, project);
2181
+ }
2182
+ function executorName(tool) {
2183
+ const name = tool.toLowerCase();
2184
+ if (!name.startsWith("mcp__")) return name;
2185
+ const at = name.lastIndexOf("__");
2186
+ return at >= "mcp__".length ? name.slice(at + 2) : name;
2187
+ }
2188
+ function entryFields(entry) {
2189
+ if (typeof entry === "object") return [entry.field];
2190
+ return entry === "command" ? ["command"] : null;
2191
+ }
2165
2192
  function shellFields(tool, inspection) {
2166
2193
  if (isBashTool(tool)) return ["command"];
2167
- const entry = inspectionEntry(tool, inspection);
2168
- if (typeof entry === "object") return [entry.field];
2169
- if (entry !== void 0) return entry === "command" ? [entry] : null;
2170
- const name = tool.toLowerCase().replace(/^mcp__[a-z0-9_-]+?__/, "");
2171
- return MCP_EXECUTORS.has(name) ? ["command", "cmd", "script"] : null;
2194
+ const { trusted, project } = layersOf(inspection);
2195
+ const trustedEntry = entryOf(tool, trusted);
2196
+ if (trustedEntry !== void 0) return entryFields(trustedEntry);
2197
+ const builtin = MCP_EXECUTORS.has(executorName(tool)) ? BUILTIN_FIELDS : null;
2198
+ const projectEntry = entryOf(tool, project);
2199
+ if (projectEntry === void 0) return builtin;
2200
+ const added = entryFields(projectEntry);
2201
+ if (!builtin) return added;
2202
+ if (!added) return builtin;
2203
+ return [.../* @__PURE__ */ new Set([...builtin, ...added])];
2172
2204
  }
2173
2205
  function isShellShapedTool(tool, inspection = {}) {
2174
2206
  return shellFields(tool, inspection) !== null;
2175
2207
  }
2208
+ function shellWord(word) {
2209
+ if (BARE_WORD.test(word)) return word;
2210
+ return word === "" ? "''" : `'${word.replace(/'/g, `'\\''`)}'`;
2211
+ }
2212
+ function isStringArray(value) {
2213
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
2214
+ }
2215
+ function commandLine(args, fieldPath, value, native) {
2216
+ const missing = { reason: "Shell command field is missing or is not a string" };
2217
+ const unreadable = { reason: "Shell command arguments could not be read" };
2218
+ if (typeof value !== "string" && (native || !Array.isArray(value))) return missing;
2219
+ const dot = fieldPath.lastIndexOf(".");
2220
+ const parent = dot === -1 ? args : getNestedValue(args, fieldPath.slice(0, dot));
2221
+ const siblings = ARGV_FIELDS.map((key) => ({
2222
+ key,
2223
+ value: parent && typeof parent === "object" ? parent[key] : void 0
2224
+ })).filter(({ value: value2 }) => value2 !== void 0 && value2 !== null);
2225
+ let argv = siblings[0];
2226
+ if (native) argv = siblings.find(({ value: v }) => isStringArray(v));
2227
+ else if (siblings.length === 2 && JSON.stringify(siblings[0].value) !== JSON.stringify(siblings[1].value))
2228
+ return { reason: "Shell command arguments disagree" };
2229
+ let command;
2230
+ let source = fieldPath;
2231
+ if (Array.isArray(value)) {
2232
+ if (argv || !value.length || !isStringArray(value)) return unreadable;
2233
+ command = value.map(shellWord).join(" ");
2234
+ } else if (!argv) {
2235
+ return { command: value, fieldPath };
2236
+ } else {
2237
+ if (!isStringArray(argv.value)) return unreadable;
2238
+ if (!argv.value.length) return { command: value, fieldPath };
2239
+ command = `${value} ${argv.value.map(shellWord).join(" ")}`;
2240
+ source = `${fieldPath}+${argv.key}`;
2241
+ }
2242
+ if (command.length > MAX_ARGV_CHARACTERS)
2243
+ return { reason: "Shell command arguments exceed the inspectable size" };
2244
+ return { command, fieldPath: source };
2245
+ }
2176
2246
  function resolveShellInput(tool, args, inspection = {}) {
2177
2247
  const fields = shellFields(tool, inspection);
2178
2248
  if (!fields) return { kind: "not-shell" };
2179
2249
  const values = fields.map((fieldPath) => ({ fieldPath, value: getNestedValue(args, fieldPath) })).filter(({ value }) => value !== void 0 && value !== null && value !== "");
2180
- if (!values.length || values.some(({ value }) => typeof value !== "string")) {
2250
+ if (!values.length) {
2181
2251
  return { kind: "uninspectable", reason: "Shell command field is missing or is not a string" };
2182
2252
  }
2183
- if (new Set(values.map(({ value }) => value)).size !== 1) {
2253
+ const native = isBashTool(tool);
2254
+ const lines = values.map(({ fieldPath, value }) => commandLine(args, fieldPath, value, native));
2255
+ const failed = lines.find((line) => "reason" in line);
2256
+ if (failed) return { kind: "uninspectable", reason: failed.reason };
2257
+ const commands = lines.filter(
2258
+ (line) => "command" in line
2259
+ );
2260
+ if (new Set(commands.map(({ command }) => command)).size !== 1) {
2184
2261
  return { kind: "uninspectable", reason: "Multiple shell command fields disagree" };
2185
2262
  }
2186
- return { kind: "shell", command: values[0].value, fieldPath: values[0].fieldPath };
2263
+ return { kind: "shell", ...commands[0] };
2187
2264
  }
2188
2265
  function shellRuleArgs(tool, args, ruleTool, inspection = {}) {
2189
2266
  if (!ruleTool || !matchesPattern("bash", ruleTool) || matchesPattern(tool, ruleTool)) return args;
@@ -2924,20 +3001,25 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2924
3001
  }
2925
3002
  return best;
2926
3003
  }
2927
- function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ new Set() }) {
2928
- const words = [...text.matchAll(/[a-zA-Z]+/g)];
3004
+ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new Set() }) {
2929
3005
  let run = [];
3006
+ let runIndex = [];
2930
3007
  let previousEnd = 0;
2931
3008
  const rejected = /* @__PURE__ */ new Set();
2932
3009
  const inspect = () => {
3010
+ if (run.length < 12) return false;
3011
+ const ascending = new Array(run.length).fill(0);
3012
+ for (let i = 1; i < run.length; i++)
3013
+ ascending[i] = ascending[i - 1] + (runIndex[i] > runIndex[i - 1] ? 1 : 0);
2933
3014
  for (let start = 0; start + 12 <= run.length; start++) {
2934
3015
  for (const count of lengths) {
2935
3016
  if (start + count > run.length) continue;
3017
+ if (ascending[start + count - 1] - ascending[start] === count - 1) continue;
2936
3018
  const candidate = run.slice(start, start + count);
2937
3019
  const sentence = candidate.join(" ");
2938
3020
  if (rejected.has(sentence)) continue;
2939
- if (++budget.candidates > 1024) {
2940
- budget.reasons.add("mnemonic-candidates");
3021
+ if (++budget.hashes > MAX_MNEMONIC_HASHES) {
3022
+ budget.reasons.add("mnemonic-budget");
2941
3023
  return false;
2942
3024
  }
2943
3025
  if (BIP39_PUBLIC_VECTORS.has(hash(sentence).toString("hex"))) {
@@ -2955,13 +3037,18 @@ function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ n
2955
3037
  }
2956
3038
  return false;
2957
3039
  };
2958
- for (const word of words) {
2959
- if (!indices.has(word[0]) || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
3040
+ for (const word of text.matchAll(/[a-zA-Z]+/g)) {
3041
+ const index = indices.get(word[0]);
3042
+ if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
2960
3043
  if (inspect()) return true;
2961
- if (budget.reasons.has("mnemonic-candidates")) return false;
3044
+ if (budget.reasons.has("mnemonic-budget")) return false;
2962
3045
  run = [];
3046
+ runIndex = [];
3047
+ }
3048
+ if (index !== void 0) {
3049
+ run.push(word[0]);
3050
+ runIndex.push(index);
2963
3051
  }
2964
- if (indices.has(word[0])) run.push(word[0]);
2965
3052
  previousEnd = word.index + word[0].length;
2966
3053
  }
2967
3054
  return inspect();
@@ -3092,6 +3179,9 @@ function validateCask(raw) {
3092
3179
  const second = b64urlIndex(s[tsAt + 5]);
3093
3180
  return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
3094
3181
  }
3182
+ function isInvisibleChar(char) {
3183
+ return INVISIBLE_ONE.test(char);
3184
+ }
3095
3185
  function stripInvisible(text) {
3096
3186
  return text.replace(INVISIBLE_RE, "");
3097
3187
  }
@@ -3278,6 +3368,45 @@ function matchArgumentText(text, fieldPath, budget) {
3278
3368
  }
3279
3369
  return best;
3280
3370
  }
3371
+ function matchArgumentBatch(fragments, fieldPath, budget) {
3372
+ if (!fragments.length) return null;
3373
+ const text = fragments.join(BATCH_SEPARATOR);
3374
+ const lower2 = text.toLowerCase();
3375
+ let best = null;
3376
+ for (const pattern of DLP_PATTERNS) {
3377
+ if (pattern.keywords && !pattern.keywords.some((kw) => lower2.includes(kw.toLowerCase())))
3378
+ continue;
3379
+ const flags = pattern.regex.flags.replace(/[gmy]/g, "");
3380
+ if (!new RegExp(pattern.regex.source, flags + "m").test(text)) continue;
3381
+ for (const fragment of fragments) {
3382
+ if (firstAcceptedMatch(pattern, fragment) === null) continue;
3383
+ const severity = pattern.contextBoost && isAssignmentContext(fragment) ? "block" : pattern.severity;
3384
+ if (severity === "block")
3385
+ return {
3386
+ patternName: pattern.name,
3387
+ fieldPath,
3388
+ redactedSample: "[encoded secret]",
3389
+ severity
3390
+ };
3391
+ best ??= {
3392
+ patternName: pattern.name,
3393
+ fieldPath,
3394
+ redactedSample: "[encoded secret]",
3395
+ severity
3396
+ };
3397
+ if (!pattern.contextBoost) break;
3398
+ }
3399
+ }
3400
+ if (findMnemonic(text, budget)) {
3401
+ return {
3402
+ patternName: "BIP-39 Recovery Phrase",
3403
+ fieldPath,
3404
+ redactedSample: "[recovery phrase]",
3405
+ severity: "block"
3406
+ };
3407
+ }
3408
+ return best;
3409
+ }
3281
3410
  function scanArgs(args, depth = 0, fieldPath = "args") {
3282
3411
  if (depth > MAX_DEPTH2 || args === null || args === void 0) return null;
3283
3412
  if (Array.isArray(args)) {
@@ -4149,6 +4278,9 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
4149
4278
  return null;
4150
4279
  }
4151
4280
  }
4281
+ function newSecretViewBudget() {
4282
+ return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
4283
+ }
4152
4284
  function textBytes(buf) {
4153
4285
  if (buf.length < 16) return null;
4154
4286
  const text = buf.toString("utf8");
@@ -4167,13 +4299,41 @@ function percentOnce(text) {
4167
4299
  }
4168
4300
  });
4169
4301
  }
4302
+ function* wrappedBase64(text) {
4303
+ if (!text.includes("\n")) return;
4304
+ const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
4305
+ let run = [];
4306
+ let width = 0;
4307
+ const flush = function* () {
4308
+ if (run.length >= 2) yield run.join("");
4309
+ run = [];
4310
+ width = 0;
4311
+ };
4312
+ for (const raw of text.split("\n")) {
4313
+ const line = raw.trim();
4314
+ const full = line.length >= 16 && word.test(line) && !line.includes("=");
4315
+ if (full && (!run.length || line.length === width)) {
4316
+ run.push(line);
4317
+ width = line.length;
4318
+ continue;
4319
+ }
4320
+ if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
4321
+ run.push(line);
4322
+ yield* flush();
4323
+ continue;
4324
+ }
4325
+ yield* flush();
4326
+ if (full) {
4327
+ run.push(line);
4328
+ width = line.length;
4329
+ }
4330
+ }
4331
+ yield* flush();
4332
+ }
4170
4333
  function* encodedRegions(text, budget) {
4171
4334
  yield [text, false];
4335
+ for (const block of wrappedBase64(text)) yield [block, false];
4172
4336
  for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
4173
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4174
- budget.reasons.add("decode-candidates");
4175
- return;
4176
- }
4177
4337
  try {
4178
4338
  new URL(match[0]);
4179
4339
  } catch {
@@ -4193,6 +4353,8 @@ function* encodedRegions(text, budget) {
4193
4353
  ];
4194
4354
  for (const region of regions) if (region) yield [region, false];
4195
4355
  const labels = host2.split(".");
4356
+ 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))
4357
+ continue;
4196
4358
  for (let start = 0; start < labels.length; start++) {
4197
4359
  let joined = labels[start];
4198
4360
  for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
@@ -4200,11 +4362,13 @@ function* encodedRegions(text, budget) {
4200
4362
  if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
4201
4363
  break;
4202
4364
  joined += labels[end];
4203
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4204
- budget.reasons.add("decode-candidates");
4365
+ if (joined.length < 16) continue;
4366
+ budget.joinedLabelBytes += joined.length;
4367
+ if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
4368
+ budget.reasons.add("decode-labels");
4205
4369
  return;
4206
4370
  }
4207
- if (joined.length >= 16 && joined.length <= 253) yield [joined, true];
4371
+ yield [joined, true];
4208
4372
  }
4209
4373
  }
4210
4374
  }
@@ -4241,10 +4405,6 @@ function* decodedCandidates(text, budget) {
4241
4405
  if (joined) yield ["dns-labels", region];
4242
4406
  for (const [kind, re] of families) {
4243
4407
  for (const match of region.matchAll(re)) {
4244
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4245
- budget.reasons.add("decode-candidates");
4246
- return;
4247
- }
4248
4408
  const raw = match[0];
4249
4409
  let buf;
4250
4410
  if (kind === "base64") {
@@ -4267,42 +4427,76 @@ function* decodedCandidates(text, budget) {
4267
4427
  }
4268
4428
  }
4269
4429
  }
4270
- function* secretViews(text, budget) {
4430
+ function secretViews(text, budget) {
4271
4431
  const seen = /* @__PURE__ */ new Set([text]);
4272
- const queue = [{ text, transforms: [] }];
4273
- for (let i = 0; i < queue.length; i++) {
4274
- const view = queue[i];
4275
- yield view;
4276
- for (const [kind, decoded] of decodedCandidates(view.text, budget)) {
4277
- if (seen.has(decoded)) continue;
4278
- if (view.transforms.length >= SECRET_VIEW_LIMITS.maxDepth) {
4279
- budget.reasons.add("decode-depth");
4280
- continue;
4281
- }
4282
- const bytes = Buffer.byteLength(decoded);
4283
- if (queue.length >= SECRET_VIEW_LIMITS.maxViewsPerLeaf || budget.views >= SECRET_VIEW_LIMITS.maxViews || budget.bytes + bytes > SECRET_VIEW_LIMITS.maxDerivedBytes) {
4284
- budget.reasons.add("decode-budget");
4285
- return;
4432
+ const out = [];
4433
+ let frontier = [{ text, transforms: [] }];
4434
+ for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
4435
+ const next = [];
4436
+ for (const parent of frontier) {
4437
+ let perLeaf = 0;
4438
+ for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
4439
+ if (seen.has(decoded)) continue;
4440
+ seen.add(decoded);
4441
+ const normalizing = kind === "invisibles" || kind === "percent";
4442
+ if (depth >= 2 && !normalizing) {
4443
+ budget.nestedBytes += Buffer.byteLength(decoded);
4444
+ if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
4445
+ budget.reasons.add("decode-nested");
4446
+ return out;
4447
+ }
4448
+ }
4449
+ const view = { text: decoded, transforms: [...parent.transforms, kind] };
4450
+ out.push(view);
4451
+ next.push(view);
4286
4452
  }
4287
- seen.add(decoded);
4288
- budget.views++;
4289
- budget.bytes += bytes;
4290
- queue.push({ text: decoded, transforms: [...view.transforms, kind] });
4291
4453
  }
4292
- if (budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) return;
4454
+ frontier = next;
4455
+ if (!frontier.length) return out;
4456
+ }
4457
+ for (const view of frontier) {
4458
+ if (!decodedCandidates(view.text, budget).next().done) {
4459
+ budget.reasons.add("decode-nested");
4460
+ break;
4461
+ }
4293
4462
  }
4463
+ return out;
4464
+ }
4465
+ function batches(views) {
4466
+ const out = [];
4467
+ let current = [];
4468
+ let size = 0;
4469
+ for (const { text } of views) {
4470
+ if (current.length && size + text.length > SECRET_VIEW_LIMITS.batchCharacters) {
4471
+ out.push(current);
4472
+ current = [];
4473
+ size = 0;
4474
+ }
4475
+ current.push(text);
4476
+ size += text.length;
4477
+ }
4478
+ if (current.length) out.push(current);
4479
+ return out;
4294
4480
  }
4295
4481
  function inspectArgumentSecrets(args) {
4296
- const budget = { views: 0, bytes: 0, candidates: 0, reasons: /* @__PURE__ */ new Set() };
4482
+ const budget = newSecretViewBudget();
4297
4483
  const ancestors = /* @__PURE__ */ new Set();
4298
4484
  let nodes = 0;
4299
4485
  let sourceBytes = 0;
4300
4486
  let tailBytes = 0;
4301
4487
  let best = null;
4488
+ const blocked = () => best?.severity === "block";
4302
4489
  const accept = (match) => {
4303
4490
  if (match && (!best || best.severity !== "block" && match.severity === "block")) best = match;
4304
4491
  };
4492
+ const decodeSegment = (segment, fieldPath) => {
4493
+ for (const batch of batches(secretViews(segment, budget))) {
4494
+ accept(matchArgumentBatch(batch, fieldPath, budget));
4495
+ if (blocked()) return;
4496
+ }
4497
+ };
4305
4498
  const visit = (value, depth, fieldPath) => {
4499
+ if (blocked()) return;
4306
4500
  if (++nodes > ARGUMENT_SCAN_LIMITS.maxNodes) {
4307
4501
  budget.reasons.add("argument-nodes");
4308
4502
  return;
@@ -4344,14 +4538,11 @@ function inspectArgumentSecrets(args) {
4344
4538
  }
4345
4539
  sourceBytes += Math.min(bytes, Math.max(0, remaining));
4346
4540
  if (!text) return;
4347
- for (const view of secretViews(text, budget)) {
4348
- const hit = matchArgumentText(view.text, fieldPath, budget);
4349
- if (hit)
4350
- accept(view.transforms.length ? { ...hit, redactedSample: "[encoded secret]" } : hit);
4351
- }
4541
+ accept(matchArgumentText(text, fieldPath, budget));
4542
+ let scannedEnd = text.length;
4352
4543
  if (!truncated && value.length > window) {
4353
- budget.reasons.add("decode-window");
4354
4544
  for (let start = window - overlap; start < value.length; start += window - overlap) {
4545
+ if (blocked()) return;
4355
4546
  const chunk = value.slice(start, start + window);
4356
4547
  const chunkBytes = Buffer.byteLength(chunk);
4357
4548
  if (tailBytes + chunkBytes > ARGUMENT_SCAN_LIMITS.maxTailBytes) {
@@ -4360,9 +4551,16 @@ function inspectArgumentSecrets(args) {
4360
4551
  }
4361
4552
  tailBytes += chunkBytes;
4362
4553
  accept(matchArgumentText(chunk, fieldPath, budget));
4554
+ scannedEnd = start + chunk.length;
4363
4555
  if (start + window >= value.length) break;
4364
4556
  }
4365
4557
  }
4558
+ for (let start = 0; start < scannedEnd && !blocked(); ) {
4559
+ let end = Math.min(start + window, scannedEnd);
4560
+ while (end < scannedEnd && runChar(value, end - 1) && runChar(value, end)) end++;
4561
+ decodeSegment(value.slice(start, end), fieldPath);
4562
+ start = end;
4563
+ }
4366
4564
  const trimmed = text.trim();
4367
4565
  if (value.length < ARGUMENT_SCAN_LIMITS.maxJsonCharacters && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
4368
4566
  try {
@@ -4381,7 +4579,7 @@ function inspectArgumentSecrets(args) {
4381
4579
  function scanArgumentSecrets(args) {
4382
4580
  const result = inspectArgumentSecrets(args);
4383
4581
  if (result.match) return result.match;
4384
- return result.reasons.some((reason) => reason.startsWith("argument-")) ? {
4582
+ return result.coverage === "incomplete" ? {
4385
4583
  patternName: "Argument inspection incomplete",
4386
4584
  fieldPath: "args",
4387
4585
  redactedSample: "[uninspected content]",
@@ -4409,18 +4607,13 @@ function tokenize2(toolName) {
4409
4607
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
4410
4608
  }
4411
4609
  function extractShellCommand(toolName, args, toolInspection) {
4412
- const patterns = Object.keys(toolInspection);
4413
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4414
- if (!matchingPattern) return null;
4415
- const fieldPath = toolInspection[matchingPattern];
4610
+ const fieldPath = inspectionEntry(toolName, toolInspection);
4611
+ if (fieldPath === void 0) return null;
4416
4612
  const value = getNestedValue(args, typeof fieldPath === "string" ? fieldPath : fieldPath.field);
4417
4613
  return typeof value === "string" ? value : null;
4418
4614
  }
4419
4615
  function isSqlTool(toolName, toolInspection) {
4420
- const patterns = Object.keys(toolInspection);
4421
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4422
- if (!matchingPattern) return false;
4423
- const fieldName = toolInspection[matchingPattern];
4616
+ const fieldName = inspectionEntry(toolName, toolInspection);
4424
4617
  return fieldName === "sql" || fieldName === "query";
4425
4618
  }
4426
4619
  function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
@@ -4487,9 +4680,10 @@ function egressPolicyVerdict(eg) {
4487
4680
  }
4488
4681
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
4489
4682
  let protectedVerdict = null;
4683
+ const layers = inspectionLayers(config.policy);
4490
4684
  if (context.agent !== "Terminal") {
4491
4685
  try {
4492
- const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, config.policy.toolInspection);
4686
+ const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, layers);
4493
4687
  protectedVerdict = evaluateProtectedFileAccess(accesses, context, catalogSettings(config));
4494
4688
  } catch {
4495
4689
  protectedVerdict = incompleteFileVerdict();
@@ -4524,7 +4718,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4524
4718
  hooks
4525
4719
  );
4526
4720
  if (result.decision === "block") return result;
4527
- if (context.agent !== "Terminal" && resolveShellInput(toolName, args, config.policy.toolInspection).kind === "uninspectable") {
4721
+ if (context.agent !== "Terminal" && resolveShellInput(toolName, args, layers).kind === "uninspectable") {
4528
4722
  return {
4529
4723
  decision: "review",
4530
4724
  ruleName: "shell-inspection-incomplete",
@@ -4540,13 +4734,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4540
4734
  const { agent, cwd, activeEnvironment } = context;
4541
4735
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
4542
4736
  const settingsView = catalogSettings(config);
4737
+ const layers = inspectionLayers(config.policy);
4543
4738
  const cv = (id) => checkValue(settingsView, id);
4544
4739
  let loggedFinding;
4545
4740
  const noteLogged = (v) => {
4546
4741
  if (v?.logged && !loggedFinding) loggedFinding = v;
4547
4742
  };
4548
4743
  const allowed = () => loggedFinding ?? { decision: "allow" };
4549
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
4744
+ const shellInput = resolveShellInput(toolName, args, layers);
4550
4745
  const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
4551
4746
  {
4552
4747
  const dest = ssrfDestinationFloor(toolName, args, {
@@ -4651,13 +4846,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4651
4846
  return { id, value: cv(id) };
4652
4847
  };
4653
4848
  const matches = config.policy.smartRules.filter((rule) => {
4654
- if (!toolMatchesRule(toolName, rule.tool, config.policy.toolInspection)) return false;
4849
+ if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
4655
4850
  if (astSuppressed(rule)) return false;
4656
4851
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
4657
- if (!evaluateSmartConditions(
4658
- shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
4659
- rule
4660
- ))
4852
+ if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
4661
4853
  return false;
4662
4854
  const check = ruleCheck(rule);
4663
4855
  if (check?.value === "off") return false;
@@ -4704,7 +4896,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4704
4896
  let allTokens = [];
4705
4897
  let pathTokens = [];
4706
4898
  if (pendingToolEgress) return pendingToolEgress;
4707
- const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, config.policy.toolInspection) : null;
4899
+ const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, layers) : null;
4708
4900
  if (shellCommand) {
4709
4901
  const analyzed = analyzeShellCommand(shellCommand);
4710
4902
  allTokens = analyzed.allTokens;
@@ -4781,7 +4973,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4781
4973
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
4782
4974
  const rawTokens = shellCommand.trim().split(/\s+/);
4783
4975
  const sshHosts = extractAllSshHosts(rawTokens.slice(1));
4784
- allTokens.push(...sshHosts);
4976
+ allTokens = allTokens.concat(sshHosts);
4785
4977
  }
4786
4978
  if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
4787
4979
  const prov = checkProvenance2(firstToken, cwd);
@@ -4810,7 +5002,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4810
5002
  };
4811
5003
  }
4812
5004
  }
4813
- if (isSqlTool(toolName, config.policy.toolInspection)) {
5005
+ if (isSqlTool(toolName, layers)) {
4814
5006
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
4815
5007
  }
4816
5008
  } else {
@@ -4818,7 +5010,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4818
5010
  if (args && typeof args === "object") {
4819
5011
  const flattenedArgs = JSON.stringify(args).toLowerCase();
4820
5012
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
4821
- allTokens.push(...extraTokens);
5013
+ allTokens = allTokens.concat(extraTokens);
4822
5014
  }
4823
5015
  }
4824
5016
  const isManual = agent === "Terminal";
@@ -5587,7 +5779,7 @@ function matchCanaryArgs(args, values) {
5587
5779
  return null;
5588
5780
  }
5589
5781
  }
5590
- 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, 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, 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, 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, LOOP_MAX_RECORDS, 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;
5782
+ 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, 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, 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, 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, LOOP_MAX_RECORDS, 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;
5591
5783
  var init_dist = __esm({
5592
5784
  "packages/policy-engine/dist/index.mjs"() {
5593
5785
  "use strict";
@@ -7964,7 +8156,23 @@ var init_dist = __esm({
7964
8156
  "shell",
7965
8157
  "exec_command"
7966
8158
  ]);
7967
- MCP_EXECUTORS = /* @__PURE__ */ new Set([...BASH_TOOL_NAMES, "exec", "run_command", "terminal"]);
8159
+ MCP_EXECUTORS = /* @__PURE__ */ new Set([
8160
+ ...BASH_TOOL_NAMES,
8161
+ "exec",
8162
+ "run_command",
8163
+ "terminal",
8164
+ "execute_command",
8165
+ "execute_shell",
8166
+ "execute_shell_command",
8167
+ "shell_execute",
8168
+ "run_shell",
8169
+ "run_terminal_command",
8170
+ "start_process"
8171
+ ]);
8172
+ BUILTIN_FIELDS = ["command", "cmd", "script"];
8173
+ ARGV_FIELDS = ["args", "argv"];
8174
+ BARE_WORD = /^[A-Za-z0-9_./+:@%,-]+$/;
8175
+ MAX_ARGV_CHARACTERS = 262144;
7968
8176
  ({ syntax: syntax2 } = import_mvdan_sh2.default);
7969
8177
  MAX_ALTERNATIVES = 64;
7970
8178
  MAX_DEPTH = 64;
@@ -10090,6 +10298,7 @@ zoo`.split("\n");
10090
10298
  indices = new Map(BIP39_ENGLISH.map((word, i) => [word, i]));
10091
10299
  lengths = [24, 21, 18, 15, 12];
10092
10300
  hash = (value) => (0, import_crypto2.createHash)("sha256").update(value).digest();
10301
+ MAX_MNEMONIC_HASHES = 25e3;
10093
10302
  IBAN_LENGTH = {
10094
10303
  AD: 24,
10095
10304
  AE: 23,
@@ -10197,6 +10406,7 @@ zoo`.split("\n");
10197
10406
  CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
10198
10407
  CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
10199
10408
  INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
10409
+ INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
10200
10410
  HOMOGLYPHS = {
10201
10411
  // Cyrillic lowercase
10202
10412
  \u0430: "a",
@@ -10855,6 +11065,7 @@ zoo`.split("\n");
10855
11065
  MAX_DEPTH2 = 5;
10856
11066
  MAX_STRING_BYTES = 1e5;
10857
11067
  MAX_JSON_PARSE_BYTES = 1e4;
11068
+ BATCH_SEPARATOR = "\n";
10858
11069
  ({ syntax: syntax3 } = import_mvdan_sh3.default);
10859
11070
  NPM_INSTALL_VERBS = /* @__PURE__ */ new Set([
10860
11071
  "install",
@@ -11324,16 +11535,24 @@ zoo`.split("\n");
11324
11535
  ["browser_batch", ["actions[].input.url"]]
11325
11536
  ]);
11326
11537
  SECRET_VIEW_LIMITS = {
11538
+ /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
11327
11539
  maxDepth: 4,
11328
- maxViewsPerLeaf: 64,
11329
- maxViews: 256,
11330
- maxDerivedBytes: 1048576,
11331
- maxCandidates: 4096
11540
+ /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
11541
+ maxNestedViewsPerLeaf: 64,
11542
+ maxNestedBytes: 1048576,
11543
+ /** Joined hostname-label text per call; past it the call is reviewed. */
11544
+ maxJoinedLabelBytes: 1048576,
11545
+ /** Decoded fragments are matched in batches of about this many characters. */
11546
+ batchCharacters: 65536,
11547
+ /** Only a valid DNS name has its labels joined. */
11548
+ maxHostLength: 253,
11549
+ maxLabelLength: 63,
11550
+ maxLabels: 127
11332
11551
  };
11333
11552
  ARGUMENT_SCAN_LIMITS = {
11334
11553
  maxDepth: 12,
11335
11554
  maxNodes: 16384,
11336
- /** Raw text is matched in windows of this size; decoding views use the first window. */
11555
+ /** Text is matched and decoded in windows of this size. */
11337
11556
  maxStringCharacters: 1e5,
11338
11557
  /** Windows overlap by this much so a token on a window edge is still seen whole. */
11339
11558
  windowOverlap: 4096,
@@ -11343,6 +11562,11 @@ zoo`.split("\n");
11343
11562
  maxTailBytes: 4194304,
11344
11563
  maxJsonCharacters: 1e4
11345
11564
  };
11565
+ RUN_CHAR = /[A-Za-z0-9+/=_%\\.-]/;
11566
+ runChar = (value, index) => {
11567
+ const char = value[index] ?? "";
11568
+ return RUN_CHAR.test(char) || char !== "" && isInvisibleChar(char);
11569
+ };
11346
11570
  VERDICT_RANK = {
11347
11571
  allow: 0,
11348
11572
  review: 1,
@@ -11683,6 +11907,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
11683
11907
  taintFromEid: meta.taintFromEid,
11684
11908
  ...meta.taintSource && { taintSource: meta.taintSource }
11685
11909
  } : {};
11910
+ const exposureFields = {
11911
+ ...meta?.exposure ? { exposure: meta.exposure } : {},
11912
+ ...meta?.exposureFromEid ? { exposureFromEid: meta.exposureFromEid } : {}
11913
+ };
11686
11914
  const eid = generateEventId();
11687
11915
  appendToLog(LOCAL_AUDIT_LOG, {
11688
11916
  // eid first: the outbox shipper dedups on it, and a fixed leading field
@@ -11705,6 +11933,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
11705
11933
  ...loopCountField,
11706
11934
  ...transcriptPathField,
11707
11935
  ...taintFields,
11936
+ ...exposureFields,
11708
11937
  ...testRun,
11709
11938
  agent: meta?.agent,
11710
11939
  mcpServer: meta?.mcpServer,
@@ -11747,10 +11976,72 @@ module.exports = __toCommonJS(src_exports);
11747
11976
  // src/core.ts
11748
11977
  init_audit();
11749
11978
 
11979
+ // src/auth/trusted-recipients.ts
11980
+ var import_fs2 = __toESM(require("fs"));
11981
+ var import_path2 = __toESM(require("path"));
11982
+ var import_os2 = __toESM(require("os"));
11983
+ init_dist();
11984
+ function getTrustedRecipientsPath() {
11985
+ return import_path2.default.join(import_os2.default.homedir(), ".node9", "trusted-recipients.json");
11986
+ }
11987
+ function readTrustedRecipients() {
11988
+ try {
11989
+ const raw = import_fs2.default.readFileSync(getTrustedRecipientsPath(), "utf8");
11990
+ const parsed = JSON.parse(raw);
11991
+ return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
11992
+ } catch {
11993
+ return [];
11994
+ }
11995
+ }
11996
+ function writeTrustedRecipients(entries) {
11997
+ const filePath = getTrustedRecipientsPath();
11998
+ import_fs2.default.mkdirSync(import_path2.default.dirname(filePath), { recursive: true });
11999
+ const tmp = filePath + ".node9-tmp";
12000
+ import_fs2.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
12001
+ import_fs2.default.renameSync(tmp, filePath);
12002
+ }
12003
+ function normalizeRecipient(raw) {
12004
+ const v = raw.trim();
12005
+ if (!v) return null;
12006
+ if (/^(email|host|iban):/i.test(v)) {
12007
+ const i = v.indexOf(":");
12008
+ const kind = v.slice(0, i).toLowerCase();
12009
+ const inner = normalizeRecipient(v.slice(i + 1));
12010
+ return inner && inner.startsWith(`${kind}:`) ? inner : null;
12011
+ }
12012
+ const found = extractArgDestinations({ to: v });
12013
+ return found.length === 1 ? found[0] : null;
12014
+ }
12015
+ function addTrustedRecipient(raw, addedBy = "user") {
12016
+ const value = normalizeRecipient(raw);
12017
+ if (!value) return null;
12018
+ const entries = readTrustedRecipients();
12019
+ if (!entries.some((e) => e.value === value)) {
12020
+ entries.push({ value, addedAt: Date.now(), addedBy });
12021
+ writeTrustedRecipients(entries);
12022
+ }
12023
+ return value;
12024
+ }
12025
+ var cache = null;
12026
+ function mtimeOf() {
12027
+ try {
12028
+ return import_fs2.default.statSync(getTrustedRecipientsPath()).mtimeMs;
12029
+ } catch {
12030
+ return 0;
12031
+ }
12032
+ }
12033
+ function isTrustedRecipient(value) {
12034
+ const mtime = mtimeOf();
12035
+ if (!cache || cache.mtime !== mtime) {
12036
+ cache = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
12037
+ }
12038
+ return cache.values.has(value);
12039
+ }
12040
+
11750
12041
  // src/config/index.ts
11751
- var import_fs4 = __toESM(require("fs"));
11752
- var import_path4 = __toESM(require("path"));
11753
- var import_os4 = __toESM(require("os"));
12042
+ var import_fs5 = __toESM(require("fs"));
12043
+ var import_path5 = __toESM(require("path"));
12044
+ var import_os5 = __toESM(require("os"));
11754
12045
 
11755
12046
  // src/config-schema.ts
11756
12047
  var import_zod = require("zod");
@@ -12019,17 +12310,17 @@ function sanitizeConfig(raw) {
12019
12310
  }
12020
12311
 
12021
12312
  // src/shields.ts
12022
- var import_fs2 = __toESM(require("fs"));
12023
- var import_path2 = __toESM(require("path"));
12024
- var import_os2 = __toESM(require("os"));
12313
+ var import_fs3 = __toESM(require("fs"));
12314
+ var import_path3 = __toESM(require("path"));
12315
+ var import_os3 = __toESM(require("os"));
12025
12316
  init_dist();
12026
12317
  init_dist();
12027
- var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
12318
+ var USER_SHIELDS_DIR = import_path3.default.join(import_os3.default.homedir(), ".node9", "shields");
12028
12319
  function loadUserShields() {
12029
12320
  const result = {};
12030
12321
  let entries;
12031
12322
  try {
12032
- entries = import_fs2.default.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
12323
+ entries = import_fs3.default.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
12033
12324
  } catch (err) {
12034
12325
  if (err.code !== "ENOENT") {
12035
12326
  process.stderr.write(
@@ -12040,9 +12331,9 @@ function loadUserShields() {
12040
12331
  return result;
12041
12332
  }
12042
12333
  for (const file2 of entries) {
12043
- const filePath = import_path2.default.join(USER_SHIELDS_DIR, file2);
12334
+ const filePath = import_path3.default.join(USER_SHIELDS_DIR, file2);
12044
12335
  try {
12045
- const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
12336
+ const raw = JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
12046
12337
  const v = validateShieldDefinition(raw);
12047
12338
  if ("error" in v) {
12048
12339
  process.stderr.write(`[node9] ${v.error}: ${filePath}
@@ -12073,10 +12364,10 @@ function getShield(name) {
12073
12364
  const resolved = resolveShieldName(name);
12074
12365
  return resolved ? SHIELDS[resolved] : null;
12075
12366
  }
12076
- var SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
12367
+ var SHIELDS_STATE_FILE = import_path3.default.join(import_os3.default.homedir(), ".node9", "shields.json");
12077
12368
  function readShieldsFile() {
12078
12369
  try {
12079
- const raw = import_fs2.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
12370
+ const raw = import_fs3.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
12080
12371
  if (!raw.trim()) return { active: [] };
12081
12372
  const parsed = JSON.parse(raw);
12082
12373
  const active = Array.isArray(parsed.active) ? parsed.active.filter(
@@ -12288,15 +12579,15 @@ function pathRules(rawPath, verdict, reason) {
12288
12579
  }
12289
12580
 
12290
12581
  // src/auth/trusted-hosts.ts
12291
- var import_fs3 = __toESM(require("fs"));
12292
- var import_path3 = __toESM(require("path"));
12293
- var import_os3 = __toESM(require("os"));
12582
+ var import_fs4 = __toESM(require("fs"));
12583
+ var import_path4 = __toESM(require("path"));
12584
+ var import_os4 = __toESM(require("os"));
12294
12585
  function getTrustedHostsPath() {
12295
- return import_path3.default.join(import_os3.default.homedir(), ".node9", "trusted-hosts.json");
12586
+ return import_path4.default.join(import_os4.default.homedir(), ".node9", "trusted-hosts.json");
12296
12587
  }
12297
12588
  function readTrustedHosts() {
12298
12589
  try {
12299
- const raw = import_fs3.default.readFileSync(getTrustedHostsPath(), "utf8");
12590
+ const raw = import_fs4.default.readFileSync(getTrustedHostsPath(), "utf8");
12300
12591
  const parsed = JSON.parse(raw);
12301
12592
  return Array.isArray(parsed.hosts) ? parsed.hosts : [];
12302
12593
  } catch {
@@ -12307,7 +12598,7 @@ var _cache = null;
12307
12598
  var CACHE_TTL_MS = 5e3;
12308
12599
  function getFileMtime() {
12309
12600
  try {
12310
- return import_fs3.default.statSync(getTrustedHostsPath()).mtimeMs;
12601
+ return import_fs4.default.statSync(getTrustedHostsPath()).mtimeMs;
12311
12602
  } catch {
12312
12603
  return 0;
12313
12604
  }
@@ -12772,7 +13063,8 @@ var DEFAULT_CONFIG = {
12772
13063
  spotlight: true,
12773
13064
  afterExposure: "ask-once",
12774
13065
  orgDomains: [],
12775
- roles: {}
13066
+ roles: {},
13067
+ trustedRecipients: []
12776
13068
  },
12777
13069
  trustedHosts: [],
12778
13070
  trustedHostsManaged: false,
@@ -12846,8 +13138,8 @@ function noteRejectedApiUrl(raw) {
12846
13138
  if (rejectedApiUrlsSeen.has(key)) return;
12847
13139
  rejectedApiUrlsSeen.add(key);
12848
13140
  try {
12849
- import_fs4.default.appendFileSync(
12850
- import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
13141
+ import_fs5.default.appendFileSync(
13142
+ import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log"),
12851
13143
  `[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
12852
13144
  `
12853
13145
  );
@@ -12864,9 +13156,9 @@ function getCredentials() {
12864
13156
  };
12865
13157
  }
12866
13158
  try {
12867
- const credPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "credentials.json");
12868
- if (import_fs4.default.existsSync(credPath)) {
12869
- const creds = JSON.parse(import_fs4.default.readFileSync(credPath, "utf-8"));
13159
+ const credPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
13160
+ if (import_fs5.default.existsSync(credPath)) {
13161
+ const creds = JSON.parse(import_fs5.default.readFileSync(credPath, "utf-8"));
12870
13162
  const profileName = process.env.NODE9_PROFILE || "default";
12871
13163
  const profile = creds[profileName];
12872
13164
  if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
@@ -12898,7 +13190,7 @@ function readRulesCacheResilient(cacheFile) {
12898
13190
  for (let attempt = 0; attempt < 3; attempt++) {
12899
13191
  let content;
12900
13192
  try {
12901
- content = import_fs4.default.readFileSync(cacheFile, "utf-8");
13193
+ content = import_fs5.default.readFileSync(cacheFile, "utf-8");
12902
13194
  existed = true;
12903
13195
  } catch (err) {
12904
13196
  if (err.code === "ENOENT") return {};
@@ -12913,10 +13205,10 @@ function readRulesCacheResilient(cacheFile) {
12913
13205
  }
12914
13206
  }
12915
13207
  if (existed || sawReadError) {
12916
- const backup = import_path4.default.join(import_path4.default.dirname(cacheFile), "rules-cache.last-good.json");
13208
+ const backup = import_path5.default.join(import_path5.default.dirname(cacheFile), "rules-cache.last-good.json");
12917
13209
  if (backup !== cacheFile) {
12918
13210
  try {
12919
- const raw = JSON.parse(import_fs4.default.readFileSync(backup, "utf-8"));
13211
+ const raw = JSON.parse(import_fs5.default.readFileSync(backup, "utf-8"));
12920
13212
  logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
12921
13213
  lastParsedRulesCache = raw;
12922
13214
  return raw;
@@ -12939,8 +13231,8 @@ function logCacheReadIssue(cacheFile, kind) {
12939
13231
  if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
12940
13232
  cacheReadLastLoggedAt = now;
12941
13233
  try {
12942
- import_fs4.default.appendFileSync(
12943
- import_path4.default.join(import_os4.default.homedir(), ".node9", "hook-debug.log"),
13234
+ import_fs5.default.appendFileSync(
13235
+ import_path5.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log"),
12944
13236
  `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
12945
13237
  `
12946
13238
  );
@@ -12961,8 +13253,8 @@ function catalogSettingsFrom(settings, policy) {
12961
13253
  }
12962
13254
  function getConfig(cwd) {
12963
13255
  if (!cwd && cachedConfig) return cachedConfig;
12964
- const globalPath = import_path4.default.join(import_os4.default.homedir(), ".node9", "config.json");
12965
- const projectPath = import_path4.default.join(cwd ?? process.cwd(), "node9.config.json");
13256
+ const globalPath = import_path5.default.join(import_os5.default.homedir(), ".node9", "config.json");
13257
+ const projectPath = import_path5.default.join(cwd ?? process.cwd(), "node9.config.json");
12966
13258
  const globalConfig = tryLoadConfig(globalPath);
12967
13259
  const projectConfig = tryLoadConfig(projectPath, { project: true });
12968
13260
  const mergedSettings = {
@@ -12975,6 +13267,7 @@ function getConfig(cwd) {
12975
13267
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
12976
13268
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
12977
13269
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
13270
+ projectToolInspection: {},
12978
13271
  // Shipped rules carry `builtin`: the catalog's per-check value governs
12979
13272
  // them and only them (a user rule that reuses a name keeps its verdict).
12980
13273
  smartRules: DEFAULT_CONFIG.policy.smartRules.map((r) => ({ ...r, builtin: true })),
@@ -13001,7 +13294,8 @@ function getConfig(cwd) {
13001
13294
  untrustedContent: {
13002
13295
  ...DEFAULT_CONFIG.policy.untrustedContent,
13003
13296
  orgDomains: [...DEFAULT_CONFIG.policy.untrustedContent.orgDomains],
13004
- roles: {}
13297
+ roles: {},
13298
+ trustedRecipients: []
13005
13299
  },
13006
13300
  // Left empty on purpose: the local file is read fresh at policy-eval time
13007
13301
  // (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
@@ -13047,8 +13341,14 @@ function getConfig(cwd) {
13047
13341
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
13048
13342
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
13049
13343
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
13050
- if (p.toolInspection)
13051
- mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
13344
+ if (p.toolInspection) {
13345
+ if (isProject)
13346
+ mergedPolicy.projectToolInspection = {
13347
+ ...mergedPolicy.projectToolInspection,
13348
+ ...p.toolInspection
13349
+ };
13350
+ else mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
13351
+ }
13052
13352
  if (p.smartRules) {
13053
13353
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
13054
13354
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
@@ -13196,7 +13496,7 @@ function getConfig(cwd) {
13196
13496
  let cloudMandatesEnforcement = false;
13197
13497
  let cloudMandatesAppPerm = false;
13198
13498
  {
13199
- const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
13499
+ const cacheFile = import_path5.default.join(import_os5.default.homedir(), ".node9", "rules-cache.json");
13200
13500
  try {
13201
13501
  const raw = readRulesCacheResilient(cacheFile);
13202
13502
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
@@ -13589,10 +13889,10 @@ function getConfig(cwd) {
13589
13889
  }
13590
13890
  function tryLoadConfig(filePath, options = {}) {
13591
13891
  v2ChecksByPath.delete(filePath);
13592
- if (!import_fs4.default.existsSync(filePath)) return null;
13892
+ if (!import_fs5.default.existsSync(filePath)) return null;
13593
13893
  let raw;
13594
13894
  try {
13595
- raw = JSON.parse(import_fs4.default.readFileSync(filePath, "utf-8"));
13895
+ raw = JSON.parse(import_fs5.default.readFileSync(filePath, "utf-8"));
13596
13896
  } catch (err) {
13597
13897
  const msg = err instanceof Error ? err.message : String(err);
13598
13898
  process.stderr.write(
@@ -13667,6 +13967,13 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
13667
13967
  if (replaceLists) cur.orgDomains = domains;
13668
13968
  else for (const d of domains) if (!cur.orgDomains.includes(d)) cur.orgDomains.push(d);
13669
13969
  }
13970
+ if (replaceLists && Array.isArray(uc.trustedRecipients)) {
13971
+ cur.trustedRecipients = [
13972
+ ...new Set(
13973
+ uc.trustedRecipients.filter((v) => typeof v === "string").map((v) => normalizeRecipient(v)).filter((v) => v !== null)
13974
+ )
13975
+ ].slice(0, 200);
13976
+ }
13670
13977
  if (uc.roles && typeof uc.roles === "object" && !Array.isArray(uc.roles)) {
13671
13978
  const next = replaceLists ? {} : cur.roles;
13672
13979
  for (const [server, tools] of Object.entries(uc.roles)) {
@@ -13688,20 +13995,20 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
13688
13995
  init_dist();
13689
13996
 
13690
13997
  // src/dlp.ts
13691
- var import_fs5 = __toESM(require("fs"));
13692
- var import_path5 = __toESM(require("path"));
13998
+ var import_fs6 = __toESM(require("fs"));
13999
+ var import_path6 = __toESM(require("path"));
13693
14000
  init_dist();
13694
14001
  init_dist();
13695
14002
  function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
13696
14003
  if (!filePath) return null;
13697
14004
  let resolved;
13698
14005
  try {
13699
- const absolute = import_path5.default.resolve(cwd, filePath);
13700
- resolved = import_fs5.default.realpathSync.native(absolute);
14006
+ const absolute = import_path6.default.resolve(cwd, filePath);
14007
+ resolved = import_fs6.default.realpathSync.native(absolute);
13701
14008
  } catch (err) {
13702
14009
  const code = err.code;
13703
14010
  if (code === "ENOENT" || code === "ENOTDIR") {
13704
- resolved = import_path5.default.resolve(cwd, filePath);
14011
+ resolved = import_path6.default.resolve(cwd, filePath);
13705
14012
  } else {
13706
14013
  return sensitivePathMatch(filePath);
13707
14014
  }
@@ -13710,30 +14017,30 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
13710
14017
  }
13711
14018
 
13712
14019
  // src/policy/file-access.ts
13713
- var import_fs6 = __toESM(require("fs"));
13714
- var import_os5 = __toESM(require("os"));
13715
- var import_path6 = __toESM(require("path"));
14020
+ var import_fs7 = __toESM(require("fs"));
14021
+ var import_os6 = __toESM(require("os"));
14022
+ var import_path7 = __toESM(require("path"));
13716
14023
  init_dist();
13717
14024
  function absoluteSpelling(p, cwd, home) {
13718
14025
  if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
13719
- return import_path6.default.isAbsolute(p) ? p : `${cwd}/${p}`;
14026
+ return import_path7.default.isAbsolute(p) ? p : `${cwd}/${p}`;
13720
14027
  }
13721
14028
  function physical(p, followFinal, links = 0) {
13722
14029
  if (links > 40) throw new Error("Too many symbolic links");
13723
14030
  if (!followFinal) {
13724
- const parent = physical(import_path6.default.dirname(p), true, links);
13725
- return { path: `${parent.path}/${import_path6.default.basename(p)}`, missing: parent.missing };
14031
+ const parent = physical(import_path7.default.dirname(p), true, links);
14032
+ return { path: `${parent.path}/${import_path7.default.basename(p)}`, missing: parent.missing };
13726
14033
  }
13727
14034
  try {
13728
- return { path: import_fs6.default.realpathSync.native(p), missing: false };
14035
+ return { path: import_fs7.default.realpathSync.native(p), missing: false };
13729
14036
  } catch (error) {
13730
14037
  const code = error.code;
13731
14038
  if (!["ENOENT", "ENOTDIR"].includes(code ?? "")) throw error;
13732
14039
  try {
13733
- if (import_fs6.default.lstatSync(p).isSymbolicLink()) {
13734
- const target = import_fs6.default.readlinkSync(p);
14040
+ if (import_fs7.default.lstatSync(p).isSymbolicLink()) {
14041
+ const target = import_fs7.default.readlinkSync(p);
13735
14042
  return physical(
13736
- import_path6.default.isAbsolute(target) ? target : `${import_path6.default.dirname(p)}/${target}`,
14043
+ import_path7.default.isAbsolute(target) ? target : `${import_path7.default.dirname(p)}/${target}`,
13737
14044
  true,
13738
14045
  links + 1
13739
14046
  );
@@ -13742,13 +14049,13 @@ function physical(p, followFinal, links = 0) {
13742
14049
  if (!["ENOENT", "ENOTDIR"].includes(linkError.code ?? ""))
13743
14050
  throw linkError;
13744
14051
  }
13745
- const parent = import_path6.default.dirname(p);
14052
+ const parent = import_path7.default.dirname(p);
13746
14053
  if (parent === p) return { path: p, missing: true };
13747
14054
  const resolved = physical(parent, true, links);
13748
- return { path: `${resolved.path}/${import_path6.default.basename(p)}`, missing: true };
14055
+ return { path: `${resolved.path}/${import_path7.default.basename(p)}`, missing: true };
13749
14056
  }
13750
14057
  }
13751
- function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = import_os5.default.homedir()) {
14058
+ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = import_os6.default.homedir()) {
13752
14059
  const context = { cwd, homeDirs: [home, "/root"] };
13753
14060
  const accesses = collectFileAccesses(tool, args, context, inspection);
13754
14061
  const handledPaths = /* @__PURE__ */ new Set();
@@ -13771,14 +14078,14 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
13771
14078
  const raw = absoluteSpelling(access.path, access.cwd ?? cwd, home);
13772
14079
  if (access.directoryTarget) {
13773
14080
  try {
13774
- if (!import_fs6.default.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
14081
+ if (!import_fs7.default.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
13775
14082
  continue;
13776
14083
  } catch {
13777
14084
  }
13778
14085
  }
13779
14086
  if (access.operation === "create") {
13780
14087
  try {
13781
- import_fs6.default.statSync(raw);
14088
+ import_fs7.default.statSync(raw);
13782
14089
  continue;
13783
14090
  } catch {
13784
14091
  }
@@ -13827,27 +14134,27 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
13827
14134
  }
13828
14135
 
13829
14136
  // src/utils/provenance.ts
13830
- var import_fs7 = __toESM(require("fs"));
13831
- var import_path7 = __toESM(require("path"));
13832
- var import_os6 = __toESM(require("os"));
14137
+ var import_fs8 = __toESM(require("fs"));
14138
+ var import_path8 = __toESM(require("path"));
14139
+ var import_os7 = __toESM(require("os"));
13833
14140
  var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
13834
14141
  var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
13835
14142
  var USER_PREFIXES = [
13836
- import_path7.default.join(import_os6.default.homedir(), "bin"),
13837
- import_path7.default.join(import_os6.default.homedir(), ".local", "bin"),
13838
- import_path7.default.join(import_os6.default.homedir(), ".cargo", "bin"),
13839
- import_path7.default.join(import_os6.default.homedir(), ".npm-global", "bin"),
13840
- import_path7.default.join(import_os6.default.homedir(), ".volta", "bin")
14143
+ import_path8.default.join(import_os7.default.homedir(), "bin"),
14144
+ import_path8.default.join(import_os7.default.homedir(), ".local", "bin"),
14145
+ import_path8.default.join(import_os7.default.homedir(), ".cargo", "bin"),
14146
+ import_path8.default.join(import_os7.default.homedir(), ".npm-global", "bin"),
14147
+ import_path8.default.join(import_os7.default.homedir(), ".volta", "bin")
13841
14148
  ];
13842
14149
  var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
13843
14150
  function findInPath(cmd) {
13844
- if (import_path7.default.posix.isAbsolute(cmd)) return cmd;
14151
+ if (import_path8.default.posix.isAbsolute(cmd)) return cmd;
13845
14152
  const pathEnv = process.env.PATH ?? "";
13846
- for (const dir2 of pathEnv.split(import_path7.default.delimiter)) {
14153
+ for (const dir2 of pathEnv.split(import_path8.default.delimiter)) {
13847
14154
  if (!dir2) continue;
13848
- const full = import_path7.default.join(dir2, cmd);
14155
+ const full = import_path8.default.join(dir2, cmd);
13849
14156
  try {
13850
- import_fs7.default.accessSync(full, import_fs7.default.constants.X_OK);
14157
+ import_fs8.default.accessSync(full, import_fs8.default.constants.X_OK);
13851
14158
  return full;
13852
14159
  } catch {
13853
14160
  }
@@ -13858,7 +14165,7 @@ function _classifyPath(resolved, cwd) {
13858
14165
  if (cwd && resolved.startsWith(cwd + "/")) {
13859
14166
  return { trustLevel: "user", reason: "binary in project directory" };
13860
14167
  }
13861
- const osTmp = import_os6.default.tmpdir();
14168
+ const osTmp = import_os7.default.tmpdir();
13862
14169
  const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
13863
14170
  if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
13864
14171
  return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
@@ -13876,7 +14183,7 @@ function _classifyPath(resolved, cwd) {
13876
14183
  }
13877
14184
  function checkProvenance(cmd, cwd) {
13878
14185
  const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
13879
- if (import_path7.default.posix.isAbsolute(bare)) {
14186
+ if (import_path8.default.posix.isAbsolute(bare)) {
13880
14187
  const early = _classifyPath(bare, cwd);
13881
14188
  if (early.trustLevel === "suspect") {
13882
14189
  return { resolvedPath: bare, ...early };
@@ -13892,7 +14199,7 @@ function checkProvenance(cmd, cwd) {
13892
14199
  reason: "binary not found in PATH"
13893
14200
  };
13894
14201
  }
13895
- resolved = import_fs7.default.realpathSync(found);
14202
+ resolved = import_fs8.default.realpathSync(found);
13896
14203
  } catch {
13897
14204
  return {
13898
14205
  resolvedPath: cmd,
@@ -13901,7 +14208,7 @@ function checkProvenance(cmd, cwd) {
13901
14208
  };
13902
14209
  }
13903
14210
  try {
13904
- const stat = import_fs7.default.statSync(resolved);
14211
+ const stat = import_fs8.default.statSync(resolved);
13905
14212
  if (stat.mode & 2) {
13906
14213
  return {
13907
14214
  resolvedPath: resolved,
@@ -13926,7 +14233,7 @@ init_dist();
13926
14233
  init_dist();
13927
14234
  async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
13928
14235
  const config = opts?.config ?? getConfig(cwd);
13929
- const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, config.policy.toolInspection);
14236
+ const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, inspectionLayers(config.policy));
13930
14237
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
13931
14238
  return evaluatePolicy(
13932
14239
  config,
@@ -13955,32 +14262,32 @@ function isIgnoredTool2(toolName) {
13955
14262
  }
13956
14263
 
13957
14264
  // src/auth/state.ts
13958
- var import_fs9 = __toESM(require("fs"));
13959
- var import_path9 = __toESM(require("path"));
13960
- var import_os7 = __toESM(require("os"));
14265
+ var import_fs10 = __toESM(require("fs"));
14266
+ var import_path10 = __toESM(require("path"));
14267
+ var import_os8 = __toESM(require("os"));
13961
14268
 
13962
14269
  // src/utils/atomic-write.ts
13963
- var import_fs8 = __toESM(require("fs"));
13964
- var import_path8 = __toESM(require("path"));
14270
+ var import_fs9 = __toESM(require("fs"));
14271
+ var import_path9 = __toESM(require("path"));
13965
14272
  var import_crypto6 = require("crypto");
13966
14273
  function atomicWriteSync(filePath, data, options) {
13967
- const dir2 = import_path8.default.dirname(filePath);
13968
- if (!import_fs8.default.existsSync(dir2)) import_fs8.default.mkdirSync(dir2, { recursive: true });
14274
+ const dir2 = import_path9.default.dirname(filePath);
14275
+ if (!import_fs9.default.existsSync(dir2)) import_fs9.default.mkdirSync(dir2, { recursive: true });
13969
14276
  const tmpPath = `${filePath}.${(0, import_crypto6.randomUUID)()}.tmp`;
13970
14277
  try {
13971
- import_fs8.default.writeFileSync(tmpPath, data, options);
14278
+ import_fs9.default.writeFileSync(tmpPath, data, options);
13972
14279
  } catch (err) {
13973
14280
  try {
13974
- import_fs8.default.unlinkSync(tmpPath);
14281
+ import_fs9.default.unlinkSync(tmpPath);
13975
14282
  } catch {
13976
14283
  }
13977
14284
  throw err;
13978
14285
  }
13979
14286
  try {
13980
- import_fs8.default.renameSync(tmpPath, filePath);
14287
+ import_fs9.default.renameSync(tmpPath, filePath);
13981
14288
  } catch (err) {
13982
14289
  try {
13983
- import_fs8.default.unlinkSync(tmpPath);
14290
+ import_fs9.default.unlinkSync(tmpPath);
13984
14291
  } catch {
13985
14292
  }
13986
14293
  throw err;
@@ -13988,8 +14295,8 @@ function atomicWriteSync(filePath, data, options) {
13988
14295
  }
13989
14296
 
13990
14297
  // src/auth/state.ts
13991
- var PAUSED_FILE = import_path9.default.join(import_os7.default.homedir(), ".node9", "PAUSED");
13992
- var TRUST_FILE = import_path9.default.join(import_os7.default.homedir(), ".node9", "trust.json");
14298
+ var PAUSED_FILE = import_path10.default.join(import_os8.default.homedir(), ".node9", "PAUSED");
14299
+ var TRUST_FILE = import_path10.default.join(import_os8.default.homedir(), ".node9", "trust.json");
13993
14300
  function extractCommandPattern(toolName, args) {
13994
14301
  const lower2 = toolName.toLowerCase();
13995
14302
  if (lower2 !== "bash" && lower2 !== "execute_bash" && lower2 !== "shell") return void 0;
@@ -14001,11 +14308,11 @@ function extractCommandPattern(toolName, args) {
14001
14308
  }
14002
14309
  function checkPause() {
14003
14310
  try {
14004
- if (!import_fs9.default.existsSync(PAUSED_FILE)) return { paused: false };
14005
- const state = JSON.parse(import_fs9.default.readFileSync(PAUSED_FILE, "utf-8"));
14311
+ if (!import_fs10.default.existsSync(PAUSED_FILE)) return { paused: false };
14312
+ const state = JSON.parse(import_fs10.default.readFileSync(PAUSED_FILE, "utf-8"));
14006
14313
  if (state.expiry > 0 && Date.now() >= state.expiry) {
14007
14314
  try {
14008
- import_fs9.default.unlinkSync(PAUSED_FILE);
14315
+ import_fs10.default.unlinkSync(PAUSED_FILE);
14009
14316
  } catch {
14010
14317
  }
14011
14318
  return { paused: false };
@@ -14017,8 +14324,8 @@ function checkPause() {
14017
14324
  }
14018
14325
  function getActiveTrustSession(toolName, args) {
14019
14326
  try {
14020
- if (!import_fs9.default.existsSync(TRUST_FILE)) return false;
14021
- const trust = JSON.parse(import_fs9.default.readFileSync(TRUST_FILE, "utf-8"));
14327
+ if (!import_fs10.default.existsSync(TRUST_FILE)) return false;
14328
+ const trust = JSON.parse(import_fs10.default.readFileSync(TRUST_FILE, "utf-8"));
14022
14329
  const now = Date.now();
14023
14330
  const active = trust.entries.filter((e) => e.expiry > now);
14024
14331
  if (active.length !== trust.entries.length) {
@@ -14041,8 +14348,8 @@ function writeTrustSession(toolName, durationMs, args) {
14041
14348
  try {
14042
14349
  let trust = { entries: [] };
14043
14350
  try {
14044
- if (import_fs9.default.existsSync(TRUST_FILE)) {
14045
- trust = JSON.parse(import_fs9.default.readFileSync(TRUST_FILE, "utf-8"));
14351
+ if (import_fs10.default.existsSync(TRUST_FILE)) {
14352
+ trust = JSON.parse(import_fs10.default.readFileSync(TRUST_FILE, "utf-8"));
14046
14353
  }
14047
14354
  } catch {
14048
14355
  }
@@ -14064,9 +14371,9 @@ function writeTrustSession(toolName, durationMs, args) {
14064
14371
  }
14065
14372
  function getPersistentDecision(toolName) {
14066
14373
  try {
14067
- const file2 = import_path9.default.join(import_os7.default.homedir(), ".node9", "decisions.json");
14068
- if (!import_fs9.default.existsSync(file2)) return null;
14069
- const decisions = JSON.parse(import_fs9.default.readFileSync(file2, "utf-8"));
14374
+ const file2 = import_path10.default.join(import_os8.default.homedir(), ".node9", "decisions.json");
14375
+ if (!import_fs10.default.existsSync(file2)) return null;
14376
+ const decisions = JSON.parse(import_fs10.default.readFileSync(file2, "utf-8"));
14070
14377
  const d = decisions[toolName];
14071
14378
  if (d === "allow" || d === "deny") return d;
14072
14379
  } catch {
@@ -14075,11 +14382,11 @@ function getPersistentDecision(toolName) {
14075
14382
  }
14076
14383
 
14077
14384
  // src/auth/daemon.ts
14078
- var import_fs10 = __toESM(require("fs"));
14385
+ var import_fs11 = __toESM(require("fs"));
14079
14386
  var import_net = __toESM(require("net"));
14080
- var import_path10 = __toESM(require("path"));
14081
- var import_os8 = __toESM(require("os"));
14082
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
14387
+ var import_path11 = __toESM(require("path"));
14388
+ var import_os9 = __toESM(require("os"));
14389
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path11.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
14083
14390
  function notifyActivitySocket(data) {
14084
14391
  return new Promise((resolve) => {
14085
14392
  try {
@@ -14121,9 +14428,9 @@ var DAEMON_PORT = resolveDaemonPort();
14121
14428
  var DAEMON_HOST = "127.0.0.1";
14122
14429
  function getInternalToken() {
14123
14430
  try {
14124
- const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
14125
- if (!import_fs10.default.existsSync(pidFile)) return null;
14126
- const data = JSON.parse(import_fs10.default.readFileSync(pidFile, "utf-8"));
14431
+ const pidFile = import_path11.default.join(import_os9.default.homedir(), ".node9", "daemon.pid");
14432
+ if (!import_fs11.default.existsSync(pidFile)) return null;
14433
+ const data = JSON.parse(import_fs11.default.readFileSync(pidFile, "utf-8"));
14127
14434
  process.kill(data.pid, 0);
14128
14435
  return data.internalToken ?? null;
14129
14436
  } catch {
@@ -14131,12 +14438,12 @@ function getInternalToken() {
14131
14438
  }
14132
14439
  }
14133
14440
  function isDaemonRunning() {
14134
- const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
14135
- if (import_fs10.default.existsSync(pidFile)) {
14441
+ const pidFile = import_path11.default.join(import_os9.default.homedir(), ".node9", "daemon.pid");
14442
+ if (import_fs11.default.existsSync(pidFile)) {
14136
14443
  let pid;
14137
14444
  let port;
14138
14445
  try {
14139
- const data = JSON.parse(import_fs10.default.readFileSync(pidFile, "utf-8"));
14446
+ const data = JSON.parse(import_fs11.default.readFileSync(pidFile, "utf-8"));
14140
14447
  pid = data.pid;
14141
14448
  port = data.port;
14142
14449
  } catch {
@@ -14154,7 +14461,7 @@ function isDaemonRunning() {
14154
14461
  } catch (err) {
14155
14462
  if (err instanceof Error && "code" in err && err.code === "ESRCH") {
14156
14463
  try {
14157
- import_fs10.default.unlinkSync(pidFile);
14464
+ import_fs11.default.unlinkSync(pidFile);
14158
14465
  } catch {
14159
14466
  }
14160
14467
  }
@@ -14362,10 +14669,10 @@ var import_os18 = __toESM(require("os"));
14362
14669
 
14363
14670
  // src/ui/native.ts
14364
14671
  var import_child_process = require("child_process");
14365
- var import_path12 = __toESM(require("path"));
14672
+ var import_path13 = __toESM(require("path"));
14366
14673
 
14367
14674
  // src/context-sniper.ts
14368
- var import_path11 = __toESM(require("path"));
14675
+ var import_path12 = __toESM(require("path"));
14369
14676
  function smartTruncate(str, maxLen = 500) {
14370
14677
  if (str.length <= maxLen) return str;
14371
14678
  const edge = Math.floor(maxLen / 2) - 3;
@@ -14433,7 +14740,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
14433
14740
  intent = "EDIT";
14434
14741
  if (obj.file_path) {
14435
14742
  editFilePath = String(obj.file_path);
14436
- editFileName = import_path11.default.basename(editFilePath);
14743
+ editFileName = import_path12.default.basename(editFilePath);
14437
14744
  }
14438
14745
  const result = extractContext(String(obj.new_string), matchedWord);
14439
14746
  contextSnippet = result.snippet;
@@ -14501,7 +14808,7 @@ function formatArgs(args, matchedField, matchedWord) {
14501
14808
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
14502
14809
  const obj = parsed;
14503
14810
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
14504
- const file2 = obj.file_path ? import_path12.default.basename(String(obj.file_path)) : "file";
14811
+ const file2 = obj.file_path ? import_path13.default.basename(String(obj.file_path)) : "file";
14505
14812
  const oldPreview = smartTruncate(String(obj.old_string), 120);
14506
14813
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
14507
14814
  return {
@@ -14704,24 +15011,24 @@ end run`;
14704
15011
  }
14705
15012
 
14706
15013
  // src/canary/registry.ts
14707
- var import_fs12 = __toESM(require("fs"));
14708
- var import_os10 = __toESM(require("os"));
14709
- var import_path14 = __toESM(require("path"));
15014
+ var import_fs13 = __toESM(require("fs"));
15015
+ var import_os11 = __toESM(require("os"));
15016
+ var import_path15 = __toESM(require("path"));
14710
15017
  init_dist();
14711
15018
 
14712
15019
  // src/shields/jail.ts
14713
- var import_fs11 = __toESM(require("fs"));
14714
- var import_os9 = __toESM(require("os"));
14715
- var import_path13 = __toESM(require("path"));
15020
+ var import_fs12 = __toESM(require("fs"));
15021
+ var import_os10 = __toESM(require("os"));
15022
+ var import_path14 = __toESM(require("path"));
14716
15023
  init_dist();
14717
15024
  var USER_JAIL_SHIELD = "user-jail";
14718
15025
  function jailStorePath() {
14719
- return import_path13.default.join(import_os9.default.homedir(), ".node9", "jail-paths.json");
15026
+ return import_path14.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
14720
15027
  }
14721
15028
  function readJailPaths() {
14722
15029
  let text;
14723
15030
  try {
14724
- text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
15031
+ text = import_fs12.default.readFileSync(jailStorePath(), "utf8");
14725
15032
  } catch (err) {
14726
15033
  if (err.code === "ENOENT") return [];
14727
15034
  throw err;
@@ -14750,7 +15057,7 @@ function findJailedPathIn(candidate, paths) {
14750
15057
 
14751
15058
  // src/canary/registry.ts
14752
15059
  function canaryStorePath() {
14753
- return import_path14.default.join(import_os10.default.homedir(), ".node9", "canaries.json");
15060
+ return import_path15.default.join(import_os11.default.homedir(), ".node9", "canaries.json");
14754
15061
  }
14755
15062
  function isRecord(x) {
14756
15063
  if (!x || typeof x !== "object") return false;
@@ -14761,7 +15068,7 @@ function loadCanaries(opts) {
14761
15068
  const p = canaryStorePath();
14762
15069
  let raw;
14763
15070
  try {
14764
- raw = import_fs12.default.readFileSync(p, "utf-8");
15071
+ raw = import_fs13.default.readFileSync(p, "utf-8");
14765
15072
  } catch (e) {
14766
15073
  if (e.code === "ENOENT") return [];
14767
15074
  throw e;
@@ -14790,26 +15097,26 @@ init_audit();
14790
15097
  init_dist();
14791
15098
 
14792
15099
  // src/exposure/known-hosts.ts
14793
- var import_fs14 = __toESM(require("fs"));
14794
- var import_path16 = __toESM(require("path"));
15100
+ var import_fs15 = __toESM(require("fs"));
15101
+ var import_path17 = __toESM(require("path"));
14795
15102
 
14796
15103
  // src/utils/git-remotes.ts
14797
- var import_fs13 = __toESM(require("fs"));
14798
- var import_path15 = __toESM(require("path"));
15104
+ var import_fs14 = __toESM(require("fs"));
15105
+ var import_path16 = __toESM(require("path"));
14799
15106
  init_dist();
14800
15107
  function readSmallFile(p, max) {
14801
15108
  let fd;
14802
15109
  try {
14803
- fd = import_fs13.default.openSync(p, import_fs13.default.constants.O_RDONLY | (import_fs13.default.constants.O_NOFOLLOW ?? 0));
14804
- const st = import_fs13.default.fstatSync(fd);
15110
+ fd = import_fs14.default.openSync(p, import_fs14.default.constants.O_RDONLY | (import_fs14.default.constants.O_NOFOLLOW ?? 0));
15111
+ const st = import_fs14.default.fstatSync(fd);
14805
15112
  if (!st.isFile() || st.size > max) return null;
14806
15113
  const buf = Buffer.alloc(st.size);
14807
- const n = import_fs13.default.readSync(fd, buf, 0, st.size, 0);
15114
+ const n = import_fs14.default.readSync(fd, buf, 0, st.size, 0);
14808
15115
  return buf.subarray(0, n).toString("utf8");
14809
15116
  } catch {
14810
15117
  return null;
14811
15118
  } finally {
14812
- if (fd !== void 0) import_fs13.default.closeSync(fd);
15119
+ if (fd !== void 0) import_fs14.default.closeSync(fd);
14813
15120
  }
14814
15121
  }
14815
15122
  function gitRemoteHost(url) {
@@ -14828,35 +15135,35 @@ function gitRemoteHost(url) {
14828
15135
  return null;
14829
15136
  }
14830
15137
  function findGitConfig(dir2, walkUp = true) {
14831
- let cur = import_path15.default.resolve(dir2);
15138
+ let cur = import_path16.default.resolve(dir2);
14832
15139
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
14833
- const dotGit = import_path15.default.join(cur, ".git");
14834
- const direct = readSmallFile(import_path15.default.join(dotGit, "config"), 1e6);
15140
+ const dotGit = import_path16.default.join(cur, ".git");
15141
+ const direct = readSmallFile(import_path16.default.join(dotGit, "config"), 1e6);
14835
15142
  if (direct !== null) return direct;
14836
15143
  const pointer = readSmallFile(dotGit, 4096);
14837
15144
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
14838
15145
  if (m) {
14839
- let gitDir = import_path15.default.resolve(cur, m[1]);
14840
- const common = readSmallFile(import_path15.default.join(gitDir, "commondir"), 4096);
14841
- if (common !== null) gitDir = import_path15.default.resolve(gitDir, common.trim());
14842
- return readSmallFile(import_path15.default.join(gitDir, "config"), 1e6);
15146
+ let gitDir = import_path16.default.resolve(cur, m[1]);
15147
+ const common = readSmallFile(import_path16.default.join(gitDir, "commondir"), 4096);
15148
+ if (common !== null) gitDir = import_path16.default.resolve(gitDir, common.trim());
15149
+ return readSmallFile(import_path16.default.join(gitDir, "config"), 1e6);
14843
15150
  }
14844
- const parent = import_path15.default.dirname(cur);
15151
+ const parent = import_path16.default.dirname(cur);
14845
15152
  if (parent === cur) return null;
14846
15153
  cur = parent;
14847
15154
  }
14848
15155
  return null;
14849
15156
  }
14850
- var cache = /* @__PURE__ */ new Map();
15157
+ var cache2 = /* @__PURE__ */ new Map();
14851
15158
  function readGitRemotes(dir2, opts = {}) {
14852
15159
  if (!dir2) return [];
14853
15160
  const walkUp = opts.walkUp !== false;
14854
- const key = `${walkUp ? "up" : "at"}:${import_path15.default.resolve(dir2)}`;
14855
- const hit = cache.get(key);
15161
+ const key = `${walkUp ? "up" : "at"}:${import_path16.default.resolve(dir2)}`;
15162
+ const hit = cache2.get(key);
14856
15163
  if (hit) return hit;
14857
15164
  const out = [];
14858
15165
  try {
14859
- const cfg = findGitConfig(import_path15.default.resolve(dir2), walkUp);
15166
+ const cfg = findGitConfig(import_path16.default.resolve(dir2), walkUp);
14860
15167
  if (cfg !== null) {
14861
15168
  let current = null;
14862
15169
  for (const line of cfg.split(/\r?\n/)) {
@@ -14878,7 +15185,7 @@ function readGitRemotes(dir2, opts = {}) {
14878
15185
  }
14879
15186
  } catch {
14880
15187
  }
14881
- cache.set(key, out);
15188
+ cache2.set(key, out);
14882
15189
  return out;
14883
15190
  }
14884
15191
 
@@ -14890,17 +15197,17 @@ var ECOSYSTEMS = [
14890
15197
  ["Cargo.toml", "host:crates.io"],
14891
15198
  ["Gemfile", "host:rubygems.org"]
14892
15199
  ];
14893
- var cache2 = /* @__PURE__ */ new Map();
15200
+ var cache3 = /* @__PURE__ */ new Map();
14894
15201
  function manifestRoot(dir2) {
14895
- let cur = import_path16.default.resolve(dir2);
15202
+ let cur = import_path17.default.resolve(dir2);
14896
15203
  for (let i = 0; i < 64; i++) {
14897
15204
  for (const [m] of ECOSYSTEMS) {
14898
15205
  try {
14899
- if (import_fs14.default.existsSync(import_path16.default.join(cur, m))) return cur;
15206
+ if (import_fs15.default.existsSync(import_path17.default.join(cur, m))) return cur;
14900
15207
  } catch {
14901
15208
  }
14902
15209
  }
14903
- const parent = import_path16.default.dirname(cur);
15210
+ const parent = import_path17.default.dirname(cur);
14904
15211
  if (parent === cur) return null;
14905
15212
  cur = parent;
14906
15213
  }
@@ -14909,8 +15216,8 @@ function manifestRoot(dir2) {
14909
15216
  function knownProjectHosts(dir2) {
14910
15217
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
14911
15218
  if (!dir2) return empty;
14912
- const key = import_path16.default.resolve(dir2);
14913
- const hit = cache2.get(key);
15219
+ const key = import_path17.default.resolve(dir2);
15220
+ const hit = cache3.get(key);
14914
15221
  if (hit) return hit;
14915
15222
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
14916
15223
  try {
@@ -14922,24 +15229,24 @@ function knownProjectHosts(dir2) {
14922
15229
  const root = manifestRoot(key);
14923
15230
  if (root !== null) {
14924
15231
  for (const [m, registry] of ECOSYSTEMS) {
14925
- if (import_fs14.default.existsSync(import_path16.default.join(root, m))) out.hosts.add(registry);
15232
+ if (import_fs15.default.existsSync(import_path17.default.join(root, m))) out.hosts.add(registry);
14926
15233
  }
14927
15234
  try {
14928
- if (import_fs14.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
15235
+ if (import_fs15.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
14929
15236
  out.hosts.add("host:rubygems.org");
14930
15237
  } catch {
14931
15238
  }
14932
15239
  }
14933
15240
  } catch {
14934
15241
  }
14935
- cache2.set(key, out);
15242
+ cache3.set(key, out);
14936
15243
  return out;
14937
15244
  }
14938
15245
 
14939
15246
  // src/exposure/writes.ts
14940
- var import_fs15 = __toESM(require("fs"));
14941
- var import_os11 = __toESM(require("os"));
14942
- var import_path17 = __toESM(require("path"));
15247
+ var import_fs16 = __toESM(require("fs"));
15248
+ var import_os12 = __toESM(require("os"));
15249
+ var import_path18 = __toESM(require("path"));
14943
15250
  init_dist();
14944
15251
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
14945
15252
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
@@ -14969,7 +15276,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
14969
15276
  if (typeof a.content === "string") {
14970
15277
  try {
14971
15278
  const next = JSON.parse(a.content);
14972
- const prev = JSON.parse(import_fs15.default.readFileSync(absPath, "utf8"));
15279
+ const prev = JSON.parse(import_fs16.default.readFileSync(absPath, "utf8"));
14973
15280
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
14974
15281
  } catch {
14975
15282
  return LIFECYCLE_RE.test(a.content);
@@ -14979,8 +15286,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
14979
15286
  }
14980
15287
  function judgeWritesAfterExposure(opts) {
14981
15288
  const holds = [];
14982
- const home = opts.home ?? import_os11.default.homedir();
14983
- const tmp = normalizeFilePath(import_os11.default.tmpdir());
15289
+ const home = opts.home ?? import_os12.default.homedir();
15290
+ const tmp = normalizeFilePath(import_os12.default.tmpdir());
14984
15291
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
14985
15292
  const seen = /* @__PURE__ */ new Set();
14986
15293
  for (const access of opts.accesses) {
@@ -14990,7 +15297,7 @@ function judgeWritesAfterExposure(opts) {
14990
15297
  seen.add(p);
14991
15298
  if (p === "/dev" || p.startsWith("/dev/")) continue;
14992
15299
  if (root !== null && isInside(root, p)) {
14993
- if (isAgentInstructionPath(p, root) && (import_path17.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
15300
+ if (isAgentInstructionPath(p, root) && (import_path18.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
14994
15301
  holds.push({ path: p, why: "agent-instruction" });
14995
15302
  continue;
14996
15303
  }
@@ -15012,25 +15319,25 @@ function judgeWritesAfterExposure(opts) {
15012
15319
 
15013
15320
  // src/exposure/marker.ts
15014
15321
  var import_crypto7 = __toESM(require("crypto"));
15015
- var import_fs16 = __toESM(require("fs"));
15016
- var import_os12 = __toESM(require("os"));
15017
- var import_path18 = __toESM(require("path"));
15322
+ var import_fs17 = __toESM(require("fs"));
15323
+ var import_os13 = __toESM(require("os"));
15324
+ var import_path19 = __toESM(require("path"));
15018
15325
 
15019
15326
  // src/daemon/exposure-store.ts
15020
15327
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
15021
15328
 
15022
15329
  // src/exposure/marker.ts
15023
15330
  function dir() {
15024
- return import_path18.default.join(import_os12.default.homedir(), ".node9", "exposure-sessions");
15331
+ return import_path19.default.join(import_os13.default.homedir(), ".node9", "exposure-sessions");
15025
15332
  }
15026
15333
  function file(sessionId) {
15027
15334
  const id = import_crypto7.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
15028
- return import_path18.default.join(dir(), id);
15335
+ return import_path19.default.join(dir(), id);
15029
15336
  }
15030
15337
  function maybeSessionExposed(sessionId) {
15031
15338
  if (!sessionId) return false;
15032
15339
  try {
15033
- const st = import_fs16.default.statSync(file(sessionId));
15340
+ const st = import_fs17.default.statSync(file(sessionId));
15034
15341
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
15035
15342
  } catch (e) {
15036
15343
  return e.code !== "ENOENT";
@@ -15038,20 +15345,20 @@ function maybeSessionExposed(sessionId) {
15038
15345
  }
15039
15346
 
15040
15347
  // src/exposure/roles.ts
15041
- var import_fs18 = __toESM(require("fs"));
15348
+ var import_fs19 = __toESM(require("fs"));
15042
15349
 
15043
15350
  // src/daemon/mcp-tools.ts
15044
- var import_fs17 = __toESM(require("fs"));
15045
- var import_path19 = __toESM(require("path"));
15046
- var import_os13 = __toESM(require("os"));
15351
+ var import_fs18 = __toESM(require("fs"));
15352
+ var import_path20 = __toESM(require("path"));
15353
+ var import_os14 = __toESM(require("os"));
15047
15354
  function getMcpToolsFile() {
15048
- return import_path19.default.join(import_os13.default.homedir(), ".node9", "mcp-tools.json");
15355
+ return import_path20.default.join(import_os14.default.homedir(), ".node9", "mcp-tools.json");
15049
15356
  }
15050
15357
  function readMcpToolsConfig() {
15051
15358
  try {
15052
15359
  const file2 = getMcpToolsFile();
15053
- if (!import_fs17.default.existsSync(file2)) return {};
15054
- const raw = import_fs17.default.readFileSync(file2, "utf-8");
15360
+ if (!import_fs18.default.existsSync(file2)) return {};
15361
+ const raw = import_fs18.default.readFileSync(file2, "utf-8");
15055
15362
  return JSON.parse(raw);
15056
15363
  } catch {
15057
15364
  return {};
@@ -15068,7 +15375,7 @@ var inventoryCache = null;
15068
15375
  var inventoryStamp = null;
15069
15376
  function inventoryFileStamp() {
15070
15377
  try {
15071
- const st = import_fs18.default.statSync(getMcpToolsFile());
15378
+ const st = import_fs19.default.statSync(getMcpToolsFile());
15072
15379
  return `${st.mtimeMs}:${st.size}`;
15073
15380
  } catch {
15074
15381
  return "";
@@ -15147,68 +15454,6 @@ function sourceWord(toolName) {
15147
15454
  return "tool result";
15148
15455
  }
15149
15456
 
15150
- // src/auth/trusted-recipients.ts
15151
- var import_fs19 = __toESM(require("fs"));
15152
- var import_path20 = __toESM(require("path"));
15153
- var import_os14 = __toESM(require("os"));
15154
- init_dist();
15155
- function getTrustedRecipientsPath() {
15156
- return import_path20.default.join(import_os14.default.homedir(), ".node9", "trusted-recipients.json");
15157
- }
15158
- function readTrustedRecipients() {
15159
- try {
15160
- const raw = import_fs19.default.readFileSync(getTrustedRecipientsPath(), "utf8");
15161
- const parsed = JSON.parse(raw);
15162
- return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
15163
- } catch {
15164
- return [];
15165
- }
15166
- }
15167
- function writeTrustedRecipients(entries) {
15168
- const filePath = getTrustedRecipientsPath();
15169
- import_fs19.default.mkdirSync(import_path20.default.dirname(filePath), { recursive: true });
15170
- const tmp = filePath + ".node9-tmp";
15171
- import_fs19.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
15172
- import_fs19.default.renameSync(tmp, filePath);
15173
- }
15174
- function normalizeRecipient(raw) {
15175
- const v = raw.trim();
15176
- if (!v) return null;
15177
- if (/^(email|host|iban):/i.test(v)) {
15178
- const i = v.indexOf(":");
15179
- const kind = v.slice(0, i).toLowerCase();
15180
- const inner = normalizeRecipient(v.slice(i + 1));
15181
- return inner && inner.startsWith(`${kind}:`) ? inner : null;
15182
- }
15183
- const found = extractArgDestinations({ to: v });
15184
- return found.length === 1 ? found[0] : null;
15185
- }
15186
- function addTrustedRecipient(raw, addedBy = "user") {
15187
- const value = normalizeRecipient(raw);
15188
- if (!value) return null;
15189
- const entries = readTrustedRecipients();
15190
- if (!entries.some((e) => e.value === value)) {
15191
- entries.push({ value, addedAt: Date.now(), addedBy });
15192
- writeTrustedRecipients(entries);
15193
- }
15194
- return value;
15195
- }
15196
- var cache3 = null;
15197
- function mtimeOf() {
15198
- try {
15199
- return import_fs19.default.statSync(getTrustedRecipientsPath()).mtimeMs;
15200
- } catch {
15201
- return 0;
15202
- }
15203
- }
15204
- function isTrustedRecipient(value) {
15205
- const mtime = mtimeOf();
15206
- if (!cache3 || cache3.mtime !== mtime) {
15207
- cache3 = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
15208
- }
15209
- return cache3.values.has(value);
15210
- }
15211
-
15212
15457
  // src/auth/cloud.ts
15213
15458
  var import_fs21 = __toESM(require("fs"));
15214
15459
  var import_os15 = __toESM(require("os"));
@@ -15303,11 +15548,15 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
15303
15548
  cwd: process.cwd(),
15304
15549
  platform: import_os15.default.platform()
15305
15550
  },
15306
- // Local-only fields (fw#318 exposureDestinations) stay off the wire:
15307
- // the SaaS validates riskMetadata strictly and would drop it whole.
15551
+ // The SaaS validates riskMetadata strictly and would drop it whole on
15552
+ // an unknown key, so only fields its schema declares go on the wire.
15553
+ // exposureDestinations ships since the dashboard's 2b contract
15554
+ // (fw#318): "Approve and trust" needs them.
15308
15555
  ...riskMetadata && {
15309
15556
  riskMetadata: Object.fromEntries(
15310
- Object.entries(riskMetadata).filter(([, v]) => !Array.isArray(v))
15557
+ Object.entries(riskMetadata).filter(
15558
+ ([k, v]) => !Array.isArray(v) || k === "exposureDestinations"
15559
+ )
15311
15560
  )
15312
15561
  },
15313
15562
  ...ciContext && { ciContext },
@@ -16030,7 +16279,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16030
16279
  let fileAccess;
16031
16280
  let fileFinding;
16032
16281
  try {
16033
- fileAccess = prepareFileAccesses(toolName, args, options?.cwd, config.policy.toolInspection);
16282
+ fileAccess = prepareFileAccesses(toolName, args, options?.cwd, inspectionLayers(config.policy));
16034
16283
  fileFinding = isManual ? null : evaluateProtectedFileAccess(
16035
16284
  fileAccess.accesses,
16036
16285
  fileAccess,
@@ -16049,7 +16298,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16049
16298
  fileAccess = { cwd: options?.cwd, homeDirs: [], accesses: [], handledPaths: /* @__PURE__ */ new Set() };
16050
16299
  fileFinding = isManual ? null : incompleteFileVerdict();
16051
16300
  }
16052
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
16301
+ const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
16053
16302
  const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
16054
16303
  const shellReview = !isManual && shellInput.kind === "uninspectable";
16055
16304
  if (shellReview) {
@@ -16159,6 +16408,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16159
16408
  let exposureReview = null;
16160
16409
  let exposureKeys = [];
16161
16410
  let exposureDestinations = [];
16411
+ let exposureFromEid;
16162
16412
  const ucPolicy = config.policy.untrustedContent;
16163
16413
  if (ucPolicy?.enabled && !taintWarning && meta?.sessionId && !isManual) {
16164
16414
  const role = toolRole(toolName, ucPolicy);
@@ -16166,7 +16416,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16166
16416
  const shellExp = shellInput.kind === "shell" ? classifyShellExposure(shellInput.command) : null;
16167
16417
  const bareName = splitMcpToolName(toolName)?.bare;
16168
16418
  const mcpFileWrite = bareName !== void 0 && fileToolOperation(bareName, args) === "write";
16169
- const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, config.policy.toolInspection).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
16419
+ const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, inspectionLayers(config.policy)).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
16170
16420
  const sendsSomething = acts && !mcpFileWrite || !!shellExp?.sends;
16171
16421
  const mayBeExposed = daemonSaysExposed ?? (maybeSessionExposed(meta.sessionId) || writes.length > 0 && judgeWritesAfterExposure({
16172
16422
  accesses: writes,
@@ -16210,7 +16460,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16210
16460
  const isKnown = (d) => {
16211
16461
  const domain = destinationDomain(d);
16212
16462
  const value = destinationValue(d);
16213
- 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));
16463
+ 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));
16214
16464
  };
16215
16465
  const approvedOnce = (key) => ucPolicy.afterExposure === "ask-once" && approved.has(key);
16216
16466
  const repos = (shellExp?.sends ? shellExp.repos : []).filter(
@@ -16274,6 +16524,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16274
16524
  );
16275
16525
  }
16276
16526
  exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
16527
+ exposureFromEid = last?.eid;
16528
+ if (exposureFromEid) meta = { ...meta, exposureFromEid };
16277
16529
  exposureKeys = [
16278
16530
  ...held.filter((h) => h.dest || h.why === "unresolved" || h.why === "repository").map(
16279
16531
  (h) => h.why === "repository" ? `${toolName}|repo:${h.repo}` : `${toolName}|${h.dest || "unresolved"}`
@@ -16287,6 +16539,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16287
16539
  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("; ") + ".";
16288
16540
  }
16289
16541
  } else if (held.length > 0 || writeHolds.length > 0 || logged.length > 0) {
16542
+ meta = {
16543
+ ...meta,
16544
+ exposure: { kind: "logged", destinations: logged.length },
16545
+ ...last?.eid ? { exposureFromEid: last.eid } : {}
16546
+ };
16290
16547
  appendToLog(HOOK_DEBUG_LOG, {
16291
16548
  ts: (/* @__PURE__ */ new Date()).toISOString(),
16292
16549
  event: "exposure-logged",
@@ -16840,6 +17097,7 @@ ${packageReview}`;
16840
17097
  // deferReview and serverKey never co-occur in production — see above.)
16841
17098
  reason: taintWarning || packageReview || exposureReview || explainableLabel || "Node9 flagged this action for review.",
16842
17099
  ...exposureReview && exposureKeys.length > 0 ? { exposureKeys } : {},
17100
+ ...exposureReview && exposureFromEid ? { exposureFromEid } : {},
16843
17101
  ruleDescription: policyRuleDescription,
16844
17102
  blockedByLabel: explainableLabel
16845
17103
  };