Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions agent/anthropic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from hermes_constants import get_hermes_home
from types import SimpleNamespace
from typing import Any, Dict, List, Optional, Tuple
from utils import normalize_proxy_env_vars

try:
import anthropic as _anthropic_sdk
Expand Down Expand Up @@ -308,6 +309,9 @@ def build_anthropic_client(api_key: str, base_url: str = None, timeout: float =
"The 'anthropic' package is required for the Anthropic provider. "
"Install it with: pip install 'anthropic>=0.39.0'"
)

normalize_proxy_env_vars()

from httpx import Timeout

normalized_base_url = _normalize_base_url_text(base_url)
Expand Down
4 changes: 3 additions & 1 deletion agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from agent.credential_pool import load_pool
from hermes_cli.config import get_hermes_home
from hermes_constants import OPENROUTER_BASE_URL
from utils import base_url_host_matches, base_url_hostname
from utils import base_url_host_matches, base_url_hostname, normalize_proxy_env_vars

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1028,6 +1028,8 @@ def _validate_proxy_env_urls() -> None:
"""
from urllib.parse import urlparse

normalize_proxy_env_vars()

for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY",
"https_proxy", "http_proxy", "all_proxy"):
value = str(os.environ.get(key) or "").strip()
Expand Down
8 changes: 5 additions & 3 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from abc import ABC, abstractmethod
from urllib.parse import urlsplit

from utils import normalize_proxy_url

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -159,13 +161,13 @@ def resolve_proxy_url(platform_env_var: str | None = None) -> str | None:
if platform_env_var:
value = (os.environ.get(platform_env_var) or "").strip()
if value:
return value
return normalize_proxy_url(value)
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY",
"https_proxy", "http_proxy", "all_proxy"):
value = (os.environ.get(key) or "").strip()
if value:
return value
return _detect_macos_system_proxy()
return normalize_proxy_url(value)
return normalize_proxy_url(_detect_macos_system_proxy())


def proxy_kwargs_for_bot(proxy_url: str | None) -> dict:
Expand Down
4 changes: 2 additions & 2 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
convert_scratchpad_to_think, has_incomplete_scratchpad,
save_trajectory as _save_trajectory_to_file,
)
from utils import atomic_json_write, base_url_host_matches, base_url_hostname, env_var_enabled
from utils import atomic_json_write, base_url_host_matches, base_url_hostname, env_var_enabled, normalize_proxy_url



Expand Down Expand Up @@ -187,7 +187,7 @@ def _get_proxy_from_env() -> Optional[str]:
"https_proxy", "http_proxy", "all_proxy"):
value = os.environ.get(key, "").strip()
if value:
return value
return normalize_proxy_url(value)
return None


Expand Down
8 changes: 8 additions & 0 deletions tests/agent/test_proxy_and_url_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"""
from __future__ import annotations

import os

import pytest

from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls
Expand All @@ -31,6 +33,12 @@ def test_proxy_env_accepts_empty(monkeypatch):
_validate_proxy_env_urls() # should not raise


def test_proxy_env_normalizes_socks_alias(monkeypatch):
monkeypatch.setenv("ALL_PROXY", "socks://127.0.0.1:1080/")
_validate_proxy_env_urls()
assert os.environ["ALL_PROXY"] == "socks5://127.0.0.1:1080/"


@pytest.mark.parametrize("key", [
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY",
"http_proxy", "https_proxy", "all_proxy",
Expand Down
10 changes: 10 additions & 0 deletions tests/gateway/test_proxy_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest

from gateway.config import Platform, StreamingConfig
from gateway.platforms.base import resolve_proxy_url
from gateway.run import GatewayRunner
from gateway.session import SessionSource

Expand Down Expand Up @@ -133,6 +134,15 @@ def test_empty_string_treated_as_unset(self, monkeypatch):
assert runner._get_proxy_url() is None


class TestResolveProxyUrl:
def test_normalizes_socks_alias_from_all_proxy(self, monkeypatch):
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY",
"https_proxy", "http_proxy", "all_proxy"):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("ALL_PROXY", "socks://127.0.0.1:1080/")
assert resolve_proxy_url() == "socks5://127.0.0.1:1080/"


class TestRunAgentProxyDispatch:
"""Test that _run_agent() delegates to proxy when configured."""

Expand Down
8 changes: 8 additions & 0 deletions tests/run_agent/test_create_openai_client_proxy_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ def test_get_proxy_from_env_ignores_blank_values(monkeypatch):
assert _get_proxy_from_env() == "http://real-proxy:8080"


def test_get_proxy_from_env_normalizes_socks_alias(monkeypatch):
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY",
"https_proxy", "http_proxy", "all_proxy"):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("ALL_PROXY", "socks://127.0.0.1:1080/")
assert _get_proxy_from_env() == "socks5://127.0.0.1:1080/"


@patch("run_agent.OpenAI")
def test_create_openai_client_routes_via_proxy_when_env_set(mock_openai, monkeypatch):
"""With HTTPS_PROXY set, the custom httpx.Client must mount an HTTPProxy pool.
Expand Down
34 changes: 33 additions & 1 deletion utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,39 @@ def env_bool(key: str, default: bool = False) -> bool:
return is_truthy_value(os.getenv(key, ""), default=default)


# ─── Proxy Helpers ────────────────────────────────────────────────────────────


_PROXY_ENV_KEYS = (
"HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY",
"https_proxy", "http_proxy", "all_proxy",
)


def normalize_proxy_url(proxy_url: str | None) -> str | None:
"""Normalize proxy URLs for httpx/aiohttp compatibility.

WSL/Clash-style environments often export SOCKS proxies as
``socks://127.0.0.1:PORT``. httpx rejects that alias and expects the
explicit ``socks5://`` scheme instead.
"""
candidate = str(proxy_url or "").strip()
if not candidate:
return None
if candidate.lower().startswith("socks://"):
return f"socks5://{candidate[len('socks://'):]}"
return candidate


def normalize_proxy_env_vars() -> None:
"""Rewrite supported proxy env vars to canonical URL forms in-place."""
for key in _PROXY_ENV_KEYS:
value = os.getenv(key, "")
normalized = normalize_proxy_url(value)
if normalized and normalized != value:
os.environ[key] = normalized


# ─── URL Parsing Helpers ──────────────────────────────────────────────────────


Expand Down Expand Up @@ -236,4 +269,3 @@ def base_url_host_matches(base_url: str, domain: str) -> bool:
if not domain:
return False
return hostname == domain or hostname.endswith("." + domain)