fix(gateway): scope default-listener api_server requests under multiplex
Rebuilt from PR #61283 onto the /p/<profile>/ routing world (7aa21e336):
_profile_scope(None) now enters the DEFAULT profile's runtime scope when
multiplexing is active instead of returning nullcontext(). api_server is
a port-binding platform living on the default profile, so plain requests
(no /p/ prefix) are the primary path — with fail-closed get_secret they
crashed with UnscopedSecretError on the first credential read (#61276).
All three wrapped call sites (chat-completions executor, /v1/runs agent
construction and _run_sync) inherit the fix through the one seam.
Single-profile gateways keep the no-op. Regression tests ported from
the original PR to the _profile_scope seam.
Fixes #61276
fix/verification-admin-route-recovery
parent
fef0b2d600
commit
6ff65c4d20
|
|
@ -1284,8 +1284,27 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _profile_scope(profile: Optional[str]):
|
def _profile_scope(profile: Optional[str]):
|
||||||
"""Enter the multiplex profile runtime scope, or a no-op when unset."""
|
"""Enter the multiplex profile runtime scope, or a no-op when unset.
|
||||||
|
|
||||||
|
When no ``/p/<profile>/`` prefix was given AND multiplexing is active,
|
||||||
|
enter the DEFAULT profile's scope instead of a no-op: api_server is a
|
||||||
|
port-binding platform that lives on the default profile, and with
|
||||||
|
multiplex fail-closed ``get_secret`` active, an unscoped agent run
|
||||||
|
raises ``UnscopedSecretError`` on its first credential read (#61276).
|
||||||
|
Single-profile gateways keep the no-op — ``get_secret`` falls through
|
||||||
|
to ``os.environ`` there, unchanged.
|
||||||
|
"""
|
||||||
if not profile:
|
if not profile:
|
||||||
|
try:
|
||||||
|
from agent.secret_scope import is_multiplex_active
|
||||||
|
|
||||||
|
if is_multiplex_active():
|
||||||
|
from gateway.run import _profile_runtime_scope
|
||||||
|
from hermes_constants import get_hermes_home
|
||||||
|
|
||||||
|
return _profile_runtime_scope(get_hermes_home())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return nullcontext()
|
return nullcontext()
|
||||||
from gateway.run import _profile_runtime_scope
|
from gateway.run import _profile_runtime_scope
|
||||||
from hermes_cli.profiles import get_profile_dir
|
from hermes_cli.profiles import get_profile_dir
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
"""Regression for #61276: api_server agent entry under multiplex isolation.
|
||||||
|
|
||||||
|
When gateway.multiplex_profiles is on, get_secret fails closed without a
|
||||||
|
profile secret scope. Requests with a ``/p/<profile>/`` prefix are scoped by
|
||||||
|
``_profile_scope(profile)``, but plain requests on the default listener used
|
||||||
|
to get ``nullcontext()`` — so agent runs crashed with UnscopedSecretError on
|
||||||
|
their first credential read (e.g. OPENROUTER_BASE_URL). ``_profile_scope``
|
||||||
|
now enters the DEFAULT profile's runtime scope when multiplex is active and
|
||||||
|
no profile was requested.
|
||||||
|
|
||||||
|
Adapted from PR #61283 by @giggling-ginger (originally targeting a
|
||||||
|
pre-``_profile_scope`` helper); no live gateway or network.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agent import secret_scope as ss
|
||||||
|
from gateway.config import PlatformConfig
|
||||||
|
from gateway.platforms.api_server import APIServerAdapter
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _reset_multiplex():
|
||||||
|
ss.set_multiplex_active(False)
|
||||||
|
yield
|
||||||
|
ss.set_multiplex_active(False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def adapter():
|
||||||
|
return APIServerAdapter(PlatformConfig(enabled=True))
|
||||||
|
|
||||||
|
|
||||||
|
class TestProfileScopeDefaultFallback:
|
||||||
|
def test_noop_when_multiplex_off(self, adapter, monkeypatch):
|
||||||
|
monkeypatch.setenv("OPENROUTER_BASE_URL", "https://from-environ.example/v1")
|
||||||
|
with adapter._profile_scope(None):
|
||||||
|
# Legacy single-profile path: unscoped get_secret reads os.environ.
|
||||||
|
assert ss.get_secret("OPENROUTER_BASE_URL") == "https://from-environ.example/v1"
|
||||||
|
assert ss.current_secret_scope() is None
|
||||||
|
|
||||||
|
def test_default_scope_installed_under_multiplex(self, adapter, tmp_path, monkeypatch):
|
||||||
|
"""No /p/ prefix + multiplex active → default profile scope, not nullcontext."""
|
||||||
|
(tmp_path / ".env").write_text(
|
||||||
|
"OPENROUTER_BASE_URL=https://openrouter.ai/api/v1\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"hermes_constants.get_hermes_home",
|
||||||
|
lambda: tmp_path,
|
||||||
|
)
|
||||||
|
monkeypatch.setenv("OPENROUTER_BASE_URL", "https://leak.example/v1")
|
||||||
|
ss.set_multiplex_active(True)
|
||||||
|
|
||||||
|
with adapter._profile_scope(None):
|
||||||
|
assert ss.current_secret_scope() is not None
|
||||||
|
# Profile .env wins; process env must not leak through.
|
||||||
|
assert ss.get_secret("OPENROUTER_BASE_URL") == "https://openrouter.ai/api/v1"
|
||||||
|
|
||||||
|
# Scope torn down; fail-closed behavior restored outside.
|
||||||
|
assert ss.current_secret_scope() is None
|
||||||
|
with pytest.raises(ss.UnscopedSecretError):
|
||||||
|
ss.get_secret("OPENROUTER_BASE_URL")
|
||||||
|
|
||||||
|
def test_named_profile_scope_still_wins(self, adapter, tmp_path, monkeypatch):
|
||||||
|
"""A /p/<profile>/ request keeps resolving that profile's scope."""
|
||||||
|
profile_home = tmp_path / "profiles" / "worker"
|
||||||
|
profile_home.mkdir(parents=True)
|
||||||
|
(profile_home / ".env").write_text(
|
||||||
|
"OPENROUTER_BASE_URL=https://worker.example/v1\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"hermes_cli.profiles.get_profile_dir", lambda name: profile_home
|
||||||
|
)
|
||||||
|
ss.set_multiplex_active(True)
|
||||||
|
|
||||||
|
with adapter._profile_scope("worker"):
|
||||||
|
assert ss.get_secret("OPENROUTER_BASE_URL") == "https://worker.example/v1"
|
||||||
|
assert ss.current_secret_scope() is None
|
||||||
Loading…
Reference in New Issue