@node9/proxy 2.16.2 → 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
@@ -2265,18 +2265,18 @@ function ssrfFloor(tokens, opts = {}) {
2265
2265
  return null;
2266
2266
  }
2267
2267
  function hostMatches(host, pattern) {
2268
- const h = host.toLowerCase();
2269
- const p = pattern.toLowerCase().trim();
2268
+ const p = pattern.trim().toLowerCase();
2270
2269
  if (!p) return false;
2271
2270
  if (p === "*") return true;
2271
+ const h = canonicalHost(host);
2272
2272
  if (p.startsWith("*.")) {
2273
- const suffix = p.slice(2);
2273
+ const suffix = canonicalHost(p.slice(2));
2274
2274
  return h === suffix || h.endsWith("." + suffix);
2275
2275
  }
2276
- return h === p;
2276
+ return h === canonicalHost(p);
2277
2277
  }
2278
2278
  function matchesAny(host, patterns) {
2279
- for (const p of patterns) if (hostMatches(host, p)) return true;
2279
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
2280
2280
  return false;
2281
2281
  }
2282
2282
  function isUniqueLocalV6(host) {
@@ -2286,13 +2286,17 @@ function isUniqueLocalV6(host) {
2286
2286
  return g !== null && (g[0] & 65024) === 64512;
2287
2287
  }
2288
2288
  function isPrivateHost(host) {
2289
- const h = host.trim().toLowerCase();
2289
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
2290
2290
  const m = classifySsrf(h);
2291
2291
  if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
2292
2292
  if (h === "localhost") return true;
2293
2293
  if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
2294
2294
  return isUniqueLocalV6(h);
2295
2295
  }
2296
+ function canonicalHost(host) {
2297
+ const ip = normalizeIpLiteral(host);
2298
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
2299
+ }
2296
2300
  function evaluateEgress(dests, policy) {
2297
2301
  if (!policy.enabled) return null;
2298
2302
  let review = null;
@@ -2544,6 +2548,22 @@ function hostOf(value) {
2544
2548
  return null;
2545
2549
  }
2546
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
+ }
2547
2567
  function ssrfDestinationFloor(toolName, args, opts = {}) {
2548
2568
  try {
2549
2569
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -2645,6 +2665,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2645
2665
  tier: 3
2646
2666
  };
2647
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
+ }
2648
2678
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
2649
2679
  const { agent, cwd, activeEnvironment } = context;
2650
2680
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -2679,7 +2709,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2679
2709
  };
2680
2710
  }
2681
2711
  }
2682
- 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
+ }
2683
2720
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2684
2721
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2685
2722
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -2768,6 +2805,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2768
2805
  }
2769
2806
  let allTokens = [];
2770
2807
  let pathTokens = [];
2808
+ if (pendingToolEgress) return pendingToolEgress;
2771
2809
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
2772
2810
  if (shellCommand) {
2773
2811
  const analyzed = analyzeShellCommand(shellCommand);
@@ -2833,16 +2871,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2833
2871
  const dests = extractShellDestinations(shellCommand);
2834
2872
  if (dests.length > 0) {
2835
2873
  const eg = evaluateEgress(dests, config.policy.egress);
2836
- if (eg) {
2837
- return {
2838
- decision: eg.verdict,
2839
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2840
- reason: eg.reason,
2841
- ruleName: `egress:${eg.binary}:${eg.host}`,
2842
- ruleDescription: eg.reason,
2843
- tier: eg.verdict === "block" ? 3 : 4
2844
- };
2845
- }
2874
+ if (eg) return egressPolicyVerdict(eg);
2846
2875
  }
2847
2876
  }
2848
2877
  const firstToken = analyzed.actions[0] ?? "";
@@ -9945,8 +9974,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9945
9974
  };
9946
9975
  }
9947
9976
  }
