feat(auth): configurable runtime-token header (server + SDK) to avoid gateway Authorization collision [HYBIM-866] - #253
Open
josjeon wants to merge 7 commits into
Conversation
josjeon
force-pushed
the
hybim-741-runtime-token-configurable-header
branch
4 times, most recently
from
July 23, 2026 22:15
19dfb5a to
6c4e9c7
Compare
josjeon
force-pushed
the
hybim-741-runtime-token-configurable-header
branch
from
July 23, 2026 22:21
6c4e9c7 to
a1d2410
Compare
josjeon
marked this pull request as ready for review
July 28, 2026 18:58
josjeon
pushed a commit
to josjeon/agent-control
that referenced
this pull request
Aug 7, 2026
The positional-order regression guard stopped at api_key_header (6th positional), but the compatibility regression begins at controls_file (7th), so the prior test would also pass against the broken signature. Supply positionals through target_id (16th) and assert target_type/ target_id bind to their pre-change slots; runtime_token_header stays last and unset. Verified this fails if the header is inserted before target_id. Addresses review comment on PR agentcontrol#253. Co-Authored-By: Claude <noreply@anthropic.com>
…ization collision HYBIM-741. When Agent Control runs behind the O11y gateway, the gateway overwrites `Authorization` with its own downstream identity JWT, clobbering AC's runtime-eval token on the hot path. Make LocalJwtVerifyProvider read the runtime token from a configurable header, selected by AGENT_CONTROL_RUNTIME_TOKEN_HEADER (default `Authorization`). Behind the gateway, point it at a dedicated header (e.g. `X-Agent-Control-Runtime-Token`) so the runtime token and the gateway's Authorization JWT no longer collide. - `Authorization` keeps the mandatory `Bearer` scheme (existing contract). - A dedicated header accepts the raw token (Bearer optional). - Default unchanged: with the env unset, behavior is identical to before. Server-side only; the SDK send-side change is tracked separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… gateway Authorization collision HYBIM-741. When Agent Control runs behind the O11y gateway, the gateway overwrites `Authorization` with its own downstream identity JWT, clobbering AC's runtime-eval token on the hot path. Make the runtime token ride a configurable header on both sides, selected by AGENT_CONTROL_RUNTIME_TOKEN_HEADER (default `Authorization`): - Server: LocalJwtVerifyProvider reads the token from the configured header; Bearer stays mandatory on Authorization, raw token accepted on a dedicated header. - SDK: AgentControlClient sends the token on the configured header (raw on a dedicated header, Bearer on Authorization) and suppresses the API-key fallback when the runtime token rides its own header, so a runtime request carries a single credential. Default unchanged: with the env unset, behavior is identical to before. Point both sides at a dedicated header (e.g. X-Agent-Control-Runtime-Token) behind the gateway so the runtime token and the gateway Authorization JWT no longer collide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Preserve the outer gateway credential: _AgentControlAuth no longer suppresses the API key when the runtime token rides a dedicated header. The existing same-header guard already prevents collisions, so the API key remains available as the outer credential a gateway may require. - Preserve positional argument order: move runtime_token_header to the end of AgentControlClient.__init__ so existing positional callers are unaffected. - Expose runtime_token_header through the high-level SDK: agent_control.init() accepts it, stores it in session state, and threads it into the evaluation clients (evaluation.py, control_decorators.py); cleared on reset. - Add app-level /api/v1/evaluation tests exercising config wiring + Operation.RUNTIME_USE routing: runtime token on a dedicated header with an outer Authorization gateway JWT is accepted; a token on Authorization is ignored when a custom header is configured. Co-Authored-By: Claude <noreply@anthropic.com>
…HYBIM-866] Move runtime_token_header to the end of agent_control.init()'s signature (after target_id, before **kwargs) so existing positional callers are not shifted — previously it sat before controls_file, binding a controls path to the header and shifting every later arg. Mirrors the AgentControlClient fix. Add a positional-compatibility test. Addresses PR review. Co-Authored-By: Claude <noreply@anthropic.com>
…ate gateway 401 [HYBIM-866] Addresses PR review (Namrata): - Validate the runtime-token header against the RFC 7230 field-name grammar, not just non-blank (P2). New shared validate_http_field_name / resolve_runtime_token_header in the SDK runtime_auth, and a matching validate_http_field_name on the server (local_jwt). Applied in AgentControlClient.__init__, agent_control.init() (before stopping the refresh loop / mutating session state), LocalJwtVerifyProvider, and config._resolve_runtime_token_header — so an invalid header fails at construction/startup instead of on the first evaluation. - Distinguish a gateway 401 from a runtime-token failure (P1). When the runtime token rides a dedicated header (Authorization then carries the gateway's own identity), a bare 401 is ambiguous; _should_refresh_runtime_token now only refreshes on 401 there when WWW-Authenticate flags the token invalid, so a gateway 401 no longer evicts a valid runtime token and masks the original error. Default (token on Authorization) behavior is unchanged. Tests: SDK field-name validation (param + env), 401-disambiguation unit tests; server field-name validation for provider + resolver. SDK 59 pass, server 138 pass. Co-Authored-By: Claude <noreply@anthropic.com>
The positional-order regression guard stopped at api_key_header (6th positional), but the compatibility regression begins at controls_file (7th), so the prior test would also pass against the broken signature. Supply positionals through target_id (16th) and assert target_type/ target_id bind to their pre-change slots; runtime_token_header stays last and unset. Verified this fails if the header is inserted before target_id. Addresses review comment on PR agentcontrol#253. Co-Authored-By: Claude <noreply@anthropic.com>
Two import blocks in the new test file (top-level, and the local import in test_runtime_token_rejects_management_token_passed_to_runtime_verify) were un-sorted per ruff I001. Auto-fixed so CI lint passes. Co-Authored-By: Claude <noreply@anthropic.com>
josjeon
force-pushed
the
hybim-741-runtime-token-configurable-header
branch
from
August 7, 2026 20:10
1f122bb to
53a6f6e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When Agent Control runs behind the O11y gateway, the gateway overwrites
Authorizationwith its own downstream identity JWT — clobbering AC's runtime-eval token on the hot path (HYBIM-866, under epic HYBIM-741). The runtime token and the gateway's identity JWT both want theAuthorizationheader.Fix
Make the runtime token ride a configurable header on both sides, selected by
AGENT_CONTROL_RUNTIME_TOKEN_HEADER(defaultAuthorization). Behind the gateway, point both sides at a dedicated header (e.g.X-Agent-Control-Runtime-Token); the runtime token rides that header while the gateway keepsAuthorizationfor its identity JWT — no collision.Authorization: keeps the mandatoryBearerscheme (existing contract).Bearerprefix).Server (verify side)
auth_framework/providers/local_jwt.py:LocalJwtVerifyProvidertakes aheader_name(defaultAuthorization) and reads the token from it.Bearerrequired only onAuthorization; raw token accepted on a dedicated header.auth_framework/config.py:_resolve_runtime_token_header()readsAGENT_CONTROL_RUNTIME_TOKEN_HEADER(blank → default), passed into the provider when runtime mode isjwt.SDK (send side)
sdks/python/.../client.py:AgentControlClientgains aruntime_token_headerparam (+ same env var). Sends the runtime token on the configured header — raw on a dedicated header,BeareronAuthorization(single_format_runtime_tokenhelper is the sole authority for that rule).X-API-Key), so a request can authenticate at the gateway while the runtime JWT is verified by Agent Control. The existing same-header guard prevents any collision.agent_control.init()acceptsruntime_token_header, stores it in session state, and threads it into the evaluation clients (evaluation.py,control_decorators.py); it is cleared on reset.Configuration (behind the gateway)
Tests
test_auth_framework.py): default Bearer path; default rejects raw onAuthorization; dedicated header reads raw token and coexists with a gatewayAuthorizationJWT; Bearer also accepted on dedicated header; missing-header error names the configured header; blankheader_namerejected; env resolver (unset → default, set → trimmed, whitespace → default).test_runtime_token_exchange_endpoint.py): end-to-end through/api/v1/evaluationexercising config wiring +Operation.RUNTIME_USErouting — runtime token on a dedicated header with an outerAuthorizationgateway JWT is accepted; a token presented onAuthorizationis rejected (401) when a custom header is configured.test_client.py): header resolution (param/env/default, blank rejected, whitespace-env fallback); raw token on dedicated header withAuthorizationfree while the API key is preserved on its own header; default sendsBeareronAuthorization; auto-mode fallback keeps the API key when the exchange is unavailable.test_init_validation.py):init()storesruntime_token_headerin session state; defaults toNonewhen unset; validates the header up front (blank and bad field-name rejected before state is mutated); a positional call throughtarget_idproves the new param is appended last and does not shift any existing slot (controls_fileonward).Security notes
Authorizationis ignored, so it can't be smuggled past the gateway boundary.verify_runtime_token) are unchanged.Notes / scope
noneruntime modes.X-Agent-Control-Runtime-Tokenand the server reading the same header, runtime JWT exchange, control steering, and control-span ingestion all worked. This confirms the SDK and server agree on the custom header. It does not yet exercise the gateway-collision path (the devstack has no O11y gateway overwritingAuthorization).ao-agent-control).