Skip to content

Commit 479ca86

Browse files
fix(server): preserve default runtime auth fallback
1 parent 45ceb25 commit 479ca86

2 files changed

Lines changed: 62 additions & 19 deletions

File tree

server/src/agent_control_server/auth_framework/config.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
:class:`NoAuthProvider`, ``api_key`` uses
1717
:class:`HeaderAuthProvider`, and ``jwt`` uses
1818
:class:`LocalJwtVerifyProvider`. When the mode is unset, startup
19-
preserves historical behavior by selecting ``jwt`` if
20-
``AGENT_CONTROL_RUNTIME_TOKEN_SECRET`` is set, otherwise ``api_key``.
19+
selects ``jwt`` if ``AGENT_CONTROL_RUNTIME_TOKEN_SECRET`` is set;
20+
otherwise runtime falls through to the default authorizer.
2121
The ``runtime.token_exchange`` operation continues to flow through
2222
the default authorizer because the exchange itself is shaped like a
2323
management call (forward credential, get grant).
@@ -96,10 +96,11 @@ def configure_auth_from_env() -> None:
9696
Runtime flow:
9797
9898
- ``AGENT_CONTROL_RUNTIME_AUTH_MODE=none``: :class:`NoAuthProvider`.
99-
- ``AGENT_CONTROL_RUNTIME_AUTH_MODE=api_key`` (default when no runtime
100-
token secret is configured): :class:`HeaderAuthProvider`.
99+
- ``AGENT_CONTROL_RUNTIME_AUTH_MODE=api_key``: :class:`HeaderAuthProvider`.
101100
- ``AGENT_CONTROL_RUNTIME_AUTH_MODE=jwt`` (default when a runtime token
102101
secret is configured): :class:`LocalJwtVerifyProvider`.
102+
- unset mode without a runtime token secret: fall through to the default
103+
authorizer.
103104
104105
Clears any previously-installed default and operation overrides
105106
before installing fresh ones, so reconfiguration cannot leave
@@ -121,20 +122,26 @@ def configure_auth_from_env() -> None:
121122
set_authorizer(default)
122123
_active_providers.append(default)
123124

124-
runtime_provider = _build_runtime_provider(runtime_mode, _runtime_auth_config)
125-
set_authorizer(runtime_provider, operation=Operation.RUNTIME_USE)
126-
_active_providers.append(runtime_provider)
127-
if runtime_mode == "jwt":
125+
if runtime_mode == "default":
128126
_logger.info(
129-
"Runtime auth provider: jwt override installed for %s",
127+
"Runtime auth provider: default authorizer handles %s",
130128
Operation.RUNTIME_USE.value,
131129
)
132130
else:
133-
_logger.info(
134-
"Runtime auth provider: %s override installed for %s",
135-
runtime_mode,
136-
Operation.RUNTIME_USE.value,
137-
)
131+
runtime_provider = _build_runtime_provider(runtime_mode, _runtime_auth_config)
132+
set_authorizer(runtime_provider, operation=Operation.RUNTIME_USE)
133+
_active_providers.append(runtime_provider)
134+
if runtime_mode == "jwt":
135+
_logger.info(
136+
"Runtime auth provider: jwt override installed for %s",
137+
Operation.RUNTIME_USE.value,
138+
)
139+
else:
140+
_logger.info(
141+
"Runtime auth provider: %s override installed for %s",
142+
runtime_mode,
143+
Operation.RUNTIME_USE.value,
144+
)
138145

139146

140147
async def teardown_auth() -> None:
@@ -242,7 +249,7 @@ def _parse_extra_forward_headers(raw: str | None) -> tuple[str, ...]:
242249
def _resolve_runtime_mode() -> str:
243250
raw = os.environ.get(_RUNTIME_MODE_ENV)
244251
if raw is None or not raw.strip():
245-
return "jwt" if os.environ.get(_RUNTIME_TOKEN_SECRET_ENV) else "api_key"
252+
return "jwt" if os.environ.get(_RUNTIME_TOKEN_SECRET_ENV) else "default"
246253

247254
mode = raw.strip().lower()
248255
if mode in {"none", "no_auth"}:

