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
12 changes: 10 additions & 2 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,16 @@ def resolve_proxy_url(
target_hosts: str | list[str] | tuple[str, ...] | set[str] | None = None) -> str | None:
"""Proxy URL: *platform_env_var* (e.g. ``DISCORD_PROXY``) first, then HTTPS_PROXY /
HTTP_PROXY / ALL_PROXY (any case), then the macOS system proxy — the latter two only when
``gateway.trust_env`` is true. None when nothing is found or NO_PROXY matches a target."""
value = (os.environ.get(platform_env_var) or "").strip() if platform_env_var else ""
``gateway.trust_env`` is true. None when nothing is found or NO_PROXY matches a target.

*platform_env_var* is a per-adapter, per-profile-configurable setting (each proxy URL can
embed credentials, e.g. ``http://user:pass@host``) so it is read scope-aware: under a
secondary multiplex profile it comes from that profile's own ``.env``, not the shared
process env another profile's ``TELEGRAM_PROXY``/``DISCORD_PROXY``/etc. may hold. The
generic ``HTTPS_PROXY``/``HTTP_PROXY``/``ALL_PROXY`` fallback stays a raw process-env read —
those are OS/system-level network settings, not a per-profile Hermes concept."""
from gateway.platforms._shared import get_scoped_secret as _get_scoped_proxy_var
value = (_get_scoped_proxy_var(platform_env_var, "") or "").strip() if platform_env_var else ""
if not value:
if not gateway_trust_env(): # only the explicit per-platform var is honored
return None
Expand Down
45 changes: 45 additions & 0 deletions tests/gateway/test_gateway_trust_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,51 @@ def test_gateway_trust_env_reads_config(tmp_path, monkeypatch, yaml_body, expect
assert gw_base.resolve_proxy_url("X_PLATFORM_PROXY") == "http://127.0.0.1:1080"


class TestResolveProxyUrlMultiplexScope:
"""A secondary multiplex profile's own TELEGRAM_PROXY/DISCORD_PROXY/etc. must gate its
adapter, not the default profile's YAML-to-env bridge output sitting in the shared
process's os.environ (the #72348 class, applied to this shared chokepoint used by
Telegram, Discord, Mattermost, Matrix, SMS, and Slack)."""

def test_scoped_profile_uses_its_own_value(self, monkeypatch):
from agent.secret_scope import reset_secret_scope, set_multiplex_active, set_secret_scope

monkeypatch.setenv("DISCORD_PROXY", "http://default-profile-proxy:8080")
monkeypatch.delenv("NO_PROXY", raising=False)
monkeypatch.delenv("no_proxy", raising=False)

set_multiplex_active(True)
token = set_secret_scope({"DISCORD_PROXY": "http://secondary-profile-proxy:9090"})
try:
assert gw_base.resolve_proxy_url("DISCORD_PROXY") == "http://secondary-profile-proxy:9090"
finally:
reset_secret_scope(token)
set_multiplex_active(False)

def test_scoped_profile_without_own_value_does_not_borrow_default(self, monkeypatch):
from agent.secret_scope import reset_secret_scope, set_multiplex_active, set_secret_scope

monkeypatch.setenv("DISCORD_PROXY", "http://default-profile-proxy:8080")
monkeypatch.delenv("NO_PROXY", raising=False)
monkeypatch.delenv("no_proxy", raising=False)

set_multiplex_active(True)
token = set_secret_scope({})
try:
assert gw_base.resolve_proxy_url("DISCORD_PROXY") is None
finally:
reset_secret_scope(token)
set_multiplex_active(False)

def test_unscoped_default_profile_still_reads_env(self, monkeypatch):
"""Control: outside multiplex (or the default profile), the legacy env read is
unchanged."""
monkeypatch.setenv("DISCORD_PROXY", "http://default-profile-proxy:8080")
monkeypatch.delenv("NO_PROXY", raising=False)
monkeypatch.delenv("no_proxy", raising=False)
assert gw_base.resolve_proxy_url("DISCORD_PROXY") == "http://default-profile-proxy:8080"


def test_no_bare_trust_env_literal_in_adapters():
"""Every aiohttp session in gateway/ + plugins/platforms/ must go through gateway_trust_env()."""
bare = re.compile(r"trust_env\s*=\s*(True|False)\b")
Expand Down
Loading