Skip to content
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

<!-- version list -->

## Unreleased

### Changed

- **sdk**: Strict `runtime_auth_mode="jwt"` evaluation requests now require both
`target_type` and `target_id`; missing target context raises an error instead
of falling back to API-key auth.

## v7.7.0 (2026-05-07)

### Bug Fixes
Expand Down
21 changes: 20 additions & 1 deletion sdks/python/src/agent_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class _RefreshContext:
agent_name: str
server_url: str
api_key: str | None
api_key_header: str | None
target_type: str | None
target_id: str | None

Expand Down Expand Up @@ -221,6 +222,7 @@ def _snapshot_refresh_context() -> _RefreshContext:
agent = state.current_agent
server_url = state.server_url
api_key = state.api_key
api_key_header = state.api_key_header
target_type = state.target_type
target_id = state.target_id

Expand All @@ -234,6 +236,7 @@ def _snapshot_refresh_context() -> _RefreshContext:
agent_name=agent.agent_name,
server_url=server_url,
api_key=api_key,
api_key_header=api_key_header,
target_type=target_type,
target_id=target_id,
)
Expand All @@ -244,6 +247,7 @@ async def _fetch_controls_for_context_async(context: _RefreshContext) -> list[di
async with AgentControlClient(
base_url=context.server_url,
api_key=context.api_key,
api_key_header=context.api_key_header,
) as client:
response = await agents.list_agent_controls(
client,
Expand Down Expand Up @@ -430,6 +434,7 @@ def init(
agent_version: str | None = None,
server_url: str | None = None,
api_key: str | None = None,
api_key_header: str | None = None,
controls_file: str | None = None,
steps: list[StepSchemaDict] | None = None,
conflict_mode: Literal["strict", "overwrite"] = "overwrite",
Expand Down Expand Up @@ -468,6 +473,8 @@ def init(
server_url: Optional server URL (defaults to AGENT_CONTROL_URL env var
or http://localhost:8000)
api_key: Optional API key for authentication (defaults to AGENT_CONTROL_API_KEY env var)
api_key_header: Optional HTTP header name for API key authentication
(defaults to AGENT_CONTROL_API_KEY_HEADER env var or X-API-Key)
controls_file: Optional explicit path to controls.yaml (auto-discovered if not provided)
steps: Optional list of step schemas for registration:
[{"type": "tool", "name": "search", "input_schema": {...}, "output_schema": {...}}]
Expand Down Expand Up @@ -535,6 +542,11 @@ async def handle(message: str):
raise ValueError(
"target_type and target_id must be supplied together."
)
resolved_api_key_header = (
api_key_header
or os.getenv(AgentControlClient.API_KEY_HEADER_ENV_VAR)
or AgentControlClient.DEFAULT_API_KEY_HEADER
)

# Re-init behavior: always stop the existing refresh loop before mutating
# shared agent/session globals.
Expand Down Expand Up @@ -562,6 +574,8 @@ async def handle(message: str):
state.current_agent = next_agent
state.server_url = server_url or os.getenv('AGENT_CONTROL_URL') or 'http://localhost:8000'
state.api_key = api_key
state.api_key_header = resolved_api_key_header
state.runtime_token_cache.clear()
state.target_type = target_type
state.target_id = target_id

Expand Down Expand Up @@ -597,7 +611,9 @@ async def register() -> list[dict[str, Any]] | None:
assert state.current_agent is not None

async with AgentControlClient(
base_url=state.server_url, api_key=state.api_key
base_url=state.server_url,
api_key=state.api_key,
api_key_header=state.api_key_header,
) as client:
# Check server health first
try:
Expand Down Expand Up @@ -684,6 +700,7 @@ def run_in_thread() -> None:
batcher = init_observability(
server_url=state.server_url,
api_key=state.api_key,
api_key_header=state.api_key_header,
enabled=observability_enabled,
sink_name=observability_sink_name,
sink_config=observability_sink_config,
Expand Down Expand Up @@ -715,6 +732,8 @@ def _reset_state() -> None:
state.server_controls = None
state.server_url = None
state.api_key = None
state.api_key_header = None
state.runtime_token_cache.clear()
state.target_type = None
state.target_id = None

Expand Down
4 changes: 4 additions & 0 deletions sdks/python/src/agent_control/_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from typing import TYPE_CHECKING, Any

from .runtime_auth import RuntimeTokenCache

if TYPE_CHECKING:
from agent_control_models import Agent

Expand All @@ -24,6 +26,8 @@ def __init__(self) -> None:
self.server_controls: list[dict[str, Any]] | None = None
self.server_url: str | None = None
self.api_key: str | None = None
self.api_key_header: str | None = None
self.runtime_token_cache = RuntimeTokenCache()
# Optional target context fixed at init() time; both fields are set
# together or both remain None.
self.target_type: str | None = None
Expand Down
Loading
Loading