@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.mjs CHANGED
@@ -2147,32 +2147,109 @@ function evaluateSmartConditions(args, rule) {
2147
2147
  function isBashTool(tool) {
2148
2148
  return BASH_TOOL_NAMES.has(tool.toLowerCase());
2149
2149
  }
2150
- function inspectionEntry(tool, inspection = {}) {
2150
+ function inspectionLayers(policy) {
2151
+ return { trusted: policy.toolInspection, project: policy.projectToolInspection };
2152
+ }
2153
+ function isShellEntry(value) {
2154
+ return typeof value === "object" && value !== null && value.kind === "shell" && typeof value.field === "string" && Object.keys(value).length === 2;
2155
+ }
2156
+ function isInspectionLayers(input) {
2157
+ const trusted = input.trusted;
2158
+ return typeof trusted === "object" && trusted !== null && !isShellEntry(trusted);
2159
+ }
2160
+ function layersOf(input) {
2161
+ return isInspectionLayers(input) ? { trusted: input.trusted, project: input.project ?? {} } : { trusted: input, project: {} };
2162
+ }
2163
+ function entryOf(tool, inspection) {
2151
2164
  const pattern = Object.keys(inspection).find((p) => matchesPattern(tool, p));
2152
2165
  return pattern === void 0 ? void 0 : inspection[pattern];
2153
2166
  }
2167
+ function inspectionEntry(tool, inspection = {}) {
2168
+ const { trusted, project } = layersOf(inspection);
2169
+ return entryOf(tool, trusted) ?? entryOf(tool, project);
2170
+ }
2171
+ function executorName(tool) {
2172
+ const name = tool.toLowerCase();
2173
+ if (!name.startsWith("mcp__")) return name;
2174
+ const at = name.lastIndexOf("__");
2175
+ return at >= "mcp__".length ? name.slice(at + 2) : name;
2176
+ }
2177
+ function entryFields(entry) {
2178
+ if (typeof entry === "object") return [entry.field];
2179
+ return entry === "command" ? ["command"] : null;
2180
+ }
2154
2181
  function shellFields(tool, inspection) {
2155
2182
  if (isBashTool(tool)) return ["command"];
2156
- const entry = inspectionEntry(tool, inspection);
2157
- if (typeof entry === "object") return [entry.field];
2158
- if (entry !== void 0) return entry === "command" ? [entry] : null;
2159
- const name = tool.toLowerCase().replace(/^mcp__[a-z0-9_-]+?__/, "");
2160
- return MCP_EXECUTORS.has(name) ? ["command", "cmd", "script"] : null;
2183
+ const { trusted, project } = layersOf(inspection);
2184
+ const trustedEntry = entryOf(tool, trusted);
2185
+ if (trustedEntry !== void 0) return entryFields(trustedEntry);
2186
+ const builtin = MCP_EXECUTORS.has(executorName(tool)) ? BUILTIN_FIELDS : null;
2187
+ const projectEntry = entryOf(tool, project);
2188
+ if (projectEntry === void 0) return builtin;
2189
+ const added = entryFields(projectEntry);
2190
+ if (!builtin) return added;
2191
+ if (!added) return builtin;
2192
+ return [.../* @__PURE__ */ new Set([...builtin, ...added])];
2161
2193
  }
2162
2194
  function isShellShapedTool(tool, inspection = {}) {
2163
2195
  return shellFields(tool, inspection) !== null;
2164
2196
  }
2197
+ function shellWord(word) {
2198
+ if (BARE_WORD.test(word)) return word;
2199
+ return word === "" ? "''" : `'${word.replace(/'/g, `'\\''`)}'`;
2200
+ }
2201
+ function isStringArray(value) {
2202
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
2203
+ }
2204
+ function commandLine(args, fieldPath, value, native) {
2205
+ const missing = { reason: "Shell command field is missing or is not a string" };
2206
+ const unreadable = { reason: "Shell command arguments could not be read" };
2207
+ if (typeof value !== "string" && (native || !Array.isArray(value))) return missing;
2208
+ const dot = fieldPath.lastIndexOf(".");
2209
+ const parent = dot === -1 ? args : getNestedValue(args, fieldPath.slice(0, dot));
2210
+ const siblings = ARGV_FIELDS.map((key) => ({
2211
+ key,
2212
+ value: parent && typeof parent === "object" ? parent[key] : void 0
2213
+ })).filter(({ value: value2 }) => value2 !== void 0 && value2 !== null);
2214
+ let argv = siblings[0];
2215
+ if (native) argv = siblings.find(({ value: v }) => isStringArray(v));
2216
+ else if (siblings.length === 2 && JSON.stringify(siblings[0].value) !== JSON.stringify(siblings[1].value))
2217
+ return { reason: "Shell command arguments disagree" };
2218
+ let command;
2219
+ let source = fieldPath;
2220
+ if (Array.isArray(value)) {
2221
+ if (argv || !value.length || !isStringArray(value)) return unreadable;
2222
+ command = value.map(shellWord).join(" ");
2223
+ } else if (!argv) {
2224
+ return { command: value, fieldPath };
2225
+ } else {
2226
+ if (!isStringArray(argv.value)) return unreadable;
2227
+ if (!argv.value.length) return { command: value, fieldPath };
2228
+ command = `${value} ${argv.value.map(shellWord).join(" ")}`;
2229
+ source = `${fieldPath}+${argv.key}`;
2230
+ }
2231
+ if (command.length > MAX_ARGV_CHARACTERS)
2232
+ return { reason: "Shell command arguments exceed the inspectable size" };
2233
+ return { command, fieldPath: source };
2234
+ }
2165
2235
  function resolveShellInput(tool, args, inspection = {}) {
2166
2236
  const fields = shellFields(tool, inspection);
2167
2237
  if (!fields) return { kind: "not-shell" };
2168
2238
  const values = fields.map((fieldPath) => ({ fieldPath, value: getNestedValue(args, fieldPath) })).filter(({ value }) => value !== void 0 && value !== null && value !== "");
2169
- if (!values.length || values.some(({ value }) => typeof value !== "string")) {
2239
+ if (!values.length) {
2170
2240
  return { kind: "uninspectable", reason: "Shell command field is missing or is not a string" };
2171
2241
  }
2172
- if (new Set(values.map(({ value }) => value)).size !== 1) {
2242
+ const native = isBashTool(tool);
2243
+ const lines = values.map(({ fieldPath, value }) => commandLine(args, fieldPath, value, native));
2244
+ const failed = lines.find((line) => "reason" in line);
2245
+ if (failed) return { kind: "uninspectable", reason: failed.reason };
2246
+ const commands = lines.filter(
2247
+ (line) => "command" in line
2248
+ );
2249
+ if (new Set(commands.map(({ command }) => command)).size !== 1) {
2173
2250
  return { kind: "uninspectable", reason: "Multiple shell command fields disagree" };
2174
2251
  }
2175
- return { kind: "shell", command: values[0].value, fieldPath: values[0].fieldPath };
2252
+ return { kind: "shell", ...commands[0] };
2176
2253
  }
2177
2254
  function shellRuleArgs(tool, args, ruleTool, inspection = {}) {
2178
2255
  if (!ruleTool || !matchesPattern("bash", ruleTool) || matchesPattern(tool, ruleTool)) return args;
@@ -2913,20 +2990,25 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2913
2990
  }
2914
2991
  return best;
2915
2992
  }
2916
- function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ new Set() }) {
2917
- const words = [...text.matchAll(/[a-zA-Z]+/g)];
2993
+ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new Set() }) {
2918
2994
  let run = [];
2995
+ let runIndex = [];
2919
2996
  let previousEnd = 0;
2920
2997
  const rejected = /* @__PURE__ */ new Set();
2921
2998
  const inspect = () => {
2999
+ if (run.length < 12) return false;
3000
+ const ascending = new Array(run.length).fill(0);
3001
+ for (let i = 1; i < run.length; i++)
3002
+ ascending[i] = ascending[i - 1] + (runIndex[i] > runIndex[i - 1] ? 1 : 0);
2922
3003
  for (let start = 0; start + 12 <= run.length; start++) {
2923
3004
  for (const count of lengths) {
2924
3005
  if (start + count > run.length) continue;
3006
+ if (ascending[start + count - 1] - ascending[start] === count - 1) continue;
2925
3007
  const candidate = run.slice(start, start + count);
2926
3008
  const sentence = candidate.join(" ");
2927
3009
  if (rejected.has(sentence)) continue;
2928
- if (++budget.candidates > 1024) {
2929
- budget.reasons.add("mnemonic-candidates");
3010
+ if (++budget.hashes > MAX_MNEMONIC_HASHES) {
3011
+ budget.reasons.add("mnemonic-budget");
2930
3012
  return false;
2931
3013
  }
2932
3014
  if (BIP39_PUBLIC_VECTORS.has(hash(sentence).toString("hex"))) {
@@ -2944,13 +3026,18 @@ function findMnemonic(text, budget = { candidates: 0, reasons: /* @__PURE__ */ n
2944
3026
  }
2945
3027
  return false;
2946
3028
  };
2947
- for (const word of words) {
2948
- if (!indices.has(word[0]) || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
3029
+ for (const word of text.matchAll(/[a-zA-Z]+/g)) {
3030
+ const index = indices.get(word[0]);
3031
+ if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
2949
3032
  if (inspect()) return true;
2950
- if (budget.reasons.has("mnemonic-candidates")) return false;
3033
+ if (budget.reasons.has("mnemonic-budget")) return false;
2951
3034
  run = [];
3035
+ runIndex = [];
3036
+ }
3037
+ if (index !== void 0) {
3038
+ run.push(word[0]);
3039
+ runIndex.push(index);
2952
3040
  }
2953
- if (indices.has(word[0])) run.push(word[0]);
2954
3041
  previousEnd = word.index + word[0].length;
2955
3042
  }
2956
3043
  return inspect();
@@ -3081,6 +3168,9 @@ function validateCask(raw) {
3081
3168
  const second = b64urlIndex(s[tsAt + 5]);
3082
3169
  return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
3083
3170
  }
3171
+ function isInvisibleChar(char) {
3172
+ return INVISIBLE_ONE.test(char);
3173
+ }
3084
3174
  function stripInvisible(text) {
3085
3175
  return text.replace(INVISIBLE_RE, "");
3086
3176
  }
@@ -3267,6 +3357,45 @@ function matchArgumentText(text, fieldPath, budget) {
3267
3357
  }
3268
3358
  return best;
3269
3359
  }
3360
+ function matchArgumentBatch(fragments, fieldPath, budget) {
3361
+ if (!fragments.length) return null;
3362
+ const text = fragments.join(BATCH_SEPARATOR);
3363
+ const lower2 = text.toLowerCase();
3364
+ let best = null;
3365
+ for (const pattern of DLP_PATTERNS) {
3366
+ if (pattern.keywords && !pattern.keywords.some((kw) => lower2.includes(kw.toLowerCase())))
3367
+ continue;
3368
+ const flags = pattern.regex.flags.replace(/[gmy]/g, "");
3369
+ if (!new RegExp(pattern.regex.source, flags + "m").test(text)) continue;
3370
+ for (const fragment of fragments) {
3371
+ if (firstAcceptedMatch(pattern, fragment) === null) continue;
3372
+ const severity = pattern.contextBoost && isAssignmentContext(fragment) ? "block" : pattern.severity;
3373
+ if (severity === "block")
3374
+ return {
3375
+ patternName: pattern.name,
3376
+ fieldPath,
3377
+ redactedSample: "[encoded secret]",
3378
+ severity
3379
+ };
3380
+ best ??= {
3381
+ patternName: pattern.name,
3382
+ fieldPath,
3383
+ redactedSample: "[encoded secret]",
3384
+ severity
3385
+ };
3386
+ if (!pattern.contextBoost) break;
3387
+ }
3388
+ }
3389
+ if (findMnemonic(text, budget)) {
3390
+ return {
3391
+ patternName: "BIP-39 Recovery Phrase",
3392
+ fieldPath,
3393
+ redactedSample: "[recovery phrase]",
3394
+ severity: "block"
3395
+ };
3396
+ }
3397
+ return best;
3398
+ }
3270
3399
  function scanArgs(args, depth = 0, fieldPath = "args") {
3271
3400
  if (depth > MAX_DEPTH2 || args === null || args === void 0) return null;
3272
3401
  if (Array.isArray(args)) {
@@ -4138,6 +4267,9 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
4138
4267
  return null;
4139
4268
  }
4140
4269
  }
4270
+ function newSecretViewBudget() {
4271
+ return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
4272
+ }
4141
4273
  function textBytes(buf) {
4142
4274
  if (buf.length < 16) return null;
4143
4275
  const text = buf.toString("utf8");
@@ -4156,13 +4288,41 @@ function percentOnce(text) {
4156
4288
  }
4157
4289
  });
4158
4290
  }
4291
+ function* wrappedBase64(text) {
4292
+ if (!text.includes("\n")) return;
4293
+ const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
4294
+ let run = [];
4295
+ let width = 0;
4296
+ const flush = function* () {
4297
+ if (run.length >= 2) yield run.join("");
4298
+ run = [];
4299
+ width = 0;
4300
+ };
4301
+ for (const raw of text.split("\n")) {
4302
+ const line = raw.trim();
4303
+ const full = line.length >= 16 && word.test(line) && !line.includes("=");
4304
+ if (full && (!run.length || line.length === width)) {
4305
+ run.push(line);
4306
+ width = line.length;
4307
+ continue;
4308
+ }
4309
+ if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
4310
+ run.push(line);
4311
+ yield* flush();
4312
+ continue;
4313
+ }
4314
+ yield* flush();
4315
+ if (full) {
4316
+ run.push(line);
4317
+ width = line.length;
4318
+ }
4319
+ }
4320
+ yield* flush();
4321
+ }
4159
4322
  function* encodedRegions(text, budget) {
4160
4323
  yield [text, false];
4324
+ for (const block of wrappedBase64(text)) yield [block, false];
4161
4325
  for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
4162
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4163
- budget.reasons.add("decode-candidates");
4164
- return;
4165
- }
4166
4326
  try {
4167
4327
  new URL(match[0]);
4168
4328
  } catch {
@@ -4182,6 +4342,8 @@ function* encodedRegions(text, budget) {
4182
4342
  ];
4183
4343
  for (const region of regions) if (region) yield [region, false];
4184
4344
  const labels = host2.split(".");
4345
+ 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))
4346
+ continue;
4185
4347
  for (let start = 0; start < labels.length; start++) {
4186
4348
  let joined = labels[start];
4187
4349
  for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
@@ -4189,11 +4351,13 @@ function* encodedRegions(text, budget) {
4189
4351
  if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
4190
4352
  break;
4191
4353
  joined += labels[end];
4192
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4193
- budget.reasons.add("decode-candidates");
4354
+ if (joined.length < 16) continue;
4355
+ budget.joinedLabelBytes += joined.length;
4356
+ if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
4357
+ budget.reasons.add("decode-labels");
4194
4358
  return;
4195
4359
  }
4196
- if (joined.length >= 16 && joined.length <= 253) yield [joined, true];
4360
+ yield [joined, true];
4197
4361
  }
4198
4362
  }
4199
4363
  }
@@ -4230,10 +4394,6 @@ function* decodedCandidates(text, budget) {
4230
4394
  if (joined) yield ["dns-labels", region];
4231
4395
  for (const [kind, re] of families) {
4232
4396
  for (const match of region.matchAll(re)) {
4233
- if (++budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) {
4234
- budget.reasons.add("decode-candidates");
4235
- return;
4236
- }
4237
4397
  const raw = match[0];
4238
4398
  let buf;
4239
4399
  if (kind === "base64") {
@@ -4256,42 +4416,76 @@ function* decodedCandidates(text, budget) {
4256
4416
  }
4257
4417
  }
4258
4418
  }
4259
- function* secretViews(text, budget) {
4419
+ function secretViews(text, budget) {
4260
4420
  const seen = /* @__PURE__ */ new Set([text]);
4261
- const queue = [{ text, transforms: [] }];
4262
- for (let i = 0; i < queue.length; i++) {
4263
- const view = queue[i];
4264
- yield view;
4265
- for (const [kind, decoded] of decodedCandidates(view.text, budget)) {
4266
- if (seen.has(decoded)) continue;
4267
- if (view.transforms.length >= SECRET_VIEW_LIMITS.maxDepth) {
4268
- budget.reasons.add("decode-depth");
4269
- continue;
4270
- }
4271
- const bytes = Buffer.byteLength(decoded);
4272
- if (queue.length >= SECRET_VIEW_LIMITS.maxViewsPerLeaf || budget.views >= SECRET_VIEW_LIMITS.maxViews || budget.bytes + bytes > SECRET_VIEW_LIMITS.maxDerivedBytes) {
4273
- budget.reasons.add("decode-budget");
4274
- return;
4421
+ const out = [];
4422
+ let frontier = [{ text, transforms: [] }];
4423
+ for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
4424
+ const next = [];
4425
+ for (const parent of frontier) {
4426
+ let perLeaf = 0;
4427
+ for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
4428
+ if (seen.has(decoded)) continue;
4429
+ seen.add(decoded);
4430
+ const normalizing = kind === "invisibles" || kind === "percent";
4431
+ if (depth >= 2 && !normalizing) {
4432
+ budget.nestedBytes += Buffer.byteLength(decoded);
4433
+ if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
4434
+ budget.reasons.add("decode-nested");
4435
+ return out;
4436
+ }
4437
+ }
4438
+ const view = { text: decoded, transforms: [...parent.transforms, kind] };
4439
+ out.push(view);
4440
+ next.push(view);
4275
4441
  }
4276
- seen.add(decoded);
4277
- budget.views++;
4278
- budget.bytes += bytes;
4279
- queue.push({ text: decoded, transforms: [...view.transforms, kind] });
4280
4442
  }
4281
- if (budget.candidates > SECRET_VIEW_LIMITS.maxCandidates) return;
4443
+ frontier = next;
4444
+ if (!frontier.length) return out;
4445
+ }
4446
+ for (const view of frontier) {
4447
+ if (!decodedCandidates(view.text, budget).next().done) {
4448
+ budget.reasons.add("decode-nested");
4449
+ break;
4450
+ }
4282
4451
  }
4452
+ return out;
4453
+ }
4454
+ function batches(views) {
4455
+ const out = [];
4456
+ let current = [];
4457
+ let size = 0;
4458
+ for (const { text } of views) {
4459
+ if (current.length && size + text.length > SECRET_VIEW_LIMITS.batchCharacters) {
4460
+ out.push(current);
4461
+ current = [];
4462
+ size = 0;
4463
+ }
4464
+ current.push(text);
4465
+ size += text.length;
4466
+ }
4467
+ if (current.length) out.push(current);
4468
+ return out;
4283
4469
  }
4284
4470
  function inspectArgumentSecrets(args) {
4285
- const budget = { views: 0, bytes: 0, candidates: 0, reasons: /* @__PURE__ */ new Set() };
4471
+ const budget = newSecretViewBudget();
4286
4472
  const ancestors = /* @__PURE__ */ new Set();
4287
4473
  let nodes = 0;
4288
4474
  let sourceBytes = 0;
4289
4475
  let tailBytes = 0;
4290
4476
  let best = null;
4477
+ const blocked = () => best?.severity === "block";
4291
4478
  const accept = (match) => {
4292
4479
  if (match && (!best || best.severity !== "block" && match.severity === "block")) best = match;
4293
4480
  };
4481
+ const decodeSegment = (segment, fieldPath) => {
4482
+ for (const batch of batches(secretViews(segment, budget))) {
4483
+ accept(matchArgumentBatch(batch, fieldPath, budget));
4484
+ if (blocked()) return;
4485
+ }
4486
+ };
4294
4487
  const visit = (value, depth, fieldPath) => {
4488
+ if (blocked()) return;
4295
4489
  if (++nodes > ARGUMENT_SCAN_LIMITS.maxNodes) {
4296
4490
  budget.reasons.add("argument-nodes");
4297
4491
  return;
@@ -4333,14 +4527,11 @@ function inspectArgumentSecrets(args) {
4333
4527
  }
4334
4528
  sourceBytes += Math.min(bytes, Math.max(0, remaining));
4335
4529
  if (!text) return;
4336
- for (const view of secretViews(text, budget)) {
4337
- const hit = matchArgumentText(view.text, fieldPath, budget);
4338
- if (hit)
4339
- accept(view.transforms.length ? { ...hit, redactedSample: "[encoded secret]" } : hit);
4340
- }
4530
+ accept(matchArgumentText(text, fieldPath, budget));
4531
+ let scannedEnd = text.length;
4341
4532
  if (!truncated && value.length > window) {
4342
- budget.reasons.add("decode-window");
4343
4533
  for (let start = window - overlap; start < value.length; start += window - overlap) {
4534
+ if (blocked()) return;
4344
4535
  const chunk = value.slice(start, start + window);
4345
4536
  const chunkBytes = Buffer.byteLength(chunk);
4346
4537
  if (tailBytes + chunkBytes > ARGUMENT_SCAN_LIMITS.maxTailBytes) {
@@ -4349,9 +4540,16 @@ function inspectArgumentSecrets(args) {
4349
4540
  }
4350
4541
  tailBytes += chunkBytes;
4351
4542
  accept(matchArgumentText(chunk, fieldPath, budget));
4543
+ scannedEnd = start + chunk.length;
4352
4544
  if (start + window >= value.length) break;
4353
4545
  }
4354
4546
  }
4547
+ for (let start = 0; start < scannedEnd && !blocked(); ) {
4548
+ let end = Math.min(start + window, scannedEnd);
4549
+ while (end < scannedEnd && runChar(value, end - 1) && runChar(value, end)) end++;
4550
+ decodeSegment(value.slice(start, end), fieldPath);
4551
+ start = end;
4552
+ }
4355
4553
  const trimmed = text.trim();
4356
4554
  if (value.length < ARGUMENT_SCAN_LIMITS.maxJsonCharacters && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
4357
4555
  try {
@@ -4370,7 +4568,7 @@ function inspectArgumentSecrets(args) {
4370
4568
  function scanArgumentSecrets(args) {
4371
4569
  const result = inspectArgumentSecrets(args);
4372
4570
  if (result.match) return result.match;
4373
- return result.reasons.some((reason) => reason.startsWith("argument-")) ? {
4571
+ return result.coverage === "incomplete" ? {
4374
4572
  patternName: "Argument inspection incomplete",
4375
4573
  fieldPath: "args",
4376
4574
  redactedSample: "[uninspected content]",
@@ -4398,18 +4596,13 @@ function tokenize2(toolName) {
4398
4596
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
4399
4597
  }
4400
4598
  function extractShellCommand(toolName, args, toolInspection) {
4401
- const patterns = Object.keys(toolInspection);
4402
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4403
- if (!matchingPattern) return null;
4404
- const fieldPath = toolInspection[matchingPattern];
4599
+ const fieldPath = inspectionEntry(toolName, toolInspection);
4600
+ if (fieldPath === void 0) return null;
4405
4601
  const value = getNestedValue(args, typeof fieldPath === "string" ? fieldPath : fieldPath.field);
4406
4602
  return typeof value === "string" ? value : null;
4407
4603
  }
4408
4604
  function isSqlTool(toolName, toolInspection) {
4409
- const patterns = Object.keys(toolInspection);
4410
- const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
4411
- if (!matchingPattern) return false;
4412
- const fieldName = toolInspection[matchingPattern];
4605
+ const fieldName = inspectionEntry(toolName, toolInspection);
4413
4606
  return fieldName === "sql" || fieldName === "query";
4414
4607
  }
4415
4608
  function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
@@ -4476,9 +4669,10 @@ function egressPolicyVerdict(eg) {
4476
4669
  }
4477
4670
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
4478
4671
  let protectedVerdict = null;
4672
+ const layers = inspectionLayers(config.policy);
4479
4673
  if (context.agent !== "Terminal") {
4480
4674
  try {
4481
- const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, config.policy.toolInspection);
4675
+ const accesses = context.accesses ?? collectFileAccesses(toolName, args, context, layers);
4482
4676
  protectedVerdict = evaluateProtectedFileAccess(accesses, context, catalogSettings(config));
4483
4677
  } catch {
4484
4678
  protectedVerdict = incompleteFileVerdict();
@@ -4513,7 +4707,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4513
4707
  hooks
4514
4708
  );
4515
4709
  if (result.decision === "block") return result;
4516
- if (context.agent !== "Terminal" && resolveShellInput(toolName, args, config.policy.toolInspection).kind === "uninspectable") {
4710
+ if (context.agent !== "Terminal" && resolveShellInput(toolName, args, layers).kind === "uninspectable") {
4517
4711
  return {
4518
4712
  decision: "review",
4519
4713
  ruleName: "shell-inspection-incomplete",
@@ -4529,13 +4723,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4529
4723
  const { agent, cwd, activeEnvironment } = context;
4530
4724
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
4531
4725
  const settingsView = catalogSettings(config);
4726
+ const layers = inspectionLayers(config.policy);
4532
4727
  const cv = (id) => checkValue(settingsView, id);
4533
4728
  let loggedFinding;
4534
4729
  const noteLogged = (v) => {
4535
4730
  if (v?.logged && !loggedFinding) loggedFinding = v;
4536
4731
  };
4537
4732
  const allowed = () => loggedFinding ?? { decision: "allow" };
4538
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
4733
+ const shellInput = resolveShellInput(toolName, args, layers);
4539
4734
  const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
4540
4735
  {
4541
4736
  const dest = ssrfDestinationFloor(toolName, args, {
@@ -4640,13 +4835,10 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4640
4835
  return { id, value: cv(id) };
4641
4836
  };
4642
4837
  const matches = config.policy.smartRules.filter((rule) => {
4643
- if (!toolMatchesRule(toolName, rule.tool, config.policy.toolInspection)) return false;
4838
+ if (!toolMatchesRule(toolName, rule.tool, layers)) return false;
4644
4839
  if (astSuppressed(rule)) return false;
4645
4840
  if (rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") return false;
4646
- if (!evaluateSmartConditions(
4647
- shellRuleArgs(toolName, args, rule.tool, config.policy.toolInspection),
4648
- rule
4649
- ))
4841
+ if (!evaluateSmartConditions(shellRuleArgs(toolName, args, rule.tool, layers), rule))
4650
4842
  return false;
4651
4843
  const check = ruleCheck(rule);
4652
4844
  if (check?.value === "off") return false;
@@ -4693,7 +4885,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4693
4885
  let allTokens = [];
4694
4886
  let pathTokens = [];
4695
4887
  if (pendingToolEgress) return pendingToolEgress;
4696
- const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, config.policy.toolInspection) : null;
4888
+ const shellCommand = shellInput.kind === "shell" ? shellInput.command : shellInput.kind === "not-shell" ? extractShellCommand(toolName, args, layers) : null;
4697
4889
  if (shellCommand) {
4698
4890
  const analyzed = analyzeShellCommand(shellCommand);
4699
4891
  allTokens = analyzed.allTokens;
@@ -4770,7 +4962,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4770
4962
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
4771
4963
  const rawTokens = shellCommand.trim().split(/\s+/);
4772
4964
  const sshHosts = extractAllSshHosts(rawTokens.slice(1));
4773
- allTokens.push(...sshHosts);
4965
+ allTokens = allTokens.concat(sshHosts);
4774
4966
  }
4775
4967
  if (firstToken && firstToken.startsWith("/") && checkProvenance2) {
4776
4968
  const prov = checkProvenance2(firstToken, cwd);
@@ -4799,7 +4991,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4799
4991
  };
4800
4992
  }
4801
4993
  }
4802
- if (isSqlTool(toolName, config.policy.toolInspection)) {
4994
+ if (isSqlTool(toolName, layers)) {
4803
4995
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
4804
4996
  }
4805
4997
  } else {
@@ -4807,7 +4999,7 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
4807
4999
  if (args && typeof args === "object") {
4808
5000
  const flattenedArgs = JSON.stringify(args).toLowerCase();
4809
5001
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
4810
- allTokens.push(...extraTokens);
5002
+ allTokens = allTokens.concat(extraTokens);
4811
5003
  }
4812
5004
  }
4813
5005
  const isManual = agent === "Terminal";
@@ -5576,7 +5768,7 @@ function matchCanaryArgs(args, values) {
5576
5768
  return null;
5577
5769
  }
5578
5770
  }
5579
- var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, 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;
5771
+ var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, 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;
5580
5772
  var init_dist = __esm({
5581
5773
  "packages/policy-engine/dist/index.mjs"() {
5582
5774
  "use strict";
@@ -7941,7 +8133,23 @@ var init_dist = __esm({
7941
8133
  "shell",
7942
8134
  "exec_command"
7943
8135
  ]);
7944
- MCP_EXECUTORS = /* @__PURE__ */ new Set([...BASH_TOOL_NAMES, "exec", "run_command", "terminal"]);
8136
+ MCP_EXECUTORS = /* @__PURE__ */ new Set([
8137
+ ...BASH_TOOL_NAMES,
8138
+ "exec",
8139
+ "run_command",
8140
+ "terminal",
8141
+ "execute_command",
8142
+ "execute_shell",
8143
+ "execute_shell_command",
8144
+ "shell_execute",
8145
+ "run_shell",
8146
+ "run_terminal_command",
8147
+ "start_process"
8148
+ ]);
8149
+ BUILTIN_FIELDS = ["command", "cmd", "script"];
8150
+ ARGV_FIELDS = ["args", "argv"];
8151
+ BARE_WORD = /^[A-Za-z0-9_./+:@%,-]+$/;
8152
+ MAX_ARGV_CHARACTERS = 262144;
7945
8153
  ({ syntax: syntax2 } = mvdanSh2);
7946
8154
  MAX_ALTERNATIVES = 64;
7947
8155
  MAX_DEPTH = 64;
@@ -10067,6 +10275,7 @@ zoo`.split("\n");
10067
10275
  indices = new Map(BIP39_ENGLISH.map((word, i) => [word, i]));
10068
10276
  lengths = [24, 21, 18, 15, 12];
10069
10277
  hash = (value) => createHash2("sha256").update(value).digest();
10278
+ MAX_MNEMONIC_HASHES = 25e3;
10070
10279
  IBAN_LENGTH = {
10071
10280
  AD: 24,
10072
10281
  AE: 23,
@@ -10174,6 +10383,7 @@ zoo`.split("\n");
10174
10383
  CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
10175
10384
  CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
10176
10385
  INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
10386
+ INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
10177
10387
  HOMOGLYPHS = {
10178
10388
  // Cyrillic lowercase
10179
10389
  \u0430: "a",
@@ -10832,6 +11042,7 @@ zoo`.split("\n");
10832
11042
  MAX_DEPTH2 = 5;
10833
11043
  MAX_STRING_BYTES = 1e5;
10834
11044
  MAX_JSON_PARSE_BYTES = 1e4;
11045
+ BATCH_SEPARATOR = "\n";
10835
11046
  ({ syntax: syntax3 } = mvdanSh3);
10836
11047
  NPM_INSTALL_VERBS = /* @__PURE__ */ new Set([
10837
11048
  "install",
@@ -11301,16 +11512,24 @@ zoo`.split("\n");
11301
11512
  ["browser_batch", ["actions[].input.url"]]
11302
11513
  ]);
11303
11514
  SECRET_VIEW_LIMITS = {
11515
+ /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
11304
11516
  maxDepth: 4,
11305
- maxViewsPerLeaf: 64,
11306
- maxViews: 256,
11307
- maxDerivedBytes: 1048576,
11308
- maxCandidates: 4096
11517
+ /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
11518
+ maxNestedViewsPerLeaf: 64,
11519
+ maxNestedBytes: 1048576,
11520
+ /** Joined hostname-label text per call; past it the call is reviewed. */
11521
+ maxJoinedLabelBytes: 1048576,
11522
+ /** Decoded fragments are matched in batches of about this many characters. */
11523
+ batchCharacters: 65536,
11524
+ /** Only a valid DNS name has its labels joined. */
11525
+ maxHostLength: 253,
11526
+ maxLabelLength: 63,
11527
+ maxLabels: 127
11309
11528
  };
11310
11529
  ARGUMENT_SCAN_LIMITS = {
11311
11530
  maxDepth: 12,
11312
11531
  maxNodes: 16384,
11313
- /** Raw text is matched in windows of this size; decoding views use the first window. */
11532
+ /** Text is matched and decoded in windows of this size. */
11314
11533
  maxStringCharacters: 1e5,
11315
11534
  /** Windows overlap by this much so a token on a window edge is still seen whole. */
11316
11535
  windowOverlap: 4096,
@@ -11320,6 +11539,11 @@ zoo`.split("\n");
11320
11539
  maxTailBytes: 4194304,
11321
11540
  maxJsonCharacters: 1e4
11322
11541
  };
11542
+ RUN_CHAR = /[A-Za-z0-9+/=_%\\.-]/;
11543
+ runChar = (value, index) => {
11544
+ const char = value[index] ?? "";
11545
+ return RUN_CHAR.test(char) || char !== "" && isInvisibleChar(char);
11546
+ };
11323
11547
  VERDICT_RANK = {
11324
11548
  allow: 0,
11325
11549
  review: 1,
@@ -11664,6 +11888,10 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
11664
11888
  taintFromEid: meta.taintFromEid,
11665
11889
  ...meta.taintSource && { taintSource: meta.taintSource }
11666
11890
  } : {};
11891
+ const exposureFields = {
11892
+ ...meta?.exposure ? { exposure: meta.exposure } : {},
11893
+ ...meta?.exposureFromEid ? { exposureFromEid: meta.exposureFromEid } : {}
11894
+ };
11667
11895
  const eid = generateEventId();
11668
11896
  appendToLog(LOCAL_AUDIT_LOG, {
11669
11897
  // eid first: the outbox shipper dedups on it, and a fixed leading field
@@ -11686,6 +11914,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
11686
11914
  ...loopCountField,
11687
11915
  ...transcriptPathField,
11688
11916
  ...taintFields,
11917
+ ...exposureFields,
11689
11918
  ...testRun,
11690
11919
  agent: meta?.agent,
11691
11920
  mcpServer: meta?.mcpServer,
@@ -11717,10 +11946,72 @@ var init_audit = __esm({
11717
11946
  // src/core.ts
11718
11947
  init_audit();
11719
11948
 
11949
+ // src/auth/trusted-recipients.ts
11950
+ init_dist();
11951
+ import fs2 from "fs";
11952
+ import path2 from "path";
11953
+ import os2 from "os";
11954
+ function getTrustedRecipientsPath() {
11955
+ return path2.join(os2.homedir(), ".node9", "trusted-recipients.json");
11956
+ }
11957
+ function readTrustedRecipients() {
11958
+ try {
11959
+ const raw = fs2.readFileSync(getTrustedRecipientsPath(), "utf8");
11960
+ const parsed = JSON.parse(raw);
11961
+ return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
11962
+ } catch {
11963
+ return [];
11964
+ }
11965
+ }
11966
+ function writeTrustedRecipients(entries) {
11967
+ const filePath = getTrustedRecipientsPath();
11968
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
11969
+ const tmp = filePath + ".node9-tmp";
11970
+ fs2.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
11971
+ fs2.renameSync(tmp, filePath);
11972
+ }
11973
+ function normalizeRecipient(raw) {
11974
+ const v = raw.trim();
11975
+ if (!v) return null;
11976
+ if (/^(email|host|iban):/i.test(v)) {
11977
+ const i = v.indexOf(":");
11978
+ const kind = v.slice(0, i).toLowerCase();
11979
+ const inner = normalizeRecipient(v.slice(i + 1));
11980
+ return inner && inner.startsWith(`${kind}:`) ? inner : null;
11981
+ }
11982
+ const found = extractArgDestinations({ to: v });
11983
+ return found.length === 1 ? found[0] : null;
11984
+ }
11985
+ function addTrustedRecipient(raw, addedBy = "user") {
11986
+ const value = normalizeRecipient(raw);
11987
+ if (!value) return null;
11988
+ const entries = readTrustedRecipients();
11989
+ if (!entries.some((e) => e.value === value)) {
11990
+ entries.push({ value, addedAt: Date.now(), addedBy });
11991
+ writeTrustedRecipients(entries);
11992
+ }
11993
+ return value;
11994
+ }
11995
+ var cache = null;
11996
+ function mtimeOf() {
11997
+ try {
11998
+ return fs2.statSync(getTrustedRecipientsPath()).mtimeMs;
11999
+ } catch {
12000
+ return 0;
12001
+ }
12002
+ }
12003
+ function isTrustedRecipient(value) {
12004
+ const mtime = mtimeOf();
12005
+ if (!cache || cache.mtime !== mtime) {
12006
+ cache = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
12007
+ }
12008
+ return cache.values.has(value);
12009
+ }
12010
+
11720
12011
  // src/config/index.ts
11721
- import fs4 from "fs";
11722
- import path4 from "path";
11723
- import os4 from "os";
12012
+ import fs5 from "fs";
12013
+ import path5 from "path";
12014
+ import os5 from "os";
11724
12015
 
11725
12016
  // src/config-schema.ts
11726
12017
  import { z } from "zod";
@@ -11989,17 +12280,17 @@ function sanitizeConfig(raw) {
11989
12280
  }
11990
12281
 
11991
12282
  // src/shields.ts
11992
- import fs2 from "fs";
11993
- import path2 from "path";
11994
- import os2 from "os";
12283
+ import fs3 from "fs";
12284
+ import path3 from "path";
12285
+ import os3 from "os";
11995
12286
  init_dist();
11996
12287
  init_dist();
11997
- var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
12288
+ var USER_SHIELDS_DIR = path3.join(os3.homedir(), ".node9", "shields");
11998
12289
  function loadUserShields() {
11999
12290
  const result = {};
12000
12291
  let entries;
12001
12292
  try {
12002
- entries = fs2.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
12293
+ entries = fs3.readdirSync(USER_SHIELDS_DIR).filter((f) => f.endsWith(".json"));
12003
12294
  } catch (err) {
12004
12295
  if (err.code !== "ENOENT") {
12005
12296
  process.stderr.write(
@@ -12010,9 +12301,9 @@ function loadUserShields() {
12010
12301
  return result;
12011
12302
  }
12012
12303
  for (const file2 of entries) {
12013
- const filePath = path2.join(USER_SHIELDS_DIR, file2);
12304
+ const filePath = path3.join(USER_SHIELDS_DIR, file2);
12014
12305
  try {
12015
- const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
12306
+ const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
12016
12307
  const v = validateShieldDefinition(raw);
12017
12308
  if ("error" in v) {
12018
12309
  process.stderr.write(`[node9] ${v.error}: ${filePath}
@@ -12043,10 +12334,10 @@ function getShield(name) {
12043
12334
  const resolved = resolveShieldName(name);
12044
12335
  return resolved ? SHIELDS[resolved] : null;
12045
12336
  }
12046
- var SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
12337
+ var SHIELDS_STATE_FILE = path3.join(os3.homedir(), ".node9", "shields.json");
12047
12338
  function readShieldsFile() {
12048
12339
  try {
12049
- const raw = fs2.readFileSync(SHIELDS_STATE_FILE, "utf-8");
12340
+ const raw = fs3.readFileSync(SHIELDS_STATE_FILE, "utf-8");
12050
12341
  if (!raw.trim()) return { active: [] };
12051
12342
  const parsed = JSON.parse(raw);
12052
12343
  const active = Array.isArray(parsed.active) ? parsed.active.filter(
@@ -12258,15 +12549,15 @@ function pathRules(rawPath, verdict, reason) {
12258
12549
  }
12259
12550
 
12260
12551
  // src/auth/trusted-hosts.ts
12261
- import fs3 from "fs";
12262
- import path3 from "path";
12263
- import os3 from "os";
12552
+ import fs4 from "fs";
12553
+ import path4 from "path";
12554
+ import os4 from "os";
12264
12555
  function getTrustedHostsPath() {
12265
- return path3.join(os3.homedir(), ".node9", "trusted-hosts.json");
12556
+ return path4.join(os4.homedir(), ".node9", "trusted-hosts.json");
12266
12557
  }
12267
12558
  function readTrustedHosts() {
12268
12559
  try {
12269
- const raw = fs3.readFileSync(getTrustedHostsPath(), "utf8");
12560
+ const raw = fs4.readFileSync(getTrustedHostsPath(), "utf8");
12270
12561
  const parsed = JSON.parse(raw);
12271
12562
  return Array.isArray(parsed.hosts) ? parsed.hosts : [];
12272
12563
  } catch {
@@ -12277,7 +12568,7 @@ var _cache = null;
12277
12568
  var CACHE_TTL_MS = 5e3;
12278
12569
  function getFileMtime() {
12279
12570
  try {
12280
- return fs3.statSync(getTrustedHostsPath()).mtimeMs;
12571
+ return fs4.statSync(getTrustedHostsPath()).mtimeMs;
12281
12572
  } catch {
12282
12573
  return 0;
12283
12574
  }
@@ -12742,7 +13033,8 @@ var DEFAULT_CONFIG = {
12742
13033
  spotlight: true,
12743
13034
  afterExposure: "ask-once",
12744
13035
  orgDomains: [],
12745
- roles: {}
13036
+ roles: {},
13037
+ trustedRecipients: []
12746
13038
  },
12747
13039
  trustedHosts: [],
12748
13040
  trustedHostsManaged: false,
@@ -12816,8 +13108,8 @@ function noteRejectedApiUrl(raw) {
12816
13108
  if (rejectedApiUrlsSeen.has(key)) return;
12817
13109
  rejectedApiUrlsSeen.add(key);
12818
13110
  try {
12819
- fs4.appendFileSync(
12820
- path4.join(os4.homedir(), ".node9", "hook-debug.log"),
13111
+ fs5.appendFileSync(
13112
+ path5.join(os5.homedir(), ".node9", "hook-debug.log"),
12821
13113
  `[${(/* @__PURE__ */ new Date()).toISOString()}] REFUSED apiUrl, using the default instead: ${String(raw).slice(0, 200).replace(/[\r\n]+/g, " ")}
12822
13114
  `
12823
13115
  );
@@ -12834,9 +13126,9 @@ function getCredentials() {
12834
13126
  };
12835
13127
  }
12836
13128
  try {
12837
- const credPath = path4.join(os4.homedir(), ".node9", "credentials.json");
12838
- if (fs4.existsSync(credPath)) {
12839
- const creds = JSON.parse(fs4.readFileSync(credPath, "utf-8"));
13129
+ const credPath = path5.join(os5.homedir(), ".node9", "credentials.json");
13130
+ if (fs5.existsSync(credPath)) {
13131
+ const creds = JSON.parse(fs5.readFileSync(credPath, "utf-8"));
12840
13132
  const profileName = process.env.NODE9_PROFILE || "default";
12841
13133
  const profile = creds[profileName];
12842
13134
  if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
@@ -12868,7 +13160,7 @@ function readRulesCacheResilient(cacheFile) {
12868
13160
  for (let attempt = 0; attempt < 3; attempt++) {
12869
13161
  let content;
12870
13162
  try {
12871
- content = fs4.readFileSync(cacheFile, "utf-8");
13163
+ content = fs5.readFileSync(cacheFile, "utf-8");
12872
13164
  existed = true;
12873
13165
  } catch (err) {
12874
13166
  if (err.code === "ENOENT") return {};
@@ -12883,10 +13175,10 @@ function readRulesCacheResilient(cacheFile) {
12883
13175
  }
12884
13176
  }
12885
13177
  if (existed || sawReadError) {
12886
- const backup = path4.join(path4.dirname(cacheFile), "rules-cache.last-good.json");
13178
+ const backup = path5.join(path5.dirname(cacheFile), "rules-cache.last-good.json");
12887
13179
  if (backup !== cacheFile) {
12888
13180
  try {
12889
- const raw = JSON.parse(fs4.readFileSync(backup, "utf-8"));
13181
+ const raw = JSON.parse(fs5.readFileSync(backup, "utf-8"));
12890
13182
  logCacheReadIssue(cacheFile, "RULES_CACHE_CORRUPT_USED_BACKUP");
12891
13183
  lastParsedRulesCache = raw;
12892
13184
  return raw;
@@ -12909,8 +13201,8 @@ function logCacheReadIssue(cacheFile, kind) {
12909
13201
  if (now - cacheReadLastLoggedAt < CACHE_LOG_REARM_MS) return;
12910
13202
  cacheReadLastLoggedAt = now;
12911
13203
  try {
12912
- fs4.appendFileSync(
12913
- path4.join(os4.homedir(), ".node9", "hook-debug.log"),
13204
+ fs5.appendFileSync(
13205
+ path5.join(os5.homedir(), ".node9", "hook-debug.log"),
12914
13206
  `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} ${cacheFile}
12915
13207
  `
12916
13208
  );
@@ -12931,8 +13223,8 @@ function catalogSettingsFrom(settings, policy) {
12931
13223
  }
12932
13224
  function getConfig(cwd) {
12933
13225
  if (!cwd && cachedConfig) return cachedConfig;
12934
- const globalPath = path4.join(os4.homedir(), ".node9", "config.json");
12935
- const projectPath = path4.join(cwd ?? process.cwd(), "node9.config.json");
13226
+ const globalPath = path5.join(os5.homedir(), ".node9", "config.json");
13227
+ const projectPath = path5.join(cwd ?? process.cwd(), "node9.config.json");
12936
13228
  const globalConfig = tryLoadConfig(globalPath);
12937
13229
  const projectConfig = tryLoadConfig(projectPath, { project: true });
12938
13230
  const mergedSettings = {
@@ -12945,6 +13237,7 @@ function getConfig(cwd) {
12945
13237
  dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
12946
13238
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
12947
13239
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
13240
+ projectToolInspection: {},
12948
13241
  // Shipped rules carry `builtin`: the catalog's per-check value governs
12949
13242
  // them and only them (a user rule that reuses a name keeps its verdict).
12950
13243
  smartRules: DEFAULT_CONFIG.policy.smartRules.map((r) => ({ ...r, builtin: true })),
@@ -12971,7 +13264,8 @@ function getConfig(cwd) {
12971
13264
  untrustedContent: {
12972
13265
  ...DEFAULT_CONFIG.policy.untrustedContent,
12973
13266
  orgDomains: [...DEFAULT_CONFIG.policy.untrustedContent.orgDomains],
12974
- roles: {}
13267
+ roles: {},
13268
+ trustedRecipients: []
12975
13269
  },
12976
13270
  // Left empty on purpose: the local file is read fresh at policy-eval time
12977
13271
  // (getCachedHosts via isTrustedHost), NOT snapshotted into the frozen config
@@ -13017,8 +13311,14 @@ function getConfig(cwd) {
13017
13311
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
13018
13312
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
13019
13313
  if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
13020
- if (p.toolInspection)
13021
- mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
13314
+ if (p.toolInspection) {
13315
+ if (isProject)
13316
+ mergedPolicy.projectToolInspection = {
13317
+ ...mergedPolicy.projectToolInspection,
13318
+ ...p.toolInspection
13319
+ };
13320
+ else mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
13321
+ }
13022
13322
  if (p.smartRules) {
13023
13323
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
13024
13324
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
@@ -13166,7 +13466,7 @@ function getConfig(cwd) {
13166
13466
  let cloudMandatesEnforcement = false;
13167
13467
  let cloudMandatesAppPerm = false;
13168
13468
  {
13169
- const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
13469
+ const cacheFile = path5.join(os5.homedir(), ".node9", "rules-cache.json");
13170
13470
  try {
13171
13471
  const raw = readRulesCacheResilient(cacheFile);
13172
13472
  if (Array.isArray(raw.rules) && raw.rules.length > 0) {
@@ -13559,10 +13859,10 @@ function getConfig(cwd) {
13559
13859
  }
13560
13860
  function tryLoadConfig(filePath, options = {}) {
13561
13861
  v2ChecksByPath.delete(filePath);
13562
- if (!fs4.existsSync(filePath)) return null;
13862
+ if (!fs5.existsSync(filePath)) return null;
13563
13863
  let raw;
13564
13864
  try {
13565
- raw = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
13865
+ raw = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
13566
13866
  } catch (err) {
13567
13867
  const msg = err instanceof Error ? err.message : String(err);
13568
13868
  process.stderr.write(
@@ -13637,6 +13937,13 @@ function applyUntrustedContent(cur, raw, replaceLists = false, isProject = false
13637
13937
  if (replaceLists) cur.orgDomains = domains;
13638
13938
  else for (const d of domains) if (!cur.orgDomains.includes(d)) cur.orgDomains.push(d);
13639
13939
  }
13940
+ if (replaceLists && Array.isArray(uc.trustedRecipients)) {
13941
+ cur.trustedRecipients = [
13942
+ ...new Set(
13943
+ uc.trustedRecipients.filter((v) => typeof v === "string").map((v) => normalizeRecipient(v)).filter((v) => v !== null)
13944
+ )
13945
+ ].slice(0, 200);
13946
+ }
13640
13947
  if (uc.roles && typeof uc.roles === "object" && !Array.isArray(uc.roles)) {
13641
13948
  const next = replaceLists ? {} : cur.roles;
13642
13949
  for (const [server, tools] of Object.entries(uc.roles)) {
@@ -13660,18 +13967,18 @@ init_dist();
13660
13967
  // src/dlp.ts
13661
13968
  init_dist();
13662
13969
  init_dist();
13663
- import fs5 from "fs";
13664
- import path5 from "path";
13970
+ import fs6 from "fs";
13971
+ import path6 from "path";
13665
13972
  function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
13666
13973
  if (!filePath) return null;
13667
13974
  let resolved;
13668
13975
  try {
13669
- const absolute = path5.resolve(cwd, filePath);
13670
- resolved = fs5.realpathSync.native(absolute);
13976
+ const absolute = path6.resolve(cwd, filePath);
13977
+ resolved = fs6.realpathSync.native(absolute);
13671
13978
  } catch (err) {
13672
13979
  const code = err.code;
13673
13980
  if (code === "ENOENT" || code === "ENOTDIR") {
13674
- resolved = path5.resolve(cwd, filePath);
13981
+ resolved = path6.resolve(cwd, filePath);
13675
13982
  } else {
13676
13983
  return sensitivePathMatch(filePath);
13677
13984
  }
@@ -13681,29 +13988,29 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
13681
13988
 
13682
13989
  // src/policy/file-access.ts
13683
13990
  init_dist();
13684
- import fs6 from "fs";
13685
- import os5 from "os";
13686
- import path6 from "path";
13991
+ import fs7 from "fs";
13992
+ import os6 from "os";
13993
+ import path7 from "path";
13687
13994
  function absoluteSpelling(p, cwd, home) {
13688
13995
  if (p === "~" || p.startsWith("~/")) p = home + p.slice(1);
13689
- return path6.isAbsolute(p) ? p : `${cwd}/${p}`;
13996
+ return path7.isAbsolute(p) ? p : `${cwd}/${p}`;
13690
13997
  }
13691
13998
  function physical(p, followFinal, links = 0) {
13692
13999
  if (links > 40) throw new Error("Too many symbolic links");
13693
14000
  if (!followFinal) {
13694
- const parent = physical(path6.dirname(p), true, links);
13695
- return { path: `${parent.path}/${path6.basename(p)}`, missing: parent.missing };
14001
+ const parent = physical(path7.dirname(p), true, links);
14002
+ return { path: `${parent.path}/${path7.basename(p)}`, missing: parent.missing };
13696
14003
  }
13697
14004
  try {
13698
- return { path: fs6.realpathSync.native(p), missing: false };
14005
+ return { path: fs7.realpathSync.native(p), missing: false };
13699
14006
  } catch (error) {
13700
14007
  const code = error.code;
13701
14008
  if (!["ENOENT", "ENOTDIR"].includes(code ?? "")) throw error;
13702
14009
  try {
13703
- if (fs6.lstatSync(p).isSymbolicLink()) {
13704
- const target = fs6.readlinkSync(p);
14010
+ if (fs7.lstatSync(p).isSymbolicLink()) {
14011
+ const target = fs7.readlinkSync(p);
13705
14012
  return physical(
13706
- path6.isAbsolute(target) ? target : `${path6.dirname(p)}/${target}`,
14013
+ path7.isAbsolute(target) ? target : `${path7.dirname(p)}/${target}`,
13707
14014
  true,
13708
14015
  links + 1
13709
14016
  );
@@ -13712,13 +14019,13 @@ function physical(p, followFinal, links = 0) {
13712
14019
  if (!["ENOENT", "ENOTDIR"].includes(linkError.code ?? ""))
13713
14020
  throw linkError;
13714
14021
  }
13715
- const parent = path6.dirname(p);
14022
+ const parent = path7.dirname(p);
13716
14023
  if (parent === p) return { path: p, missing: true };
13717
14024
  const resolved = physical(parent, true, links);
13718
- return { path: `${resolved.path}/${path6.basename(p)}`, missing: true };
14025
+ return { path: `${resolved.path}/${path7.basename(p)}`, missing: true };
13719
14026
  }
13720
14027
  }
13721
- function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = os5.homedir()) {
14028
+ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, home = os6.homedir()) {
13722
14029
  const context = { cwd, homeDirs: [home, "/root"] };
13723
14030
  const accesses = collectFileAccesses(tool, args, context, inspection);
13724
14031
  const handledPaths = /* @__PURE__ */ new Set();
@@ -13741,14 +14048,14 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
13741
14048
  const raw = absoluteSpelling(access.path, access.cwd ?? cwd, home);
13742
14049
  if (access.directoryTarget) {
13743
14050
  try {
13744
- if (!fs6.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
14051
+ if (!fs7.statSync(absoluteSpelling(access.directoryTarget, access.cwd ?? cwd, home)).isDirectory())
13745
14052
  continue;
13746
14053
  } catch {
13747
14054
  }
13748
14055
  }
13749
14056
  if (access.operation === "create") {
13750
14057
  try {
13751
- fs6.statSync(raw);
14058
+ fs7.statSync(raw);
13752
14059
  continue;
13753
14060
  } catch {
13754
14061
  }
@@ -13797,27 +14104,27 @@ function prepareFileAccesses(tool, args, cwd = process.cwd(), inspection = {}, h
13797
14104
  }
13798
14105
 
13799
14106
  // src/utils/provenance.ts
13800
- import fs7 from "fs";
13801
- import path7 from "path";
13802
- import os6 from "os";
14107
+ import fs8 from "fs";
14108
+ import path8 from "path";
14109
+ import os7 from "os";
13803
14110
  var SYSTEM_PREFIXES = ["/usr/bin", "/usr/sbin", "/bin", "/sbin"];
13804
14111
  var MANAGED_PREFIXES = ["/usr/local/bin", "/opt/homebrew", "/home/linuxbrew", "/nix/store"];
13805
14112
  var USER_PREFIXES = [
13806
- path7.join(os6.homedir(), "bin"),
13807
- path7.join(os6.homedir(), ".local", "bin"),
13808
- path7.join(os6.homedir(), ".cargo", "bin"),
13809
- path7.join(os6.homedir(), ".npm-global", "bin"),
13810
- path7.join(os6.homedir(), ".volta", "bin")
14113
+ path8.join(os7.homedir(), "bin"),
14114
+ path8.join(os7.homedir(), ".local", "bin"),
14115
+ path8.join(os7.homedir(), ".cargo", "bin"),
14116
+ path8.join(os7.homedir(), ".npm-global", "bin"),
14117
+ path8.join(os7.homedir(), ".volta", "bin")
13811
14118
  ];
13812
14119
  var SUSPECT_PREFIXES = ["/tmp", "/var/tmp", "/dev/shm"];
13813
14120
  function findInPath(cmd) {
13814
- if (path7.posix.isAbsolute(cmd)) return cmd;
14121
+ if (path8.posix.isAbsolute(cmd)) return cmd;
13815
14122
  const pathEnv = process.env.PATH ?? "";
13816
- for (const dir2 of pathEnv.split(path7.delimiter)) {
14123
+ for (const dir2 of pathEnv.split(path8.delimiter)) {
13817
14124
  if (!dir2) continue;
13818
- const full = path7.join(dir2, cmd);
14125
+ const full = path8.join(dir2, cmd);
13819
14126
  try {
13820
- fs7.accessSync(full, fs7.constants.X_OK);
14127
+ fs8.accessSync(full, fs8.constants.X_OK);
13821
14128
  return full;
13822
14129
  } catch {
13823
14130
  }
@@ -13828,7 +14135,7 @@ function _classifyPath(resolved, cwd) {
13828
14135
  if (cwd && resolved.startsWith(cwd + "/")) {
13829
14136
  return { trustLevel: "user", reason: "binary in project directory" };
13830
14137
  }
13831
- const osTmp = os6.tmpdir();
14138
+ const osTmp = os7.tmpdir();
13832
14139
  const allSuspect = osTmp ? [...SUSPECT_PREFIXES, osTmp] : SUSPECT_PREFIXES;
13833
14140
  if (allSuspect.some((p) => resolved === p || resolved.startsWith(p + "/"))) {
13834
14141
  return { trustLevel: "suspect", reason: `binary in temp directory: ${resolved}` };
@@ -13846,7 +14153,7 @@ function _classifyPath(resolved, cwd) {
13846
14153
  }
13847
14154
  function checkProvenance(cmd, cwd) {
13848
14155
  const bare = cmd.startsWith("./") ? cmd.slice(2) : cmd;
13849
- if (path7.posix.isAbsolute(bare)) {
14156
+ if (path8.posix.isAbsolute(bare)) {
13850
14157
  const early = _classifyPath(bare, cwd);
13851
14158
  if (early.trustLevel === "suspect") {
13852
14159
  return { resolvedPath: bare, ...early };
@@ -13862,7 +14169,7 @@ function checkProvenance(cmd, cwd) {
13862
14169
  reason: "binary not found in PATH"
13863
14170
  };
13864
14171
  }
13865
- resolved = fs7.realpathSync(found);
14172
+ resolved = fs8.realpathSync(found);
13866
14173
  } catch {
13867
14174
  return {
13868
14175
  resolvedPath: cmd,
@@ -13871,7 +14178,7 @@ function checkProvenance(cmd, cwd) {
13871
14178
  };
13872
14179
  }
13873
14180
  try {
13874
- const stat = fs7.statSync(resolved);
14181
+ const stat = fs8.statSync(resolved);
13875
14182
  if (stat.mode & 2) {
13876
14183
  return {
13877
14184
  resolvedPath: resolved,
@@ -13896,7 +14203,7 @@ init_dist();
13896
14203
  init_dist();
13897
14204
  async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
13898
14205
  const config = opts?.config ?? getConfig(cwd);
13899
- const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, config.policy.toolInspection);
14206
+ const prepared = opts?.fileAccess ?? prepareFileAccesses(toolName, args, cwd, inspectionLayers(config.policy));
13900
14207
  const activeEnvironment = getActiveEnvironment(config) ?? void 0;
13901
14208
  return evaluatePolicy(
13902
14209
  config,
@@ -13925,32 +14232,32 @@ function isIgnoredTool2(toolName) {
13925
14232
  }
13926
14233
 
13927
14234
  // src/auth/state.ts
13928
- import fs9 from "fs";
13929
- import path9 from "path";
13930
- import os7 from "os";
14235
+ import fs10 from "fs";
14236
+ import path10 from "path";
14237
+ import os8 from "os";
13931
14238
 
13932
14239
  // src/utils/atomic-write.ts
13933
- import fs8 from "fs";
13934
- import path8 from "path";
14240
+ import fs9 from "fs";
14241
+ import path9 from "path";
13935
14242
  import { randomUUID } from "crypto";
13936
14243
  function atomicWriteSync(filePath, data, options) {
13937
- const dir2 = path8.dirname(filePath);
13938
- if (!fs8.existsSync(dir2)) fs8.mkdirSync(dir2, { recursive: true });
14244
+ const dir2 = path9.dirname(filePath);
14245
+ if (!fs9.existsSync(dir2)) fs9.mkdirSync(dir2, { recursive: true });
13939
14246
  const tmpPath = `${filePath}.${randomUUID()}.tmp`;
13940
14247
  try {
13941
- fs8.writeFileSync(tmpPath, data, options);
14248
+ fs9.writeFileSync(tmpPath, data, options);
13942
14249
  } catch (err) {
13943
14250
  try {
13944
- fs8.unlinkSync(tmpPath);
14251
+ fs9.unlinkSync(tmpPath);
13945
14252
  } catch {
13946
14253
  }
13947
14254
  throw err;
13948
14255
  }
13949
14256
  try {
13950
- fs8.renameSync(tmpPath, filePath);
14257
+ fs9.renameSync(tmpPath, filePath);
13951
14258
  } catch (err) {
13952
14259
  try {
13953
- fs8.unlinkSync(tmpPath);
14260
+ fs9.unlinkSync(tmpPath);
13954
14261
  } catch {
13955
14262
  }
13956
14263
  throw err;
@@ -13958,8 +14265,8 @@ function atomicWriteSync(filePath, data, options) {
13958
14265
  }
13959
14266
 
13960
14267
  // src/auth/state.ts
13961
- var PAUSED_FILE = path9.join(os7.homedir(), ".node9", "PAUSED");
13962
- var TRUST_FILE = path9.join(os7.homedir(), ".node9", "trust.json");
14268
+ var PAUSED_FILE = path10.join(os8.homedir(), ".node9", "PAUSED");
14269
+ var TRUST_FILE = path10.join(os8.homedir(), ".node9", "trust.json");
13963
14270
  function extractCommandPattern(toolName, args) {
13964
14271
  const lower2 = toolName.toLowerCase();
13965
14272
  if (lower2 !== "bash" && lower2 !== "execute_bash" && lower2 !== "shell") return void 0;
@@ -13971,11 +14278,11 @@ function extractCommandPattern(toolName, args) {
13971
14278
  }
13972
14279
  function checkPause() {
13973
14280
  try {
13974
- if (!fs9.existsSync(PAUSED_FILE)) return { paused: false };
13975
- const state = JSON.parse(fs9.readFileSync(PAUSED_FILE, "utf-8"));
14281
+ if (!fs10.existsSync(PAUSED_FILE)) return { paused: false };
14282
+ const state = JSON.parse(fs10.readFileSync(PAUSED_FILE, "utf-8"));
13976
14283
  if (state.expiry > 0 && Date.now() >= state.expiry) {
13977
14284
  try {
13978
- fs9.unlinkSync(PAUSED_FILE);
14285
+ fs10.unlinkSync(PAUSED_FILE);
13979
14286
  } catch {
13980
14287
  }
13981
14288
  return { paused: false };
@@ -13987,8 +14294,8 @@ function checkPause() {
13987
14294
  }
13988
14295
  function getActiveTrustSession(toolName, args) {
13989
14296
  try {
13990
- if (!fs9.existsSync(TRUST_FILE)) return false;
13991
- const trust = JSON.parse(fs9.readFileSync(TRUST_FILE, "utf-8"));
14297
+ if (!fs10.existsSync(TRUST_FILE)) return false;
14298
+ const trust = JSON.parse(fs10.readFileSync(TRUST_FILE, "utf-8"));
13992
14299
  const now = Date.now();
13993
14300
  const active = trust.entries.filter((e) => e.expiry > now);
13994
14301
  if (active.length !== trust.entries.length) {
@@ -14011,8 +14318,8 @@ function writeTrustSession(toolName, durationMs, args) {
14011
14318
  try {
14012
14319
  let trust = { entries: [] };
14013
14320
  try {
14014
- if (fs9.existsSync(TRUST_FILE)) {
14015
- trust = JSON.parse(fs9.readFileSync(TRUST_FILE, "utf-8"));
14321
+ if (fs10.existsSync(TRUST_FILE)) {
14322
+ trust = JSON.parse(fs10.readFileSync(TRUST_FILE, "utf-8"));
14016
14323
  }
14017
14324
  } catch {
14018
14325
  }
@@ -14034,9 +14341,9 @@ function writeTrustSession(toolName, durationMs, args) {
14034
14341
  }
14035
14342
  function getPersistentDecision(toolName) {
14036
14343
  try {
14037
- const file2 = path9.join(os7.homedir(), ".node9", "decisions.json");
14038
- if (!fs9.existsSync(file2)) return null;
14039
- const decisions = JSON.parse(fs9.readFileSync(file2, "utf-8"));
14344
+ const file2 = path10.join(os8.homedir(), ".node9", "decisions.json");
14345
+ if (!fs10.existsSync(file2)) return null;
14346
+ const decisions = JSON.parse(fs10.readFileSync(file2, "utf-8"));
14040
14347
  const d = decisions[toolName];
14041
14348
  if (d === "allow" || d === "deny") return d;
14042
14349
  } catch {
@@ -14045,11 +14352,11 @@ function getPersistentDecision(toolName) {
14045
14352
  }
14046
14353
 
14047
14354
  // src/auth/daemon.ts
14048
- import fs10 from "fs";
14355
+ import fs11 from "fs";
14049
14356
  import net from "net";
14050
- import path10 from "path";
14051
- import os8 from "os";
14052
- var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
14357
+ import path11 from "path";
14358
+ import os9 from "os";
14359
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path11.join(os9.tmpdir(), "node9-activity.sock");
14053
14360
  function notifyActivitySocket(data) {
14054
14361
  return new Promise((resolve) => {
14055
14362
  try {
@@ -14091,9 +14398,9 @@ var DAEMON_PORT = resolveDaemonPort();
14091
14398
  var DAEMON_HOST = "127.0.0.1";
14092
14399
  function getInternalToken() {
14093
14400
  try {
14094
- const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
14095
- if (!fs10.existsSync(pidFile)) return null;
14096
- const data = JSON.parse(fs10.readFileSync(pidFile, "utf-8"));
14401
+ const pidFile = path11.join(os9.homedir(), ".node9", "daemon.pid");
14402
+ if (!fs11.existsSync(pidFile)) return null;
14403
+ const data = JSON.parse(fs11.readFileSync(pidFile, "utf-8"));
14097
14404
  process.kill(data.pid, 0);
14098
14405
  return data.internalToken ?? null;
14099
14406
  } catch {
@@ -14101,12 +14408,12 @@ function getInternalToken() {
14101
14408
  }
14102
14409
  }
14103
14410
  function isDaemonRunning() {
14104
- const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
14105
- if (fs10.existsSync(pidFile)) {
14411
+ const pidFile = path11.join(os9.homedir(), ".node9", "daemon.pid");
14412
+ if (fs11.existsSync(pidFile)) {
14106
14413
  let pid;
14107
14414
  let port;
14108
14415
  try {
14109
- const data = JSON.parse(fs10.readFileSync(pidFile, "utf-8"));
14416
+ const data = JSON.parse(fs11.readFileSync(pidFile, "utf-8"));
14110
14417
  pid = data.pid;
14111
14418
  port = data.port;
14112
14419
  } catch {
@@ -14124,7 +14431,7 @@ function isDaemonRunning() {
14124
14431
  } catch (err) {
14125
14432
  if (err instanceof Error && "code" in err && err.code === "ESRCH") {
14126
14433
  try {
14127
- fs10.unlinkSync(pidFile);
14434
+ fs11.unlinkSync(pidFile);
14128
14435
  } catch {
14129
14436
  }
14130
14437
  }
@@ -14332,10 +14639,10 @@ import os18 from "os";
14332
14639
 
14333
14640
  // src/ui/native.ts
14334
14641
  import { spawn } from "child_process";
14335
- import path12 from "path";
14642
+ import path13 from "path";
14336
14643
 
14337
14644
  // src/context-sniper.ts
14338
- import path11 from "path";
14645
+ import path12 from "path";
14339
14646
  function smartTruncate(str, maxLen = 500) {
14340
14647
  if (str.length <= maxLen) return str;
14341
14648
  const edge = Math.floor(maxLen / 2) - 3;
@@ -14403,7 +14710,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
14403
14710
  intent = "EDIT";
14404
14711
  if (obj.file_path) {
14405
14712
  editFilePath = String(obj.file_path);
14406
- editFileName = path11.basename(editFilePath);
14713
+ editFileName = path12.basename(editFilePath);
14407
14714
  }
14408
14715
  const result = extractContext(String(obj.new_string), matchedWord);
14409
14716
  contextSnippet = result.snippet;
@@ -14471,7 +14778,7 @@ function formatArgs(args, matchedField, matchedWord) {
14471
14778
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
14472
14779
  const obj = parsed;
14473
14780
  if (obj.old_string !== void 0 && obj.new_string !== void 0) {
14474
- const file2 = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
14781
+ const file2 = obj.file_path ? path13.basename(String(obj.file_path)) : "file";
14475
14782
  const oldPreview = smartTruncate(String(obj.old_string), 120);
14476
14783
  const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
14477
14784
  return {
@@ -14674,24 +14981,24 @@ end run`;
14674
14981
  }
14675
14982
 
14676
14983
  // src/canary/registry.ts
14677
- import fs12 from "fs";
14678
- import os10 from "os";
14679
- import path14 from "path";
14984
+ import fs13 from "fs";
14985
+ import os11 from "os";
14986
+ import path15 from "path";
14680
14987
  init_dist();
14681
14988
 
14682
14989
  // src/shields/jail.ts
14683
- import fs11 from "fs";
14684
- import os9 from "os";
14685
- import path13 from "path";
14990
+ import fs12 from "fs";
14991
+ import os10 from "os";
14992
+ import path14 from "path";
14686
14993
  init_dist();
14687
14994
  var USER_JAIL_SHIELD = "user-jail";
14688
14995
  function jailStorePath() {
14689
- return path13.join(os9.homedir(), ".node9", "jail-paths.json");
14996
+ return path14.join(os10.homedir(), ".node9", "jail-paths.json");
14690
14997
  }
14691
14998
  function readJailPaths() {
14692
14999
  let text;
14693
15000
  try {
14694
- text = fs11.readFileSync(jailStorePath(), "utf8");
15001
+ text = fs12.readFileSync(jailStorePath(), "utf8");
14695
15002
  } catch (err) {
14696
15003
  if (err.code === "ENOENT") return [];
14697
15004
  throw err;
@@ -14720,7 +15027,7 @@ function findJailedPathIn(candidate, paths) {
14720
15027
 
14721
15028
  // src/canary/registry.ts
14722
15029
  function canaryStorePath() {
14723
- return path14.join(os10.homedir(), ".node9", "canaries.json");
15030
+ return path15.join(os11.homedir(), ".node9", "canaries.json");
14724
15031
  }
14725
15032
  function isRecord(x) {
14726
15033
  if (!x || typeof x !== "object") return false;
@@ -14731,7 +15038,7 @@ function loadCanaries(opts) {
14731
15038
  const p = canaryStorePath();
14732
15039
  let raw;
14733
15040
  try {
14734
- raw = fs12.readFileSync(p, "utf-8");
15041
+ raw = fs13.readFileSync(p, "utf-8");
14735
15042
  } catch (e) {
14736
15043
  if (e.code === "ENOENT") return [];
14737
15044
  throw e;
@@ -14760,26 +15067,26 @@ init_audit();
14760
15067
  init_dist();
14761
15068
 
14762
15069
  // src/exposure/known-hosts.ts
14763
- import fs14 from "fs";
14764
- import path16 from "path";
15070
+ import fs15 from "fs";
15071
+ import path17 from "path";
14765
15072
 
14766
15073
  // src/utils/git-remotes.ts
14767
15074
  init_dist();
14768
- import fs13 from "fs";
14769
- import path15 from "path";
15075
+ import fs14 from "fs";
15076
+ import path16 from "path";
14770
15077
  function readSmallFile(p, max) {
14771
15078
  let fd;
14772
15079
  try {
14773
- fd = fs13.openSync(p, fs13.constants.O_RDONLY | (fs13.constants.O_NOFOLLOW ?? 0));
14774
- const st = fs13.fstatSync(fd);
15080
+ fd = fs14.openSync(p, fs14.constants.O_RDONLY | (fs14.constants.O_NOFOLLOW ?? 0));
15081
+ const st = fs14.fstatSync(fd);
14775
15082
  if (!st.isFile() || st.size > max) return null;
14776
15083
  const buf = Buffer.alloc(st.size);
14777
- const n = fs13.readSync(fd, buf, 0, st.size, 0);
15084
+ const n = fs14.readSync(fd, buf, 0, st.size, 0);
14778
15085
  return buf.subarray(0, n).toString("utf8");
14779
15086
  } catch {
14780
15087
  return null;
14781
15088
  } finally {
14782
- if (fd !== void 0) fs13.closeSync(fd);
15089
+ if (fd !== void 0) fs14.closeSync(fd);
14783
15090
  }
14784
15091
  }
14785
15092
  function gitRemoteHost(url) {
@@ -14798,35 +15105,35 @@ function gitRemoteHost(url) {
14798
15105
  return null;
14799
15106
  }
14800
15107
  function findGitConfig(dir2, walkUp = true) {
14801
- let cur = path15.resolve(dir2);
15108
+ let cur = path16.resolve(dir2);
14802
15109
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
14803
- const dotGit = path15.join(cur, ".git");
14804
- const direct = readSmallFile(path15.join(dotGit, "config"), 1e6);
15110
+ const dotGit = path16.join(cur, ".git");
15111
+ const direct = readSmallFile(path16.join(dotGit, "config"), 1e6);
14805
15112
  if (direct !== null) return direct;
14806
15113
  const pointer = readSmallFile(dotGit, 4096);
14807
15114
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
14808
15115
  if (m) {
14809
- let gitDir = path15.resolve(cur, m[1]);
14810
- const common = readSmallFile(path15.join(gitDir, "commondir"), 4096);
14811
- if (common !== null) gitDir = path15.resolve(gitDir, common.trim());
14812
- return readSmallFile(path15.join(gitDir, "config"), 1e6);
15116
+ let gitDir = path16.resolve(cur, m[1]);
15117
+ const common = readSmallFile(path16.join(gitDir, "commondir"), 4096);
15118
+ if (common !== null) gitDir = path16.resolve(gitDir, common.trim());
15119
+ return readSmallFile(path16.join(gitDir, "config"), 1e6);
14813
15120
  }
14814
- const parent = path15.dirname(cur);
15121
+ const parent = path16.dirname(cur);
14815
15122
  if (parent === cur) return null;
14816
15123
  cur = parent;
14817
15124
  }
14818
15125
  return null;
14819
15126
  }
14820
- var cache = /* @__PURE__ */ new Map();
15127
+ var cache2 = /* @__PURE__ */ new Map();
14821
15128
  function readGitRemotes(dir2, opts = {}) {
14822
15129
  if (!dir2) return [];
14823
15130
  const walkUp = opts.walkUp !== false;
14824
- const key = `${walkUp ? "up" : "at"}:${path15.resolve(dir2)}`;
14825
- const hit = cache.get(key);
15131
+ const key = `${walkUp ? "up" : "at"}:${path16.resolve(dir2)}`;
15132
+ const hit = cache2.get(key);
14826
15133
  if (hit) return hit;
14827
15134
  const out = [];
14828
15135
  try {
14829
- const cfg = findGitConfig(path15.resolve(dir2), walkUp);
15136
+ const cfg = findGitConfig(path16.resolve(dir2), walkUp);
14830
15137
  if (cfg !== null) {
14831
15138
  let current = null;
14832
15139
  for (const line of cfg.split(/\r?\n/)) {
@@ -14848,7 +15155,7 @@ function readGitRemotes(dir2, opts = {}) {
14848
15155
  }
14849
15156
  } catch {
14850
15157
  }
14851
- cache.set(key, out);
15158
+ cache2.set(key, out);
14852
15159
  return out;
14853
15160
  }
14854
15161
 
@@ -14860,17 +15167,17 @@ var ECOSYSTEMS = [
14860
15167
  ["Cargo.toml", "host:crates.io"],
14861
15168
  ["Gemfile", "host:rubygems.org"]
14862
15169
  ];
14863
- var cache2 = /* @__PURE__ */ new Map();
15170
+ var cache3 = /* @__PURE__ */ new Map();
14864
15171
  function manifestRoot(dir2) {
14865
- let cur = path16.resolve(dir2);
15172
+ let cur = path17.resolve(dir2);
14866
15173
  for (let i = 0; i < 64; i++) {
14867
15174
  for (const [m] of ECOSYSTEMS) {
14868
15175
  try {
14869
- if (fs14.existsSync(path16.join(cur, m))) return cur;
15176
+ if (fs15.existsSync(path17.join(cur, m))) return cur;
14870
15177
  } catch {
14871
15178
  }
14872
15179
  }
14873
- const parent = path16.dirname(cur);
15180
+ const parent = path17.dirname(cur);
14874
15181
  if (parent === cur) return null;
14875
15182
  cur = parent;
14876
15183
  }
@@ -14879,8 +15186,8 @@ function manifestRoot(dir2) {
14879
15186
  function knownProjectHosts(dir2) {
14880
15187
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
14881
15188
  if (!dir2) return empty;
14882
- const key = path16.resolve(dir2);
14883
- const hit = cache2.get(key);
15189
+ const key = path17.resolve(dir2);
15190
+ const hit = cache3.get(key);
14884
15191
  if (hit) return hit;
14885
15192
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
14886
15193
  try {
@@ -14892,25 +15199,25 @@ function knownProjectHosts(dir2) {
14892
15199
  const root = manifestRoot(key);
14893
15200
  if (root !== null) {
14894
15201
  for (const [m, registry] of ECOSYSTEMS) {
14895
- if (fs14.existsSync(path16.join(root, m))) out.hosts.add(registry);
15202
+ if (fs15.existsSync(path17.join(root, m))) out.hosts.add(registry);
14896
15203
  }
14897
15204
  try {
14898
- if (fs14.readdirSync(root).some((f) => f.endsWith(".gemspec")))
15205
+ if (fs15.readdirSync(root).some((f) => f.endsWith(".gemspec")))
14899
15206
  out.hosts.add("host:rubygems.org");
14900
15207
  } catch {
14901
15208
  }
14902
15209
  }
14903
15210
  } catch {
14904
15211
  }
14905
- cache2.set(key, out);
15212
+ cache3.set(key, out);
14906
15213
  return out;
14907
15214
  }
14908
15215
 
14909
15216
  // src/exposure/writes.ts
14910
15217
  init_dist();
14911
- import fs15 from "fs";
14912
- import os11 from "os";
14913
- import path17 from "path";
15218
+ import fs16 from "fs";
15219
+ import os12 from "os";
15220
+ import path18 from "path";
14914
15221
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
14915
15222
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
14916
15223
  function writtenContent(args) {
@@ -14939,7 +15246,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
14939
15246
  if (typeof a.content === "string") {
14940
15247
  try {
14941
15248
  const next = JSON.parse(a.content);
14942
- const prev = JSON.parse(fs15.readFileSync(absPath, "utf8"));
15249
+ const prev = JSON.parse(fs16.readFileSync(absPath, "utf8"));
14943
15250
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
14944
15251
  } catch {
14945
15252
  return LIFECYCLE_RE.test(a.content);
@@ -14949,8 +15256,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
14949
15256
  }
14950
15257
  function judgeWritesAfterExposure(opts) {
14951
15258
  const holds = [];
14952
- const home = opts.home ?? os11.homedir();
14953
- const tmp = normalizeFilePath(os11.tmpdir());
15259
+ const home = opts.home ?? os12.homedir();
15260
+ const tmp = normalizeFilePath(os12.tmpdir());
14954
15261
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
14955
15262
  const seen = /* @__PURE__ */ new Set();
14956
15263
  for (const access of opts.accesses) {
@@ -14960,7 +15267,7 @@ function judgeWritesAfterExposure(opts) {
14960
15267
  seen.add(p);
14961
15268
  if (p === "/dev" || p.startsWith("/dev/")) continue;
14962
15269
  if (root !== null && isInside(root, p)) {
14963
- if (isAgentInstructionPath(p, root) && (path17.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
15270
+ if (isAgentInstructionPath(p, root) && (path18.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
14964
15271
  holds.push({ path: p, why: "agent-instruction" });
14965
15272
  continue;
14966
15273
  }
@@ -14982,25 +15289,25 @@ function judgeWritesAfterExposure(opts) {
14982
15289
 
14983
15290
  // src/exposure/marker.ts
14984
15291
  import crypto3 from "crypto";
14985
- import fs16 from "fs";
14986
- import os12 from "os";
14987
- import path18 from "path";
15292
+ import fs17 from "fs";
15293
+ import os13 from "os";
15294
+ import path19 from "path";
14988
15295
 
14989
15296
  // src/daemon/exposure-store.ts
14990
15297
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
14991
15298
 
14992
15299
  // src/exposure/marker.ts
14993
15300
  function dir() {
14994
- return path18.join(os12.homedir(), ".node9", "exposure-sessions");
15301
+ return path19.join(os13.homedir(), ".node9", "exposure-sessions");
14995
15302
  }
14996
15303
  function file(sessionId) {
14997
15304
  const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
14998
- return path18.join(dir(), id);
15305
+ return path19.join(dir(), id);
14999
15306
  }
15000
15307
  function maybeSessionExposed(sessionId) {
15001
15308
  if (!sessionId) return false;
15002
15309
  try {
15003
- const st = fs16.statSync(file(sessionId));
15310
+ const st = fs17.statSync(file(sessionId));
15004
15311
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
15005
15312
  } catch (e) {
15006
15313
  return e.code !== "ENOENT";
@@ -15008,20 +15315,20 @@ function maybeSessionExposed(sessionId) {
15008
15315
  }
15009
15316
 
15010
15317
  // src/exposure/roles.ts
15011
- import fs18 from "fs";
15318
+ import fs19 from "fs";
15012
15319
 
15013
15320
  // src/daemon/mcp-tools.ts
15014
- import fs17 from "fs";
15015
- import path19 from "path";
15016
- import os13 from "os";
15321
+ import fs18 from "fs";
15322
+ import path20 from "path";
15323
+ import os14 from "os";
15017
15324
  function getMcpToolsFile() {
15018
- return path19.join(os13.homedir(), ".node9", "mcp-tools.json");
15325
+ return path20.join(os14.homedir(), ".node9", "mcp-tools.json");
15019
15326
  }
15020
15327
  function readMcpToolsConfig() {
15021
15328
  try {
15022
15329
  const file2 = getMcpToolsFile();
15023
- if (!fs17.existsSync(file2)) return {};
15024
- const raw = fs17.readFileSync(file2, "utf-8");
15330
+ if (!fs18.existsSync(file2)) return {};
15331
+ const raw = fs18.readFileSync(file2, "utf-8");
15025
15332
  return JSON.parse(raw);
15026
15333
  } catch {
15027
15334
  return {};
@@ -15038,7 +15345,7 @@ var inventoryCache = null;
15038
15345
  var inventoryStamp = null;
15039
15346
  function inventoryFileStamp() {
15040
15347
  try {
15041
- const st = fs18.statSync(getMcpToolsFile());
15348
+ const st = fs19.statSync(getMcpToolsFile());
15042
15349
  return `${st.mtimeMs}:${st.size}`;
15043
15350
  } catch {
15044
15351
  return "";
@@ -15117,68 +15424,6 @@ function sourceWord(toolName) {
15117
15424
  return "tool result";
15118
15425
  }
15119
15426
 
15120
- // src/auth/trusted-recipients.ts
15121
- init_dist();
15122
- import fs19 from "fs";
15123
- import path20 from "path";
15124
- import os14 from "os";
15125
- function getTrustedRecipientsPath() {
15126
- return path20.join(os14.homedir(), ".node9", "trusted-recipients.json");
15127
- }
15128
- function readTrustedRecipients() {
15129
- try {
15130
- const raw = fs19.readFileSync(getTrustedRecipientsPath(), "utf8");
15131
- const parsed = JSON.parse(raw);
15132
- return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
15133
- } catch {
15134
- return [];
15135
- }
15136
- }
15137
- function writeTrustedRecipients(entries) {
15138
- const filePath = getTrustedRecipientsPath();
15139
- fs19.mkdirSync(path20.dirname(filePath), { recursive: true });
15140
- const tmp = filePath + ".node9-tmp";
15141
- fs19.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
15142
- fs19.renameSync(tmp, filePath);
15143
- }
15144
- function normalizeRecipient(raw) {
15145
- const v = raw.trim();
15146
- if (!v) return null;
15147
- if (/^(email|host|iban):/i.test(v)) {
15148
- const i = v.indexOf(":");
15149
- const kind = v.slice(0, i).toLowerCase();
15150
- const inner = normalizeRecipient(v.slice(i + 1));
15151
- return inner && inner.startsWith(`${kind}:`) ? inner : null;
15152
- }
15153
- const found = extractArgDestinations({ to: v });
15154
- return found.length === 1 ? found[0] : null;
15155
- }
15156
- function addTrustedRecipient(raw, addedBy = "user") {
15157
- const value = normalizeRecipient(raw);
15158
- if (!value) return null;
15159
- const entries = readTrustedRecipients();
15160
- if (!entries.some((e) => e.value === value)) {
15161
- entries.push({ value, addedAt: Date.now(), addedBy });
15162
- writeTrustedRecipients(entries);
15163
- }
15164
- return value;
15165
- }
15166
- var cache3 = null;
15167
- function mtimeOf() {
15168
- try {
15169
- return fs19.statSync(getTrustedRecipientsPath()).mtimeMs;
15170
- } catch {
15171
- return 0;
15172
- }
15173
- }
15174
- function isTrustedRecipient(value) {
15175
- const mtime = mtimeOf();
15176
- if (!cache3 || cache3.mtime !== mtime) {
15177
- cache3 = { mtime, values: new Set(readTrustedRecipients().map((e) => e.value)) };
15178
- }
15179
- return cache3.values.has(value);
15180
- }
15181
-
15182
15427
  // src/auth/cloud.ts
15183
15428
  init_audit();
15184
15429
  import fs21 from "fs";
@@ -15273,11 +15518,15 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
15273
15518
  cwd: process.cwd(),
15274
15519
  platform: os15.platform()
15275
15520
  },
15276
- // Local-only fields (fw#318 exposureDestinations) stay off the wire:
15277
- // the SaaS validates riskMetadata strictly and would drop it whole.
15521
+ // The SaaS validates riskMetadata strictly and would drop it whole on
15522
+ // an unknown key, so only fields its schema declares go on the wire.
15523
+ // exposureDestinations ships since the dashboard's 2b contract
15524
+ // (fw#318): "Approve and trust" needs them.
15278
15525
  ...riskMetadata && {
15279
15526
  riskMetadata: Object.fromEntries(
15280
- Object.entries(riskMetadata).filter(([, v]) => !Array.isArray(v))
15527
+ Object.entries(riskMetadata).filter(
15528
+ ([k, v]) => !Array.isArray(v) || k === "exposureDestinations"
15529
+ )
15281
15530
  )
15282
15531
  },
15283
15532
  ...ciContext && { ciContext },
@@ -16000,7 +16249,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16000
16249
  let fileAccess;
16001
16250
  let fileFinding;
16002
16251
  try {
16003
- fileAccess = prepareFileAccesses(toolName, args, options?.cwd, config.policy.toolInspection);
16252
+ fileAccess = prepareFileAccesses(toolName, args, options?.cwd, inspectionLayers(config.policy));
16004
16253
  fileFinding = isManual ? null : evaluateProtectedFileAccess(
16005
16254
  fileAccess.accesses,
16006
16255
  fileAccess,
@@ -16019,7 +16268,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16019
16268
  fileAccess = { cwd: options?.cwd, homeDirs: [], accesses: [], handledPaths: /* @__PURE__ */ new Set() };
16020
16269
  fileFinding = isManual ? null : incompleteFileVerdict();
16021
16270
  }
16022
- const shellInput = resolveShellInput(toolName, args, config.policy.toolInspection);
16271
+ const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
16023
16272
  const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
16024
16273
  const shellReview = !isManual && shellInput.kind === "uninspectable";
16025
16274
  if (shellReview) {
@@ -16129,6 +16378,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16129
16378
  let exposureReview = null;
16130
16379
  let exposureKeys = [];
16131
16380
  let exposureDestinations = [];
16381
+ let exposureFromEid;
16132
16382
  const ucPolicy = config.policy.untrustedContent;
16133
16383
  if (ucPolicy?.enabled && !taintWarning && meta?.sessionId && !isManual) {
16134
16384
  const role = toolRole(toolName, ucPolicy);
@@ -16136,7 +16386,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16136
16386
  const shellExp = shellInput.kind === "shell" ? classifyShellExposure(shellInput.command) : null;
16137
16387
  const bareName = splitMcpToolName(toolName)?.bare;
16138
16388
  const mcpFileWrite = bareName !== void 0 && fileToolOperation(bareName, args) === "write";
16139
- const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, config.policy.toolInspection).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
16389
+ const writes = (mcpFileWrite ? prepareFileAccesses(bareName, args, options?.cwd, inspectionLayers(config.policy)).accesses : fileAccess.accesses).filter((a) => ["write", "create", "remove"].includes(a.operation));
16140
16390
  const sendsSomething = acts && !mcpFileWrite || !!shellExp?.sends;
16141
16391
  const mayBeExposed = daemonSaysExposed ?? (maybeSessionExposed(meta.sessionId) || writes.length > 0 && judgeWritesAfterExposure({
16142
16392
  accesses: writes,
@@ -16180,7 +16430,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16180
16430
  const isKnown = (d) => {
16181
16431
  const domain = destinationDomain(d);
16182
16432
  const value = destinationValue(d);
16183
- 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));
16433
+ 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));
16184
16434
  };
16185
16435
  const approvedOnce = (key) => ucPolicy.afterExposure === "ask-once" && approved.has(key);
16186
16436
  const repos = (shellExp?.sends ? shellExp.repos : []).filter(
@@ -16244,6 +16494,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16244
16494
  );
16245
16495
  }
16246
16496
  exposureReview = `\u{1F6E1}\uFE0F Node9: this session read ${read}. This call ${parts.join("; and ")}.`;
16497
+ exposureFromEid = last?.eid;
16498
+ if (exposureFromEid) meta = { ...meta, exposureFromEid };
16247
16499
  exposureKeys = [
16248
16500
  ...held.filter((h) => h.dest || h.why === "unresolved" || h.why === "repository").map(
16249
16501
  (h) => h.why === "repository" ? `${toolName}|repo:${h.repo}` : `${toolName}|${h.dest || "unresolved"}`
@@ -16257,6 +16509,11 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16257
16509
  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("; ") + ".";
16258
16510
  }
16259
16511
  } else if (held.length > 0 || writeHolds.length > 0 || logged.length > 0) {
16512
+ meta = {
16513
+ ...meta,
16514
+ exposure: { kind: "logged", destinations: logged.length },
16515
+ ...last?.eid ? { exposureFromEid: last.eid } : {}
16516
+ };
16260
16517
  appendToLog(HOOK_DEBUG_LOG, {
16261
16518
  ts: (/* @__PURE__ */ new Date()).toISOString(),
16262
16519
  event: "exposure-logged",
@@ -16810,6 +17067,7 @@ ${packageReview}`;
16810
17067
  // deferReview and serverKey never co-occur in production — see above.)
16811
17068
  reason: taintWarning || packageReview || exposureReview || explainableLabel || "Node9 flagged this action for review.",
16812
17069
  ...exposureReview && exposureKeys.length > 0 ? { exposureKeys } : {},
17070
+ ...exposureReview && exposureFromEid ? { exposureFromEid } : {},
16813
17071
  ruleDescription: policyRuleDescription,
16814
17072
  blockedByLabel: explainableLabel
16815
17073
  };