9977
+ const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
9978
+ const judge = !isIgnoredTool2(toolName) || declaredEgress;
9948
9979
  if (isObserveMode) {
9949
- if (!isIgnoredTool2(toolName)) {
9980
+ if (judge) {
9950
9981
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9951
9982
  const wouldBlock = policyResult.decision === "block";
9952
9983
  if (!isManual)
@@ -9971,7 +10002,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9971
10002
  return { approved: true, checkedBy: "audit" };
9972
10003
  }
9973
10004
  if (config.settings.mode === "audit") {
9974
- if (!isIgnoredTool2(toolName)) {
10005
+ if (judge) {
9975
10006
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9976
10007
  if (policyResult.decision === "review") {
9977
10008
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
@@ -10010,9 +10041,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
10010
10041
  appPermReviewTool = bareTool;
10011
10042
  }
10012
10043
  }
10013
- if (!taintWarning && !isIgnoredTool2(toolName)) {
10044
+ if (!taintWarning && judge) {
10014
10045
  const ld = config.policy.loopDetection;
10015
- if (ld.enabled && !appPermReview) {
10046
+ if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
10016
10047
  const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
10017
10048
  if (loopResult.looping) {
10018
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?`;
package/dist/cli.mjs CHANGED
@@ -2276,18 +2276,18 @@ function ssrfFloor(tokens, opts = {}) {
2276
2276
  return null;
2277
2277
  }
2278
2278
  function hostMatches(host, pattern) {
2279
- const h = host.toLowerCase();
2280
- const p = pattern.toLowerCase().trim();
2279
+ const p = pattern.trim().toLowerCase();
2281
2280
  if (!p) return false;
2282
2281
  if (p === "*") return true;
2282
+ const h = canonicalHost(host);
2283
2283
  if (p.startsWith("*.")) {
2284
- const suffix = p.slice(2);
2284
+ const suffix = canonicalHost(p.slice(2));
2285
2285
  return h === suffix || h.endsWith("." + suffix);
2286
2286
  }
2287
- return h === p;
2287
+ return h === canonicalHost(p);
2288
2288
  }
2289
2289
  function matchesAny(host, patterns) {
2290
- for (const p of patterns) if (hostMatches(host, p)) return true;
2290
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
2291
2291
  return false;
2292
2292
  }
2293
2293
  function isUniqueLocalV6(host) {
@@ -2297,13 +2297,17 @@ function isUniqueLocalV6(host) {
2297
2297
  return g !== null && (g[0] & 65024) === 64512;
2298
2298
  }
2299
2299
  function isPrivateHost(host) {
2300
- const h = host.trim().toLowerCase();
2300
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
2301
2301
  const m = classifySsrf(h);
2302
2302
  if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
2303
2303
  if (h === "localhost") return true;
2304
2304
  if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
2305
2305
  return isUniqueLocalV6(h);
2306
2306
  }
2307
+ function canonicalHost(host) {
2308
+ const ip = normalizeIpLiteral(host);
2309
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
2310
+ }
2307
2311
  function evaluateEgress(dests, policy) {
2308
2312
  if (!policy.enabled) return null;
2309
2313
  let review = null;
@@ -2555,6 +2559,22 @@ function hostOf(value) {
2555
2559
  return null;
2556
2560
  }
2557
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
+ }
2558
2578
  function ssrfDestinationFloor(toolName, args, opts = {}) {
2559
2579
  try {
2560
2580
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -2656,6 +2676,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2656
2676
  tier: 3
2657
2677
  };
2658
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
+ }
2659
2689
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
2660
2690
  const { agent, cwd, activeEnvironment } = context;
2661
2691
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -2690,7 +2720,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2690
2720
  };
2691
2721
  }
2692
2722
  }
2693
- 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
+ }
2694
2731
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2695
2732
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2696
2733
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -2779,6 +2816,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2779
2816
  }
2780
2817
  let allTokens = [];
2781
2818
  let pathTokens = [];
2819
+ if (pendingToolEgress) return pendingToolEgress;
2782
2820
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
2783
2821
  if (shellCommand) {
2784
2822
  const analyzed = analyzeShellCommand(shellCommand);
@@ -2844,16 +2882,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2844
2882
  const dests = extractShellDestinations(shellCommand);
2845
2883
  if (dests.length > 0) {
2846
2884
  const eg = evaluateEgress(dests, config.policy.egress);
2847
- if (eg) {
2848
- return {
2849
- decision: eg.verdict,
2850
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
2851
- reason: eg.reason,
2852
- ruleName: `egress:${eg.binary}:${eg.host}`,
2853
- ruleDescription: eg.reason,
2854
- tier: eg.verdict === "block" ? 3 : 4
2855
- };
2856
- }
2885
+ if (eg) return egressPolicyVerdict(eg);
2857
2886
  }
2858
2887
  }
2859
2888
  const firstToken = analyzed.actions[0] ?? "";
@@ -9946,8 +9975,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9946
9975
  };
9947
9976
  }
9948
9977
  }
9978
+ const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
9979
+ const judge = !isIgnoredTool2(toolName) || declaredEgress;
9949
9980
  if (isObserveMode) {
9950
- if (!isIgnoredTool2(toolName)) {
9981
+ if (judge) {
9951
9982
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9952
9983
  const wouldBlock = policyResult.decision === "block";
9953
9984
  if (!isManual)
@@ -9972,7 +10003,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
9972
10003
  return { approved: true, checkedBy: "audit" };
9973
10004
  }
9974
10005
  if (config.settings.mode === "audit") {
9975
- if (!isIgnoredTool2(toolName)) {
10006
+ if (judge) {
9976
10007
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
9977
10008
  if (policyResult.decision === "review") {
9978
10009
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
@@ -10011,9 +10042,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
10011
10042
  appPermReviewTool = bareTool;
10012
10043
  }
10013
10044
  }
10014
- if (!taintWarning && !isIgnoredTool2(toolName)) {
10045
+ if (!taintWarning && judge) {
10015
10046
  const ld = config.policy.loopDetection;
10016
- if (ld.enabled && !appPermReview) {
10047
+ if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
10017
10048
  const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
10018
10049
  if (loopResult.looping) {
10019
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?`;
package/dist/index.js CHANGED
@@ -3675,18 +3675,18 @@ var DEFAULT_EGRESS_ALLOWLIST = [
3675
3675
  "*.ubuntu.com"
3676
3676
  ];
3677
3677
  function hostMatches(host, pattern) {
3678
- const h = host.toLowerCase();
3679
- const p = pattern.toLowerCase().trim();
3678
+ const p = pattern.trim().toLowerCase();
3680
3679
  if (!p) return false;
3681
3680
  if (p === "*") return true;
3681
+ const h = canonicalHost(host);
3682
3682
  if (p.startsWith("*.")) {
3683
- const suffix = p.slice(2);
3683
+ const suffix = canonicalHost(p.slice(2));
3684
3684
  return h === suffix || h.endsWith("." + suffix);
3685
3685
  }
3686
- return h === p;
3686
+ return h === canonicalHost(p);
3687
3687
  }
3688
3688
  function matchesAny(host, patterns) {
3689
- for (const p of patterns) if (hostMatches(host, p)) return true;
3689
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
3690
3690
  return false;
3691
3691
  }
3692
3692
  var PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
@@ -3697,13 +3697,17 @@ function isUniqueLocalV6(host) {
3697
3697
  return g !== null && (g[0] & 65024) === 64512;
3698
3698
  }
3699
3699
  function isPrivateHost(host) {
3700
- const h = host.trim().toLowerCase();
3700
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
3701
3701
  const m = classifySsrf(h);
3702
3702
  if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
3703
3703
  if (h === "localhost") return true;
3704
3704
  if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
3705
3705
  return isUniqueLocalV6(h);
3706
3706
  }
3707
+ function canonicalHost(host) {
3708
+ const ip = normalizeIpLiteral(host);
3709
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
3710
+ }
3707
3711
  function evaluateEgress(dests, policy) {
3708
3712
  if (!policy.enabled) return null;
3709
3713
  let review = null;
@@ -4085,6 +4089,22 @@ function hostOf(value) {
4085
4089
  return null;
4086
4090
  }
4087
4091
  }
4092
+ function extractToolDestinations(toolName, args) {
4093
+ try {
4094
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4095
+ if (!paths) return [];
4096
+ const out = [];
4097
+ for (const path16 of paths) {
4098
+ for (const value of valuesAt(args, path16)) {
4099
+ const host = hostOf(value);
4100
+ if (host) out.push({ host, binary: toolName, raw: value });
4101
+ }
4102
+ }
4103
+ return out;
4104
+ } catch {
4105
+ return [];
4106
+ }
4107
+ }
4088
4108
  function ssrfDestinationFloor(toolName, args, opts = {}) {
4089
4109
  try {
4090
4110
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -4192,6 +4212,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
4192
4212
  tier: 3
4193
4213
  };
4194
4214
  }
4215
+ function egressPolicyVerdict(eg) {
4216
+ return {
4217
+ decision: eg.verdict,
4218
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
4219
+ reason: eg.reason,
4220
+ ruleName: `egress:${eg.binary}:${eg.host}`,
4221
+ ruleDescription: eg.reason,
4222
+ tier: eg.verdict === "block" ? 3 : 4
4223
+ };
4224
+ }
4195
4225
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
4196
4226
  const { agent, cwd, activeEnvironment } = context;
4197
4227
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -4226,7 +4256,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4226
4256
  };
4227
4257
  }
4228
4258
  }
4229
- if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
4259
+ const pendingToolEgress = config.policy.egress?.enabled ? (() => {
4260
+ const dests = extractToolDestinations(toolName, args);
4261
+ const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
4262
+ return eg ? egressPolicyVerdict(eg) : void 0;
4263
+ })() : void 0;
4264
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) {
4265
+ return pendingToolEgress ?? { decision: "allow" };
4266
+ }
4230
4267
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
4231
4268
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
4232
4269
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -4315,6 +4352,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4315
4352
  }
4316
4353
  let allTokens = [];
4317
4354
  let pathTokens = [];
4355
+ if (pendingToolEgress) return pendingToolEgress;
4318
4356
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
4319
4357
  if (shellCommand) {
4320
4358
  const analyzed = analyzeShellCommand(shellCommand);
@@ -4380,16 +4418,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4380
4418
  const dests = extractShellDestinations(shellCommand);
4381
4419
  if (dests.length > 0) {
4382
4420
  const eg = evaluateEgress(dests, config.policy.egress);
4383
- if (eg) {
4384
- return {
4385
- decision: eg.verdict,
4386
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
4387
- reason: eg.reason,
4388
- ruleName: `egress:${eg.binary}:${eg.host}`,
4389
- ruleDescription: eg.reason,
4390
- tier: eg.verdict === "block" ? 3 : 4
4391
- };
4392
- }
4421
+ if (eg) return egressPolicyVerdict(eg);
4393
4422
  }
4394
4423
  }
4395
4424
  const firstToken = analyzed.actions[0] ?? "";
@@ -8329,8 +8358,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8329
8358
  };
8330
8359
  }
8331
8360
  }
