@node9/proxy 2.16.1 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2089,30 +2089,213 @@ function analyzeShellCommand(command) {
2089
2089
  }
2090
2090
  return { actions, paths, allTokens };
2091
2091
  }
2092
+ function parseComponent(s) {
2093
+ if (!s) return null;
2094
+ if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
2095
+ if (s === "0") return 0;
2096
+ if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
2097
+ if (/^[1-9][0-9]*$/.test(s)) return Number(s);
2098
+ return null;
2099
+ }
2100
+ function parseIpv4(input) {
2101
+ let s = input;
2102
+ if (s.endsWith(".")) s = s.slice(0, -1);
2103
+ if (!s) return null;
2104
+ const parts = s.split(".");
2105
+ if (parts.length > 4) return null;
2106
+ const vals = [];
2107
+ for (const p of parts) {
2108
+ const v = parseComponent(p);
2109
+ if (v === null || !Number.isFinite(v) || v < 0) return null;
2110
+ vals.push(v);
2111
+ }
2112
+ const n = vals.length;
2113
+ for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
2114
+ const last = vals[n - 1];
2115
+ const remainingBytes = 4 - (n - 1);
2116
+ const limit = Math.pow(256, remainingBytes);
2117
+ if (last >= limit) return null;
2118
+ let value = last;
2119
+ for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
2120
+ if (value > 4294967295) return null;
2121
+ return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
2122
+ }
2123
+ function expandIpv6(input) {
2124
+ const s = input.toLowerCase();
2125
+ if (!/^[0-9a-f:.]+$/.test(s)) return null;
2126
+ if ((s.match(/::/g) ?? []).length > 1) return null;
2127
+ let head = s;
2128
+ let tailV4 = null;
2129
+ const lastColon = s.lastIndexOf(":");
2130
+ const afterLast = s.slice(lastColon + 1);
2131
+ if (afterLast.includes(".")) {
2132
+ const dotted = parseIpv4(afterLast);
2133
+ if (!dotted) return null;
2134
+ const o = dotted.split(".").map(Number);
2135
+ tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
2136
+ head = s.slice(0, lastColon + 1) + "0";
2137
+ }
2138
+ const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
2139
+ const toGroups = (part) => {
2140
+ if (!part) return [];
2141
+ const out = [];
2142
+ for (const g of part.split(":")) {
2143
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
2144
+ out.push(parseInt(g, 16));
2145
+ }
2146
+ return out;
2147
+ };
2148
+ const left = toGroups(lhs);
2149
+ if (left === null) return null;
2150
+ let right = [];
2151
+ if (rhs !== null) {
2152
+ const r = toGroups(rhs);
2153
+ if (r === null) return null;
2154
+ right = r;
2155
+ }
2156
+ if (tailV4) {
2157
+ if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
2158
+ else left.splice(left.length - 1, 1, ...tailV4);
2159
+ }
2160
+ const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
2161
+ if (rhs === null && groups.length !== 8) return null;
2162
+ if (rhs !== null && left.length + right.length > 8) return null;
2163
+ if (groups.length !== 8) return null;
2164
+ return groups;
2165
+ }
2166
+ function compressIpv6(g) {
2167
+ let bestStart = -1;
2168
+ let bestLen = 0;
2169
+ let i = 0;
2170
+ while (i < 8) {
2171
+ if (g[i] !== 0) {
2172
+ i++;
2173
+ continue;
2174
+ }
2175
+ let j = i;
2176
+ while (j < 8 && g[j] === 0) j++;
2177
+ if (j - i > bestLen) {
2178
+ bestLen = j - i;
2179
+ bestStart = i;
2180
+ }
2181
+ i = j;
2182
+ }
2183
+ const hex = g.map((x) => x.toString(16));
2184
+ if (bestLen < 2) return hex.join(":");
2185
+ return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
2186
+ }
2187
+ function normalizeIpLiteral(host) {
2188
+ try {
2189
+ if (typeof host !== "string") return null;
2190
+ let s = host.trim();
2191
+ if (!s || s.length > SSRF_MAX_HOST) return null;
2192
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
2193
+ const zone = s.indexOf("%");
2194
+ if (zone >= 0) s = s.slice(0, zone);
2195
+ if (!s) return null;
2196
+ if (s.includes(":")) {
2197
+ const g = expandIpv6(s);
2198
+ if (!g) return null;
2199
+ const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
2200
+ if (mapped) {
2201
+ return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
2202
+ }
2203
+ return compressIpv6(g);
2204
+ }
2205
+ return parseIpv4(s);
2206
+ } catch {
2207
+ return null;
2208
+ }
2209
+ }
2210
+ function isStrictGatedTier(tier) {
2211
+ return STRICT_TIERS.has(tier);
2212
+ }
2213
+ function ssrfReason(m, asWritten) {
2214
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2215
+ }
2216
+ function classifySsrf(host) {
2217
+ try {
2218
+ if (typeof host !== "string" || !host) return null;
2219
+ const lower = host.trim().toLowerCase().replace(/\.$/, "");
2220
+ const ip = normalizeIpLiteral(host);
2221
+ if (ip === null) {
2222
+ return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
2223
+ }
2224
+ const hit = (tier, overridable) => ({
2225
+ tier,
2226
+ overridable,
2227
+ kind: "address",
2228
+ normalized: ip
2229
+ });
2230
+ if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2231
+ const o = v4Octets(ip);
2232
+ if (o) {
2233
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2234
+ if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2235
+ if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2236
+ if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
2237
+ if (o[0] === 127) return hit("private", true);
2238
+ if (o[0] === 10) return hit("private", true);
2239
+ if (o[0] === 192 && o[1] === 168) return hit("private", true);
2240
+ if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
2241
+ return null;
2242
+ }
2243
+ const g = expandIpv6(ip);
2244
+ if (!g) return null;
2245
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
2246
+ if ((g[0] & 65472) === 65152) return hit("link-local", false);
2247
+ if ((g[0] & 65280) === 65280) return hit("multicast", false);
2248
+ if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
2249
+ return null;
2250
+ } catch {
2251
+ return null;
2252
+ }
2253
+ }
2254
+ function ssrfFloor(tokens, opts = {}) {
2255
+ const exempt2 = new Set(
2256
+ (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
2257
+ );
2258
+ for (const { token, binary } of tokens) {
2259
+ const m = classifySsrf(token);
2260
+ if (!m) continue;
2261
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2262
+ if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2263
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2264
+ }
2265
+ return null;
2266
+ }
2092
2267
  function hostMatches(host, pattern) {
2093
- const h = host.toLowerCase();
2094
- const p = pattern.toLowerCase().trim();
2268
+ const p = pattern.trim().toLowerCase();
2095
2269
  if (!p) return false;
2096
2270
  if (p === "*") return true;
2271
+ const h = canonicalHost(host);
2097
2272
  if (p.startsWith("*.")) {
2098
- const suffix = p.slice(2);
2273
+ const suffix = canonicalHost(p.slice(2));
2099
2274
  return h === suffix || h.endsWith("." + suffix);
2100
2275
  }
2101
- return h === p;
2276
+ return h === canonicalHost(p);
2102
2277
  }
2103
2278
  function matchesAny(host, patterns) {
2104
- for (const p of patterns) if (hostMatches(host, p)) return true;
2279
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
2105
2280
  return false;
2106
2281
  }
2282
+ function isUniqueLocalV6(host) {
2283
+ const ip = normalizeIpLiteral(host);
2284
+ if (!ip || !ip.includes(":")) return false;
2285
+ const g = expandIpv6(ip);
2286
+ return g !== null && (g[0] & 65024) === 64512;
2287
+ }
2107
2288
  function isPrivateHost(host) {
2108
- const h = host.toLowerCase();
2109
- if (h === "localhost" || h === "0.0.0.0") return true;
2110
- if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
2111
- if (/^127\./.test(h)) return true;
2112
- if (/^10\./.test(h)) return true;
2113
- if (/^192\.168\./.test(h)) return true;
2114
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
2115
- return false;
2289
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
2290
+ const m = classifySsrf(h);
2291
+ if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
2292
+ if (h === "localhost") return true;
2293
+ if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
2294
+ return isUniqueLocalV6(h);
2295
+ }
2296
+ function canonicalHost(host) {
2297
+ const ip = normalizeIpLiteral(host);
2298
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
2116
2299
  }
2117
2300
  function evaluateEgress(dests, policy) {
2118
2301
  if (!policy.enabled) return null;
@@ -2332,181 +2515,6 @@ function extractAllSshHosts(tokens) {
2332
2515
  }
2333
2516
  return [...hosts].filter(Boolean);
2334
2517
  }
2335
- function parseComponent(s) {
2336
- if (!s) return null;
2337
- if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
2338
- if (s === "0") return 0;
2339
- if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
2340
- if (/^[1-9][0-9]*$/.test(s)) return Number(s);
2341
- return null;
2342
- }
2343
- function parseIpv4(input) {
2344
- let s = input;
2345
- if (s.endsWith(".")) s = s.slice(0, -1);
2346
- if (!s) return null;
2347
- const parts = s.split(".");
2348
- if (parts.length > 4) return null;
2349
- const vals = [];
2350
- for (const p of parts) {
2351
- const v = parseComponent(p);
2352
- if (v === null || !Number.isFinite(v) || v < 0) return null;
2353
- vals.push(v);
2354
- }
2355
- const n = vals.length;
2356
- for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
2357
- const last = vals[n - 1];
2358
- const remainingBytes = 4 - (n - 1);
2359
- const limit = Math.pow(256, remainingBytes);
2360
- if (last >= limit) return null;
2361
- let value = last;
2362
- for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
2363
- if (value > 4294967295) return null;
2364
- return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
2365
- }
2366
- function expandIpv6(input) {
2367
- const s = input.toLowerCase();
2368
- if (!/^[0-9a-f:.]+$/.test(s)) return null;
2369
- if ((s.match(/::/g) ?? []).length > 1) return null;
2370
- let head = s;
2371
- let tailV4 = null;
2372
- const lastColon = s.lastIndexOf(":");
2373
- const afterLast = s.slice(lastColon + 1);
2374
- if (afterLast.includes(".")) {
2375
- const dotted = parseIpv4(afterLast);
2376
- if (!dotted) return null;
2377
- const o = dotted.split(".").map(Number);
2378
- tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
2379
- head = s.slice(0, lastColon + 1) + "0";
2380
- }
2381
- const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
2382
- const toGroups = (part) => {
2383
- if (!part) return [];
2384
- const out = [];
2385
- for (const g of part.split(":")) {
2386
- if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
2387
- out.push(parseInt(g, 16));
2388
- }
2389
- return out;
2390
- };
2391
- const left = toGroups(lhs);
2392
- if (left === null) return null;
2393
- let right = [];
2394
- if (rhs !== null) {
2395
- const r = toGroups(rhs);
2396
- if (r === null) return null;
2397
- right = r;
2398
- }
2399
- if (tailV4) {
2400
- if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
2401
- else left.splice(left.length - 1, 1, ...tailV4);
2402
- }
2403
- const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
2404
- if (rhs === null && groups.length !== 8) return null;
2405
- if (rhs !== null && left.length + right.length > 8) return null;
2406
- if (groups.length !== 8) return null;
2407
- return groups;
2408
- }
2409
- function compressIpv6(g) {
2410
- let bestStart = -1;
2411
- let bestLen = 0;
2412
- let i = 0;
2413
- while (i < 8) {
2414
- if (g[i] !== 0) {
2415
- i++;
2416
- continue;
2417
- }
2418
- let j = i;
2419
- while (j < 8 && g[j] === 0) j++;
2420
- if (j - i > bestLen) {
2421
- bestLen = j - i;
2422
- bestStart = i;
2423
- }
2424
- i = j;
2425
- }
2426
- const hex = g.map((x) => x.toString(16));
2427
- if (bestLen < 2) return hex.join(":");
2428
- return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
2429
- }
2430
- function normalizeIpLiteral(host) {
2431
- try {
2432
- if (typeof host !== "string") return null;
2433
- let s = host.trim();
2434
- if (!s || s.length > SSRF_MAX_HOST) return null;
2435
- if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
2436
- const zone = s.indexOf("%");
2437
- if (zone >= 0) s = s.slice(0, zone);
2438
- if (!s) return null;
2439
- if (s.includes(":")) {
2440
- const g = expandIpv6(s);
2441
- if (!g) return null;
2442
- const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
2443
- if (mapped) {
2444
- return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
2445
- }
2446
- return compressIpv6(g);
2447
- }
2448
- return parseIpv4(s);
2449
- } catch {
2450
- return null;
2451
- }
2452
- }
2453
- function isStrictGatedTier(tier) {
2454
- return STRICT_TIERS.has(tier);
2455
- }
2456
- function ssrfReason(m, asWritten) {
2457
- return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2458
- }
2459
- function classifySsrf(host) {
2460
- try {
2461
- if (typeof host !== "string" || !host) return null;
2462
- const lower = host.trim().toLowerCase().replace(/\.$/, "");
2463
- const ip = normalizeIpLiteral(host);
2464
- if (ip === null) {
2465
- return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
2466
- }
2467
- const hit = (tier, overridable) => ({
2468
- tier,
2469
- overridable,
2470
- kind: "address",
2471
- normalized: ip
2472
- });
2473
- if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2474
- const o = v4Octets(ip);
2475
- if (o) {
2476
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2477
- if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2478
- if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2479
- if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
2480
- if (o[0] === 127) return hit("private", true);
2481
- if (o[0] === 10) return hit("private", true);
2482
- if (o[0] === 192 && o[1] === 168) return hit("private", true);
2483
- if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
2484
- return null;
2485
- }
2486
- const g = expandIpv6(ip);
2487
- if (!g) return null;
2488
- if (g.every((x) => x === 0)) return hit("unspecified", true);
2489
- if ((g[0] & 65472) === 65152) return hit("link-local", false);
2490
- if ((g[0] & 65280) === 65280) return hit("multicast", false);
2491
- if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
2492
- return null;
2493
- } catch {
2494
- return null;
2495
- }
2496
- }
2497
- function ssrfFloor(tokens, opts = {}) {
2498
- const exempt2 = new Set(
2499
- (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
2500
- );
2501
- for (const { token, binary } of tokens) {
2502
- const m = classifySsrf(token);
2503
- if (!m) continue;
2504
- if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2505
- if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2506
- return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2507
- }
2508
- return null;
2509
- }
2510
2518
  function bareToolName(toolName) {
2511
2519
  const parts = toolName.split("__");
2512
2520
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
@@ -2540,6 +2548,22 @@ function hostOf(value) {
2540
2548
  return null;
2541
2549
  }
2542
2550
  }
2551
+ function extractToolDestinations(toolName, args) {
2552
+ try {
2553
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
2554
+ if (!paths) return [];
2555
+ const out = [];
2556
+ for (const path78 of paths) {
2557
+ for (const value of valuesAt(args, path78)) {
2558
+ const host = hostOf(value);
2559
+ if (host) out.push({ host, binary: toolName, raw: value });
2560
+ }
2561
+ }
2562
+ return out;
2563
+ } catch {
2564
+ return [];
2565
+ }
2566
+ }
2543
2567
  function ssrfDestinationFloor(toolName, args, opts = {}) {
2544
2568
  try {
2545
2569
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -2641,6 +2665,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2641
2665
  tier: 3
2642
2666
  };
2643
2667
  }
2668
+ function egressPolicyVerdict(eg) {
2669
+ return {
2670
+ decision: eg.verdict,
2671
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2672
+ reason: eg.reason,
2673
+ ruleName: `egress:${eg.binary}:${eg.host}`,
2674
+ ruleDescription: eg.reason,
2675
+ tier: eg.verdict === "block" ? 3 : 4
2676
+ };
2677
+ }
2644
2678
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
2645
2679
  const { agent, cwd, activeEnvironment } = context;
2646
2680
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -2675,7 +2709,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2675
2709
  };
2676
2710
  }