server/tests/test_auth_framework.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import httpx
99
import pytest
10-
1110
from agent_control_server.auth_framework.core import (
1211
Operation,
1312
Principal,
@@ -700,7 +699,6 @@ def test_runtime_token_rejects_naive_upstream_expires_at():
700699
def test_runtime_token_rejects_management_token_passed_to_runtime_verify():
701700
"""A token without ``domain=runtime`` must be rejected by runtime verify."""
702701
import jwt
703-
704702
from agent_control_server.auth_framework.runtime_token import (
705703
RuntimeTokenError,
706704
verify_runtime_token,
@@ -1053,13 +1051,13 @@ def test_build_default_provider_accepts_none_mode(monkeypatch):
10531051
assert isinstance(auth_config._build_default_provider(), NoAuthProvider)
10541052

10551053

1056-
def test_resolve_runtime_mode_defaults_to_api_key_without_secret(monkeypatch):
1054+
def test_resolve_runtime_mode_defaults_to_default_without_secret(monkeypatch):
10571055
from agent_control_server.auth_framework import config as auth_config
10581056

10591057
monkeypatch.delenv("AGENT_CONTROL_RUNTIME_AUTH_MODE", raising=False)
10601058
monkeypatch.delenv("AGENT_CONTROL_RUNTIME_TOKEN_SECRET", raising=False)
10611059

1062-
assert auth_config._resolve_runtime_mode() == "api_key"
1060+
assert auth_config._resolve_runtime_mode() == "default"
10631061

10641062

10651063
def test_resolve_runtime_mode_defaults_to_jwt_with_secret(monkeypatch):
@@ -1099,6 +1097,44 @@ def test_configure_runtime_api_key_ignores_jwt_secret(monkeypatch):
10991097
assert auth_config.runtime_auth_config() is None
11001098

11011099

1100+
def test_configure_runtime_unset_preserves_no_auth_default(monkeypatch):
1101+
from agent_control_server.auth_framework import config as auth_config
1102+
1103+
clear_authorizers()
1104+
1105+
monkeypatch.setenv("AGENT_CONTROL_AUTH_MODE", "none")
1106+
monkeypatch.delenv("AGENT_CONTROL_RUNTIME_AUTH_MODE", raising=False)
1107+
monkeypatch.delenv("AGENT_CONTROL_RUNTIME_TOKEN_SECRET", raising=False)
1108+
1109+
auth_config.configure_auth_from_env()
1110+
1111+
assert isinstance(get_authorizer(Operation.RUNTIME_USE), NoAuthProvider)
1112+
assert auth_config.runtime_auth_config() is None
1113+
1114+
1115+
@pytest.mark.asyncio
1116+
async def test_configure_runtime_unset_preserves_http_upstream_default(monkeypatch):
1117+
from agent_control_server.auth_framework import config as auth_config
1118+
1119+
clear_authorizers()
1120+
1121+
monkeypatch.setenv("AGENT_CONTROL_AUTH_MODE", "http_upstream")
1122+
monkeypatch.setenv("AGENT_CONTROL_AUTH_UPSTREAM_URL", "https://auth.example.test/check")
1123+
monkeypatch.delenv("AGENT_CONTROL_RUNTIME_AUTH_MODE", raising=False)
1124+
monkeypatch.delenv("AGENT_CONTROL_RUNTIME_TOKEN_SECRET", raising=False)
1125+
1126+
try:
1127+
auth_config.configure_auth_from_env()
1128+
1129+
default_provider = get_authorizer(Operation.CONTROLS_READ)
1130+
runtime_provider = get_authorizer(Operation.RUNTIME_USE)
1131+
assert isinstance(default_provider, HttpUpstreamAuthProvider)
1132+
assert runtime_provider is default_provider
1133+
assert auth_config.runtime_auth_config() is None
1134+
finally:
1135+
await auth_config.teardown_auth()
1136+
1137+
11021138
@pytest.mark.asyncio
11031139
async def test_configure_http_upstream_management_with_jwt_runtime(monkeypatch):
11041140
from agent_control_server.auth_framework import config as auth_config

0 commit comments

Comments
 (0)