@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.mjs CHANGED
@@ -2100,30 +2100,213 @@ function analyzeShellCommand(command) {
2100
2100
  }
2101
2101
  return { actions, paths, allTokens };
2102
2102
  }
2103
+ function parseComponent(s) {
2104
+ if (!s) return null;
2105
+ if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
2106
+ if (s === "0") return 0;
2107
+ if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
2108
+ if (/^[1-9][0-9]*$/.test(s)) return Number(s);
2109
+ return null;
2110
+ }
2111
+ function parseIpv4(input) {
2112
+ let s = input;
2113
+ if (s.endsWith(".")) s = s.slice(0, -1);
2114
+ if (!s) return null;
2115
+ const parts = s.split(".");
2116
+ if (parts.length > 4) return null;
2117
+ const vals = [];
2118
+ for (const p of parts) {
2119
+ const v = parseComponent(p);
2120
+ if (v === null || !Number.isFinite(v) || v < 0) return null;
2121
+ vals.push(v);
2122
+ }
2123
+ const n = vals.length;
2124
+ for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
2125
+ const last = vals[n - 1];
2126
+ const remainingBytes = 4 - (n - 1);
2127
+ const limit = Math.pow(256, remainingBytes);
2128
+ if (last >= limit) return null;
2129
+ let value = last;
2130
+ for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
2131
+ if (value > 4294967295) return null;
2132
+ return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
2133
+ }
2134
+ function expandIpv6(input) {
2135
+ const s = input.toLowerCase();
2136
+ if (!/^[0-9a-f:.]+$/.test(s)) return null;
2137
+ if ((s.match(/::/g) ?? []).length > 1) return null;
2138
+ let head = s;
2139
+ let tailV4 = null;
2140
+ const lastColon = s.lastIndexOf(":");
2141
+ const afterLast = s.slice(lastColon + 1);
2142
+ if (afterLast.includes(".")) {
2143
+ const dotted = parseIpv4(afterLast);
2144
+ if (!dotted) return null;
2145
+ const o = dotted.split(".").map(Number);
2146
+ tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
2147
+ head = s.slice(0, lastColon + 1) + "0";
2148
+ }
2149
+ const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
2150
+ const toGroups = (part) => {
2151
+ if (!part) return [];
2152
+ const out = [];
2153
+ for (const g of part.split(":")) {
2154
+ if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
2155
+ out.push(parseInt(g, 16));
2156
+ }
2157
+ return out;
2158
+ };
2159
+ const left = toGroups(lhs);
2160
+ if (left === null) return null;
2161
+ let right = [];
2162
+ if (rhs !== null) {
2163
+ const r = toGroups(rhs);
2164
+ if (r === null) return null;
2165
+ right = r;
2166
+ }
2167
+ if (tailV4) {
2168
+ if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
2169
+ else left.splice(left.length - 1, 1, ...tailV4);
2170
+ }
2171
+ const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
2172
+ if (rhs === null && groups.length !== 8) return null;
2173
+ if (rhs !== null && left.length + right.length > 8) return null;
2174
+ if (groups.length !== 8) return null;
2175
+ return groups;
2176
+ }
2177
+ function compressIpv6(g) {
2178
+ let bestStart = -1;
2179
+ let bestLen = 0;
2180
+ let i = 0;
2181
+ while (i < 8) {
2182
+ if (g[i] !== 0) {
2183
+ i++;
2184
+ continue;
2185
+ }
2186
+ let j = i;
2187
+ while (j < 8 && g[j] === 0) j++;
2188
+ if (j - i > bestLen) {
2189
+ bestLen = j - i;
2190
+ bestStart = i;
2191
+ }
2192
+ i = j;
2193
+ }
2194
+ const hex = g.map((x) => x.toString(16));
2195
+ if (bestLen < 2) return hex.join(":");
2196
+ return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
2197
+ }
2198
+ function normalizeIpLiteral(host) {
2199
+ try {
2200
+ if (typeof host !== "string") return null;
2201
+ let s = host.trim();
2202
+ if (!s || s.length > SSRF_MAX_HOST) return null;
2203
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
2204
+ const zone = s.indexOf("%");
2205
+ if (zone >= 0) s = s.slice(0, zone);
2206
+ if (!s) return null;
2207
+ if (s.includes(":")) {
2208
+ const g = expandIpv6(s);
2209
+ if (!g) return null;
2210
+ const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
2211
+ if (mapped) {
2212
+ return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
2213
+ }
2214
+ return compressIpv6(g);
2215
+ }
2216
+ return parseIpv4(s);
2217
+ } catch {
2218
+ return null;
2219
+ }
2220
+ }
2221
+ function isStrictGatedTier(tier) {
2222
+ return STRICT_TIERS.has(tier);
2223
+ }
2224
+ function ssrfReason(m, asWritten) {
2225
+ return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2226
+ }
2227
+ function classifySsrf(host) {
2228
+ try {
2229
+ if (typeof host !== "string" || !host) return null;
2230
+ const lower = host.trim().toLowerCase().replace(/\.$/, "");
2231
+ const ip = normalizeIpLiteral(host);
2232
+ if (ip === null) {
2233
+ return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
2234
+ }
2235
+ const hit = (tier, overridable) => ({
2236
+ tier,
2237
+ overridable,
2238
+ kind: "address",
2239
+ normalized: ip
2240
+ });
2241
+ if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2242
+ const o = v4Octets(ip);
2243
+ if (o) {
2244
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2245
+ if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2246
+ if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2247
+ if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
2248
+ if (o[0] === 127) return hit("private", true);
2249
+ if (o[0] === 10) return hit("private", true);
2250
+ if (o[0] === 192 && o[1] === 168) return hit("private", true);
2251
+ if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
2252
+ return null;
2253
+ }
2254
+ const g = expandIpv6(ip);
2255
+ if (!g) return null;
2256
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
2257
+ if ((g[0] & 65472) === 65152) return hit("link-local", false);
2258
+ if ((g[0] & 65280) === 65280) return hit("multicast", false);
2259
+ if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
2260
+ return null;
2261
+ } catch {
2262
+ return null;
2263
+ }
2264
+ }
2265
+ function ssrfFloor(tokens, opts = {}) {
2266
+ const exempt2 = new Set(
2267
+ (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
2268
+ );
2269
+ for (const { token, binary } of tokens) {
2270
+ const m = classifySsrf(token);
2271
+ if (!m) continue;
2272
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2273
+ if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2274
+ return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2275
+ }
2276
+ return null;
2277
+ }
2103
2278
  function hostMatches(host, pattern) {
2104
- const h = host.toLowerCase();
2105
- const p = pattern.toLowerCase().trim();
2279
+ const p = pattern.trim().toLowerCase();
2106
2280
  if (!p) return false;
2107
2281
  if (p === "*") return true;
2282
+ const h = canonicalHost(host);
2108
2283
  if (p.startsWith("*.")) {
2109
- const suffix = p.slice(2);
2284
+ const suffix = canonicalHost(p.slice(2));
2110
2285
  return h === suffix || h.endsWith("." + suffix);
2111
2286
  }
2112
- return h === p;
2287
+ return h === canonicalHost(p);
2113
2288
  }
2114
2289
  function matchesAny(host, patterns) {
2115
- for (const p of patterns) if (hostMatches(host, p)) return true;
2290
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
2116
2291
  return false;
2117
2292
  }
2293
+ function isUniqueLocalV6(host) {
2294
+ const ip = normalizeIpLiteral(host);
2295
+ if (!ip || !ip.includes(":")) return false;
2296
+ const g = expandIpv6(ip);
2297
+ return g !== null && (g[0] & 65024) === 64512;
2298
+ }
2118
2299
  function isPrivateHost(host) {
2119
- const h = host.toLowerCase();
2120
- if (h === "localhost" || h === "0.0.0.0") return true;
2121
- if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
2122
- if (/^127\./.test(h)) return true;
2123
- if (/^10\./.test(h)) return true;
2124
- if (/^192\.168\./.test(h)) return true;
2125
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
2126
- return false;
2300
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
2301
+ const m = classifySsrf(h);
2302
+ if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
2303
+ if (h === "localhost") return true;
2304
+ if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
2305
+ return isUniqueLocalV6(h);
2306
+ }
2307
+ function canonicalHost(host) {
2308
+ const ip = normalizeIpLiteral(host);
2309
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
2127
2310
  }
2128
2311
  function evaluateEgress(dests, policy) {
2129
2312
  if (!policy.enabled) return null;
@@ -2343,181 +2526,6 @@ function extractAllSshHosts(tokens) {
2343
2526
  }
2344
2527
  return [...hosts].filter(Boolean);
2345
2528
  }
2346
- function parseComponent(s) {
2347
- if (!s) return null;
2348
- if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
2349
- if (s === "0") return 0;
2350
- if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
2351
- if (/^[1-9][0-9]*$/.test(s)) return Number(s);
2352
- return null;
2353
- }
2354
- function parseIpv4(input) {
2355
- let s = input;
2356
- if (s.endsWith(".")) s = s.slice(0, -1);
2357
- if (!s) return null;
2358
- const parts = s.split(".");
2359
- if (parts.length > 4) return null;
2360
- const vals = [];
2361
- for (const p of parts) {
2362
- const v = parseComponent(p);
2363
- if (v === null || !Number.isFinite(v) || v < 0) return null;
2364
- vals.push(v);
2365
- }
2366
- const n = vals.length;
2367
- for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
2368
- const last = vals[n - 1];
2369
- const remainingBytes = 4 - (n - 1);
2370
- const limit = Math.pow(256, remainingBytes);
2371
- if (last >= limit) return null;
2372
- let value = last;
2373
- for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
2374
- if (value > 4294967295) return null;
2375
- return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
2376
- }
2377
- function expandIpv6(input) {
2378
- const s = input.toLowerCase();
2379
- if (!/^[0-9a-f:.]+$/.test(s)) return null;
2380
- if ((s.match(/::/g) ?? []).length > 1) return null;
2381
- let head = s;
2382
- let tailV4 = null;
2383
- const lastColon = s.lastIndexOf(":");
2384
- const afterLast = s.slice(lastColon + 1);
2385
- if (afterLast.includes(".")) {
2386
- const dotted = parseIpv4(afterLast);
2387
- if (!dotted) return null;
2388
- const o = dotted.split(".").map(Number);
2389
- tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
2390
- head = s.slice(0, lastColon + 1) + "0";
2391
- }
2392
- const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
2393
- const toGroups = (part) => {
2394
- if (!part) return [];
2395
- const out = [];
2396
- for (const g of part.split(":")) {
2397
- if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
2398
- out.push(parseInt(g, 16));
2399
- }
2400
- return out;
2401
- };
2402
- const left = toGroups(lhs);
2403
- if (left === null) return null;
2404
- let right = [];
2405
- if (rhs !== null) {
2406
- const r = toGroups(rhs);
2407
- if (r === null) return null;
2408
- right = r;
2409
- }
2410
- if (tailV4) {
2411
- if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
2412
- else left.splice(left.length - 1, 1, ...tailV4);
2413
- }
2414
- const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
2415
- if (rhs === null && groups.length !== 8) return null;
2416
- if (rhs !== null && left.length + right.length > 8) return null;
2417
- if (groups.length !== 8) return null;
2418
- return groups;
2419
- }
2420
- function compressIpv6(g) {
2421
- let bestStart = -1;
2422
- let bestLen = 0;
2423
- let i = 0;
2424
- while (i < 8) {
2425
- if (g[i] !== 0) {
2426
- i++;
2427
- continue;
2428
- }
2429
- let j = i;
2430
- while (j < 8 && g[j] === 0) j++;
2431
- if (j - i > bestLen) {
2432
- bestLen = j - i;
2433
- bestStart = i;
2434
- }
2435
- i = j;
2436
- }
2437
- const hex = g.map((x) => x.toString(16));
2438
- if (bestLen < 2) return hex.join(":");
2439
- return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
2440
- }
2441
- function normalizeIpLiteral(host) {
2442
- try {
2443
- if (typeof host !== "string") return null;
2444
- let s = host.trim();
2445
- if (!s || s.length > SSRF_MAX_HOST) return null;
2446
- if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
2447
- const zone = s.indexOf("%");
2448
- if (zone >= 0) s = s.slice(0, zone);
2449
- if (!s) return null;
2450
- if (s.includes(":")) {
2451
- const g = expandIpv6(s);
2452
- if (!g) return null;
2453
- const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
2454
- if (mapped) {
2455
- return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
2456
- }
2457
- return compressIpv6(g);
2458
- }
2459
- return parseIpv4(s);
2460
- } catch {
2461
- return null;
2462
- }
2463
- }
2464
- function isStrictGatedTier(tier) {
2465
- return STRICT_TIERS.has(tier);
2466
- }
2467
- function ssrfReason(m, asWritten) {
2468
- return `Blocked: ${asWritten} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== asWritten ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.");
2469
- }
2470
- function classifySsrf(host) {
2471
- try {
2472
- if (typeof host !== "string" || !host) return null;
2473
- const lower = host.trim().toLowerCase().replace(/\.$/, "");
2474
- const ip = normalizeIpLiteral(host);
2475
- if (ip === null) {
2476
- return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
2477
- }
2478
- const hit = (tier, overridable) => ({
2479
- tier,
2480
- overridable,
2481
- kind: "address",
2482
- normalized: ip
2483
- });
2484
- if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2485
- const o = v4Octets(ip);
2486
- if (o) {
2487
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2488
- if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2489
- if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2490
- if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
2491
- if (o[0] === 127) return hit("private", true);
2492
- if (o[0] === 10) return hit("private", true);
2493
- if (o[0] === 192 && o[1] === 168) return hit("private", true);
2494
- if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
2495
- return null;
2496
- }
2497
- const g = expandIpv6(ip);
2498
- if (!g) return null;
2499
- if (g.every((x) => x === 0)) return hit("unspecified", true);
2500
- if ((g[0] & 65472) === 65152) return hit("link-local", false);
2501
- if ((g[0] & 65280) === 65280) return hit("multicast", false);
2502
- if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
2503
- return null;
2504
- } catch {
2505
- return null;
2506
- }
2507
- }
2508
- function ssrfFloor(tokens, opts = {}) {
2509
- const exempt2 = new Set(
2510
- (opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
2511
- );
2512
- for (const { token, binary } of tokens) {
2513
- const m = classifySsrf(token);
2514
- if (!m) continue;
2515
- if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
2516
- if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2517
- return { ...m, host: token, binary, reason: ssrfReason(m, token) };
2518
- }
2519
- return null;
2520
- }
2521
2529
  function bareToolName(toolName) {
2522
2530
  const parts = toolName.split("__");
2523
2531
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
@@ -2551,6 +2559,22 @@ function hostOf(value) {
2551
2559
  return null;
2552
2560
  }
2553
2561
  }
2562
+ function extractToolDestinations(toolName, args) {
2563
+ try {
2564
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
2565
+ if (!paths) return [];
2566
+ const out = [];
2567
+ for (const path78 of paths) {
2568
+ for (const value of valuesAt(args, path78)) {
2569
+ const host = hostOf(value);
2570
+ if (host) out.push({ host, binary: toolName, raw: value });
2571
+ }
2572
+ }
2573
+ return out;
2574
+ } catch {
2575
+ return [];
2576
+ }
2577
+ }
2554
2578
  function ssrfDestinationFloor(toolName, args, opts = {}) {
2555
2579
  try {
2556
2580
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -2652,6 +2676,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2652
2676
  tier: 3
2653
2677
  };
2654
2678
  }
2679
+ function egressPolicyVerdict(eg) {
2680
+ return {
2681
+ decision: eg.verdict,
2682
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2683
+ reason: eg.reason,
2684
+ ruleName: `egress:${eg.binary}:${eg.host}`,
2685
+ ruleDescription: eg.reason,
2686
+ tier: eg.verdict === "block" ? 3 : 4
2687
+ };
2688
+ }
2655
2689
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
2656
2690
  const { agent, cwd, activeEnvironment } = context;
2657
2691
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -2686,7 +2720,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2686
2720
  };
2687
2721
  }
2688
2722
  }
2689
- if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2723
+ const pendingToolEgress = config.policy.egress?.enabled ? (() => {
2724
+ const dests = extractToolDestinations(toolName, args);
2725
+ const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
2726
+ return eg ? egressPolicyVerdict(eg) : void 0;
2727
+ })() : void 0;
2728
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) {
2729
+ return pendingToolEgress ?? { decision: "allow" };
2730
+ }
2690
2731
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2691
2732
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2692
2733
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -2775,6 +2816,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2775
2816
  }
2776
2817
  let allTokens = [];
2777
2818
  let pathTokens = [];
2819
+ if (pendingToolEgress) return pendingToolEgress;
2778
2820
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
2779
2821
  if (shellCommand) {
2780
2822
  const analyzed = analyzeShellCommand(shellCommand);
@@ -2840,16 +2882,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2840
2882
  const dests = extractShellDestinations(shellCommand);
2841
2883
  if (dests.length > 0) {
2842
2884
  const eg = evaluateEgress(dests, config.policy.egress);
2843
- if (eg) {
2844
- return {
2845
- decision: eg.verdict,
2846
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2847
- reason: eg.reason,
2848
- ruleName: `egress:${eg.binary}:${eg.host}`,
2849
- ruleDescription: eg.reason,
2850
- tier: eg.verdict === "block" ? 3 : 4
2851
- };
2852
- }
2885
+ if (eg) return egressPolicyVerdict(eg);
2853
2886
  }
2854
2887
  }
2855
2888
  const firstToken = analyzed.actions[0] ?? "";
@@ -3723,7 +3756,7 @@ function* stringValues(obj, depth = 0) {
3723
3756
  }
3724
3757
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3725
3758
  }
3726
- var 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;
3759
+ var 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;
3727
3760
  var init_dist = __esm({
3728
3761
  "packages/policy-engine/dist/index.mjs"() {
3729
3762
  "use strict";
@@ -5161,6 +5194,36 @@ var init_dist = __esm({
5161
5194
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
5162
5195
  NONE = { kind: "none" };
5163
5196
  UNKNOWN = { kind: "unknown" };
5197
+ SSRF_MAX_HOST = 253;
5198
+ METADATA_ADDRESSES = /* @__PURE__ */ new Set([
5199
+ "169.254.169.254",
5200
+ // AWS / Azure / DigitalOcean / OpenStack IMDS
5201
+ "169.254.170.2",
5202
+ // AWS ECS task role
5203
+ "168.63.129.16",
5204
+ // Azure WireServer
5205
+ "fd00:ec2::254",
5206
+ // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
5207
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
5208
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
5209
+ // live credential endpoint. It has to be named here, above the range check,
5210
+ // and it is the reason relaxing cgnat is safe.
5211
+ "100.100.100.200"
5212
+ ]);
5213
+ STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
5214
+ METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
5215
+ v4Octets = (a) => {
5216
+ const p = a.split(".");
5217
+ return p.length === 4 ? p.map(Number) : null;
5218
+ };
5219
+ TIER_REASON = {
5220
+ metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
5221
+ "link-local": "a link-local address",
5222
+ multicast: "a multicast address",
5223
+ unspecified: "the unspecified address, which reaches this host",
5224
+ cgnat: "a carrier-grade NAT address",
5225
+ private: "a loopback or private address"
5226
+ };
5164
5227
  DEFAULT_EGRESS_ALLOWLIST = [
5165
5228
  // node9's own control plane (api, app, dev-api, staging and the apex).
5166
5229
  // Without it, turning egress on asks the user to approve node9 itself.
@@ -5184,6 +5247,7 @@ var init_dist = __esm({
5184
5247
  "deb.debian.org",
5185
5248
  "*.ubuntu.com"
5186
5249
  ];
5250
+ PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
5187
5251
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
5188
5252
  SINK_COMMANDS = /* @__PURE__ */ new Set([
5189
5253
  "curl",
@@ -5307,36 +5371,6 @@ var init_dist = __esm({
5307
5371
  socat: /* @__PURE__ */ new Set([])
5308
5372
  // socat uses address syntax, not flags — no value-flags
5309
5373
  };
5310
- SSRF_MAX_HOST = 253;
5311
- METADATA_ADDRESSES = /* @__PURE__ */ new Set([
5312
- "169.254.169.254",
5313
- // AWS / Azure / DigitalOcean / OpenStack IMDS
5314
- "169.254.170.2",
5315
- // AWS ECS task role
5316
- "168.63.129.16",
5317
- // Azure WireServer
5318
- "fd00:ec2::254",
5319
- // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
5320
- // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
5321
- // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
5322
- // live credential endpoint. It has to be named here, above the range check,
5323
- // and it is the reason relaxing cgnat is safe.
5324
- "100.100.100.200"
5325
- ]);
5326
- STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
5327
- METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
5328
- v4Octets = (a) => {
5329
- const p = a.split(".");
5330
- return p.length === 4 ? p.map(Number) : null;
5331
- };
5332
- TIER_REASON = {
5333
- metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
5334
- "link-local": "a link-local address",
5335
- multicast: "a multicast address",
5336
- unspecified: "the unspecified address, which reaches this host",
5337
- cgnat: "a carrier-grade NAT address",
5338
- private: "a loopback or private address"
5339
- };
5340
5374
  DESTINATION_ARGS = /* @__PURE__ */ new Map([
5341
5375
  ["webfetch", ["url"]],
5342
5376
  ["fetch", ["url", "uri"]],
@@ -9941,8 +9975,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9941
9975
  };
9942
9976
  }
9943
9977
  }
9978
+ const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
9979
+ const judge = !isIgnoredTool2(toolName) || declaredEgress;
9944
9980
  if (isObserveMode) {
9945
- if (!isIgnoredTool2(toolName)) {
9981
+ if (judge) {
9946
9982
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9947
9983
  const wouldBlock = policyResult.decision === "block";
9948
9984
  if (!isManual)
@@ -9967,7 +10003,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9967
10003
  return { approved: true, checkedBy: "audit" };
9968
10004
  }
9969
10005
  if (config.settings.mode === "audit") {
9970
- if (!isIgnoredTool2(toolName)) {
10006
+ if (judge) {
9971
10007
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9972
10008
  if (policyResult.decision === "review") {
9973
10009
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
@@ -10006,9 +10042,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
10006
10042
  appPermReviewTool = bareTool;
10007
10043
  }
10008
10044
  }
10009
- if (!taintWarning && !isIgnoredTool2(toolName)) {
10045
+ if (!taintWarning && judge) {
10010
10046
  const ld = config.policy.loopDetection;
10011
- if (ld.enabled && !appPermReview) {
10047
+ if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
10012
10048
  const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
10013
10049
  if (loopResult.looping) {
10014
10050
  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?`;
@@ -1944,7 +1944,7 @@ function matchCanaryArgs(args, values) {
1944
1944
  return null;
1945
1945
  }
1946
1946
  }
1947
- var B58, B58_INDEX, XPRV_VERSIONS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, 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, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, SOURCE_COMMANDS, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, TERMINAL_ESCAPE_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, 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;
1947
+ var B58, B58_INDEX, XPRV_VERSIONS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, 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, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, SOURCE_COMMANDS, TERMINAL_ESCAPE_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, 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;
1948
1948
  var init_dist = __esm({
1949
1949
  "packages/policy-engine/dist/index.mjs"() {
1950
1950
  "use strict";
@@ -3136,7 +3136,6 @@ var init_dist = __esm({
3136
3136
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
3137
3137
  NONE = { kind: "none" };
3138
3138
  UNKNOWN = { kind: "unknown" };
3139
- SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
3140
3139
  SSRF_MAX_HOST = 253;
3141
3140
  METADATA_ADDRESSES = /* @__PURE__ */ new Set([
3142
3141
  "169.254.169.254",
@@ -3158,6 +3157,7 @@ var init_dist = __esm({
3158
3157
  const p = a.split(".");
3159
3158
  return p.length === 4 ? p.map(Number) : null;
3160
3159
  };
3160
+ SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
3161
3161
  TERMINAL_ESCAPE_RE = /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
3162
3162
  aws_default = {
3163
3163
  name: "aws",