2677
2711
  }
2678
- if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2712
+ const pendingToolEgress = config.policy.egress?.enabled ? (() => {
2713
+ const dests = extractToolDestinations(toolName, args);
2714
+ const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
2715
+ return eg ? egressPolicyVerdict(eg) : void 0;
2716
+ })() : void 0;
2717
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) {
2718
+ return pendingToolEgress ?? { decision: "allow" };
2719
+ }
2679
2720
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2680
2721
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2681
2722
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -2764,6 +2805,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2764
2805
  }
2765
2806
  let allTokens = [];
2766
2807
  let pathTokens = [];
2808
+ if (pendingToolEgress) return pendingToolEgress;
2767
2809
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
2768
2810
  if (shellCommand) {
2769
2811
  const analyzed = analyzeShellCommand(shellCommand);
@@ -2829,16 +2871,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2829
2871
  const dests = extractShellDestinations(shellCommand);
2830
2872
  if (dests.length > 0) {
2831
2873
  const eg = evaluateEgress(dests, config.policy.egress);
2832
- if (eg) {
2833
- return {
2834
- decision: eg.verdict,
2835
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2836
- reason: eg.reason,
2837
- ruleName: `egress:${eg.binary}:${eg.host}`,
2838
- ruleDescription: eg.reason,
2839
- tier: eg.verdict === "block" ? 3 : 4
2840
- };
2841
- }
2874
+ if (eg) return egressPolicyVerdict(eg);
2842
2875
  }
2843
2876
  }
