fix(imports): keep `import gateway` free of httpx via lazy AuxiliaryExplicitCancellation import
`import gateway` reaches agent/conversation_compression.py through gateway/session.py -> agent/turn_context.py, and a module-level `from agent.auxiliary_client import AuxiliaryExplicitCancellation` there dragged in agent.credential_pool -> hermes_cli.auth -> httpx at import time. Minimal consumers that import the gateway package with only the lightweight wire deps (websockets/aiohttp/pyyaml/requests) — e.g. the gateway-gateway cross-repo live E2E suite — now crash with ModuleNotFoundError: No module named 'httpx' before running anything. Move the import to call time inside compress_context(), matching the existing lazy-import pattern for aux_progress_hook / aux_interrupt_protection in the same function. Both uses of the exception class (the raise and the except) are inside compress_context's dynamic extent, so behavior is unchanged. Add tests/gateway/test_gateway_import_hygiene.py: a fresh-interpreter probe that blocks httpx/openai/anthropic on the meta path and imports gateway.relay.ws_transport, so an eager heavyweight import on the gateway path fails CI instead of breaking downstream repos. Verified: probe fails on current main, passes with this change; also reproduced in a real minimal venv (pip install websockets aiohttp pyyaml requests) where `from gateway.relay.ws_transport import WebSocketRelayTransport` now succeeds with httpx absent.
This commit is contained in:
parent
a991dfc25d
commit
244b6fdb55
|
|
@ -66,7 +66,6 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from agent.auxiliary_client import AuxiliaryExplicitCancellation
|
||||
from agent.context_engine import (
|
||||
automatic_compaction_status_message,
|
||||
sanitize_memory_context,
|
||||
|
|
@ -2166,6 +2165,13 @@ def compress_context(
|
|||
prompt — the session is NOT rotated. Callers should detect the
|
||||
no-op via ``len(returned) == len(input)`` and stop the retry loop.
|
||||
"""
|
||||
# Imported at call time so that ``import gateway`` (which reaches this
|
||||
# module via agent.turn_context) stays free of the auxiliary-client →
|
||||
# credential-pool → hermes_cli.auth → httpx chain. Minimal consumers
|
||||
# (cross-repo E2E venvs, tooling) import the gateway package without the
|
||||
# full agent dependency set.
|
||||
from agent.auxiliary_client import AuxiliaryExplicitCancellation
|
||||
|
||||
_compressor_attempt_snapshot = _snapshot_compressor_attempt_state(
|
||||
agent.context_compressor
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
"""Import-hygiene guard: ``import gateway`` must not require httpx.
|
||||
|
||||
The gateway package is imported by minimal consumers that install only the
|
||||
lightweight wire deps (websockets/aiohttp/pyyaml/requests) — notably the
|
||||
gateway-gateway cross-repo live E2E suite, which drives
|
||||
``gateway.relay.ws_transport`` from a venv without the full agent dependency
|
||||
set. Any module reachable from ``import gateway`` that eagerly imports
|
||||
``hermes_cli.auth`` (and therefore httpx) breaks every one of those consumers
|
||||
at import time.
|
||||
|
||||
Regression: agent/conversation_compression.py grew a module-level
|
||||
``from agent.auxiliary_client import AuxiliaryExplicitCancellation`` which
|
||||
pulled in agent.credential_pool → hermes_cli.auth → httpx during
|
||||
``import gateway``.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# Runs in a fresh interpreter so previously-imported modules in the test
|
||||
# process can't mask an eager import. A meta-path hook makes httpx (and the
|
||||
# heavyweight SDK clients) unimportable, mimicking the minimal E2E venv.
|
||||
_PROBE = """
|
||||
import sys
|
||||
|
||||
BLOCKED = {"httpx", "openai", "anthropic"}
|
||||
|
||||
class _Blocker:
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname.split(".")[0] in BLOCKED:
|
||||
return self
|
||||
return None
|
||||
|
||||
def load_module(self, fullname):
|
||||
raise ImportError(f"blocked by import-hygiene test: {fullname}")
|
||||
|
||||
sys.meta_path.insert(0, _Blocker())
|
||||
|
||||
from gateway.relay.ws_transport import WebSocketRelayTransport # noqa: F401
|
||||
import gateway # noqa: F401
|
||||
|
||||
leaked = sorted(m for m in sys.modules if m.split(".")[0] in BLOCKED)
|
||||
if leaked:
|
||||
raise SystemExit(f"blocked modules leaked into sys.modules: {leaked}")
|
||||
print("import-hygiene-ok")
|
||||
"""
|
||||
|
||||
|
||||
def test_import_gateway_does_not_require_httpx():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", _PROBE],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
"`import gateway` dragged in a blocked heavyweight dependency "
|
||||
"(httpx/openai/anthropic). Minimal consumers (cross-repo E2E venvs) "
|
||||
"import the gateway package without the full agent deps — keep those "
|
||||
f"imports lazy.\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
||||
)
|
||||
assert "import-hygiene-ok" in result.stdout
|
||||
Loading…
Reference in New Issue