8361
+ const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
8362
+ const judge = !isIgnoredTool2(toolName) || declaredEgress;
8332
8363
  if (isObserveMode) {
8333
- if (!isIgnoredTool2(toolName)) {
8364
+ if (judge) {
8334
8365
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
8335
8366
  const wouldBlock = policyResult.decision === "block";
8336
8367
  if (!isManual)
@@ -8355,7 +8386,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8355
8386
  return { approved: true, checkedBy: "audit" };
8356
8387
  }
8357
8388
  if (config.settings.mode === "audit") {
8358
- if (!isIgnoredTool2(toolName)) {
8389
+ if (judge) {
8359
8390
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
8360
8391
  if (policyResult.decision === "review") {
8361
8392
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
@@ -8394,9 +8425,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8394
8425
  appPermReviewTool = bareTool;
8395
8426
  }
8396
8427
  }
8397
- if (!taintWarning && !isIgnoredTool2(toolName)) {
8428
+ if (!taintWarning && judge) {
8398
8429
  const ld = config.policy.loopDetection;
8399
- if (ld.enabled && !appPermReview) {
8430
+ if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
8400
8431
  const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
8401
8432
  if (loopResult.looping) {
8402
8433
  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?`;
package/dist/index.mjs CHANGED
@@ -3645,18 +3645,18 @@ var DEFAULT_EGRESS_ALLOWLIST = [
3645
3645
  "*.ubuntu.com"
3646
3646
  ];
3647
3647
  function hostMatches(host, pattern) {
3648
- const h = host.toLowerCase();
3649
- const p = pattern.toLowerCase().trim();
3648
+ const p = pattern.trim().toLowerCase();
3650
3649
  if (!p) return false;
3651
3650
  if (p === "*") return true;
3651
+ const h = canonicalHost(host);
3652
3652
  if (p.startsWith("*.")) {
3653
- const suffix = p.slice(2);
3653
+ const suffix = canonicalHost(p.slice(2));
3654
3654
  return h === suffix || h.endsWith("." + suffix);
3655
3655
  }
3656
- return h === p;
3656
+ return h === canonicalHost(p);
3657
3657
  }
3658
3658
  function matchesAny(host, patterns) {
3659
- for (const p of patterns) if (hostMatches(host, p)) return true;
3659
+ for (const p of patterns ?? []) if (hostMatches(host, p)) return true;
3660
3660
  return false;
3661
3661
  }
3662
3662
  var PRIVATE_HOST_SUFFIXES = [".local", ".internal", ".localhost"];
@@ -3667,13 +3667,17 @@ function isUniqueLocalV6(host) {
3667
3667
  return g !== null && (g[0] & 65024) === 64512;
3668
3668
  }
3669
3669
  function isPrivateHost(host) {
3670
- const h = host.trim().toLowerCase();
3670
+ const h = host.trim().toLowerCase().replace(/\.$/, "");
3671
3671
  const m = classifySsrf(h);
3672
3672
  if (m) return m.kind === "address" && (m.tier === "private" || m.tier === "unspecified");
3673
3673
  if (h === "localhost") return true;
3674
3674
  if (PRIVATE_HOST_SUFFIXES.some((s) => h.endsWith(s))) return true;
3675
3675
  return isUniqueLocalV6(h);
3676
3676
  }
3677
+ function canonicalHost(host) {
3678
+ const ip = normalizeIpLiteral(host);
3679
+ return ip ?? host.trim().toLowerCase().replace(/\.$/, "");
3680
+ }
3677
3681
  function evaluateEgress(dests, policy) {
3678
3682
  if (!policy.enabled) return null;
3679
3683
  let review = null;
@@ -4055,6 +4059,22 @@ function hostOf(value) {
4055
4059
  return null;
4056
4060
  }
4057
4061
  }
4062
+ function extractToolDestinations(toolName, args) {
4063
+ try {
4064
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4065
+ if (!paths) return [];
4066
+ const out = [];
4067
+ for (const path16 of paths) {
4068
+ for (const value of valuesAt(args, path16)) {
4069
+ const host = hostOf(value);
4070
+ if (host) out.push({ host, binary: toolName, raw: value });
4071
+ }
4072
+ }
4073
+ return out;
4074
+ } catch {
4075
+ return [];
4076
+ }
4077
+ }
4058
4078
  function ssrfDestinationFloor(toolName, args, opts = {}) {
4059
4079
  try {
4060
4080
  const paths = DESTINATION_ARGS.get(bareToolName(toolName));
@@ -4162,6 +4182,16 @@ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
4162
4182
  tier: 3
4163
4183
  };
4164
4184
  }
4185
+ function egressPolicyVerdict(eg) {
4186
+ return {
4187
+ decision: eg.verdict,
4188
+ blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
4189
+ reason: eg.reason,
4190
+ ruleName: `egress:${eg.binary}:${eg.host}`,
4191
+ ruleDescription: eg.reason,
4192
+ tier: eg.verdict === "block" ? 3 : 4
4193
+ };
4194
+ }
4165
4195
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
4166
4196
  const { agent, cwd, activeEnvironment } = context;
4167
4197
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -4196,7 +4226,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4196
4226
  };
4197
4227
  }
4198
4228
  }
4199
- if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
4229
+ const pendingToolEgress = config.policy.egress?.enabled ? (() => {
4230
+ const dests = extractToolDestinations(toolName, args);
4231
+ const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
4232
+ return eg ? egressPolicyVerdict(eg) : void 0;
4233
+ })() : void 0;
4234
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) {
4235
+ return pendingToolEgress ?? { decision: "allow" };
4236
+ }
4200
4237
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
4201
4238
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
4202
4239
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
@@ -4285,6 +4322,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4285
4322
  }
4286
4323
  let allTokens = [];
4287
4324
  let pathTokens = [];
4325
+ if (pendingToolEgress) return pendingToolEgress;
4288
4326
  const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
4289
4327
  if (shellCommand) {
4290
4328
  const analyzed = analyzeShellCommand(shellCommand);
@@ -4350,16 +4388,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
4350
4388
  const dests = extractShellDestinations(shellCommand);
4351
4389
  if (dests.length > 0) {
4352
4390
  const eg = evaluateEgress(dests, config.policy.egress);
4353
- if (eg) {
4354
- return {
4355
- decision: eg.verdict,
4356
- blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
4357
- reason: eg.reason,
4358
- ruleName: `egress:${eg.binary}:${eg.host}`,
4359
- ruleDescription: eg.reason,
4360
- tier: eg.verdict === "block" ? 3 : 4
4361
- };
4362
- }
4391
+ if (eg) return egressPolicyVerdict(eg);
4363
4392
  }
4364
4393
  }
4365
4394
  const firstToken = analyzed.actions[0] ?? "";
@@ -8299,8 +8328,10 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8299
8328
  };
8300
8329
  }
8301
8330
  }
8331
+ const declaredEgress = config.policy.egress?.enabled === true && extractToolDestinations(toolName, args).length > 0;
8332
+ const judge = !isIgnoredTool2(toolName) || declaredEgress;
8302
8333
  if (isObserveMode) {
8303
- if (!isIgnoredTool2(toolName)) {
8334
+ if (judge) {
8304
8335
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
8305
8336
  const wouldBlock = policyResult.decision === "block";
8306
8337
  if (!isManual)
@@ -8325,7 +8356,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8325
8356
  return { approved: true, checkedBy: "audit" };
8326
8357
  }
8327
8358
  if (config.settings.mode === "audit") {
8328
- if (!isIgnoredTool2(toolName)) {
8359
+ if (judge) {
8329
8360
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
8330
8361
  if (policyResult.decision === "review") {
8331
8362
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
@@ -8364,9 +8395,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
8364
8395
  appPermReviewTool = bareTool;
8365
8396
  }
8366
8397
  }
8367
- if (!taintWarning && !isIgnoredTool2(toolName)) {
8398
+ if (!taintWarning && judge) {
8368
8399
  const ld = config.policy.loopDetection;
8369
- if (ld.enabled && !appPermReview) {
8400
+ if (ld.enabled && !appPermReview && !isIgnoredTool2(toolName)) {
8370
8401
  const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
8371
8402
  if (loopResult.looping) {
8372
8403
  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?`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.16.2",
3
+ "version": "2.17.0",
4
4
  "description": "IAM for your AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",