2844
2877
  const firstToken = analyzed.actions[0] ?? "";
@@ -3712,7 +3745,7 @@ function* stringValues(obj, depth = 0) {
3712
3745
  }
3713
3746
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3714
3747
  }
3715
- var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, 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, BASH_TOOL_NAMES, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, 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, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, TERMINAL_ESCAPE_RE, CONTROL_CHAR_RE, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
3748
+ var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, 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, BASH_TOOL_NAMES, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, 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, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, TERMINAL_ESCAPE_RE, CONTROL_CHAR_RE, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
3716
3749
  var init_dist = __esm({
3717
3750
  "packages/policy-engine/dist/index.mjs"() {
3718
3751
  "use strict";
@@ -5157,6 +5190,36 @@ var init_dist = __esm({
5157
5190
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
5158
5191
  NONE = { kind: "none" };
5159
5192
  UNKNOWN = { kind: "unknown" };
5193
+ SSRF_MAX_HOST = 253;
5194
+ METADATA_ADDRESSES = /* @__PURE__ */ new Set([
5195
+ "169.254.169.254",
5196
+ // AWS / Azure / DigitalOcean / OpenStack IMDS
5197
+ "169.254.170.2",
5198
+ // AWS ECS task role
5199
+ "168.63.129.16",
5200
+ // Azure WireServer
5201
+ "fd00:ec2::254",
5202
+ // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
5203
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
5204
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
5205
+ // live credential endpoint. It has to be named here, above the range check,
5206
+ // and it is the reason relaxing cgnat is safe.
5207
+ "100.100.100.200"
5208
+ ]);
5209
+ STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
5210
+ METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
5211
+ v4Octets = (a) => {
5212
+ const p = a.split(".");
5213
+ return p.length === 4 ? p.map(Number) : null;
5214
+ };
5215
+ TIER_REASON = {
5216
+ metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
5217
+ "link-local": "a link-local address",
5218
+ multicast: "a multicast address",
5219
+ unspecified: "the unspecified address, which reaches this host",
5220
+ cgnat: "a carrier-grade NAT address",
5221
+ private: "a loopback or private address"
5222
+ };
5160
5223
  DEFAULT_EGRESS_ALLOWLIST = [
5161
5224
  // node9's own control plane (api, app, dev-api, staging and the apex).
5162
5225
  // Without it, turning egress on asks the user to approve node9 itself.
@@ -5180,6 +5243,7 @@ var init_dist = __esm({
5180
5243
  "deb.debian.org",
5181
5244
  "*.ubuntu.com"
5182
5245
  ];
5246
+ PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
5183
5247
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
5184
5248
  SINK_COMMANDS = /* @__PURE__ */ new Set([
5185
5249
  "curl",
@@ -5303,36 +5367,6 @@ var init_dist = __esm({
5303
5367
  socat: /* @__PURE__ */ new Set([])
5304
5368
  // socat uses address syntax, not flags — no value-flags
5305
5369
  };
5306
- SSRF_MAX_HOST = 253;
5307
- METADATA_ADDRESSES = /* @__PURE__ */ new Set([
5308
- "169.254.169.254",
5309
- // AWS / Azure / DigitalOcean / OpenStack IMDS
5310
- "169.254.170.2",
5311
- // AWS ECS task role
5312
- "168.63.129.16",
5313
- // Azure WireServer
5314
- "fd00:ec2::254",
5315
- // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
5316
- // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
5317
- // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
5318
- // live credential endpoint. It has to be named here, above the range check,
5319
- // and it is the reason relaxing cgnat is safe.
5320
- "100.100.100.200"
5321
- ]);
5322
- STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
5323
- METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
5324
- v4Octets = (a) => {
5325
- const p = a.split(".");
5326
- return p.length === 4 ? p.map(Number) : null;
5327
- };
5328
- TIER_REASON = {
5329
- metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
5330
- "link-local": "a link-local address",
5331
- multicast: "a multicast address",
5332
- unspecified: "the unspecified address, which reaches this host",
5333
- cgnat: "a carrier-grade NAT address",
5334
- private: "a loopback or private address"
5335
- };
5336
5370
  DESTINATION_ARGS = /* @__PURE__ */ new Map([
5337
5371
  ["webfetch", ["url"]],
5338
5372
  ["fetch", ["url", "uri"]],
@@ -9940,8 +9974,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9940
9974
  };
9941
9975
  }
9942
9976
  }
9977
+ const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
9978
+ const judge = !isIgnoredTool2(toolName) || declaredEgress;
9943
9979
  if (isObserveMode) {
9944
- if (!isIgnoredTool2(toolName)) {
9980
+ if (judge) {
9945
9981
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9946
9982
  const wouldBlock = policyResult.decision === "block";
9947
9983
  if (!isManual)
@@ -9966,7 +10002,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9966
10002
  return { approved: true, checkedBy: "audit" };
9967
10003
  }
9968
10004
  if (config.settings.mode === "audit") {
9969
- if (!isIgnoredTool2(toolName)) {
10005
+ if (judge) {
9970
10006
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9971
10007
  if (policyResult.decision === "review") {
9972
10008
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
@@ -10005,9 +10041,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
10005
10041
  appPermReviewTool = bareTool;
10006
10042
  }
10007
10043
  }
10008
- if (!taintWarning && !isIgnoredTool2(toolName)) {
10044
+ if (!taintWarning && judge) {
10009
10045
  const ld = config.policy.loopDetection;
10010
- if (ld.enabled && !appPermReview) {
10046
+ if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
10011
10047
  const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
10012
10048
  if (loopResult.looping) {
10013
10049
  const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;