feat: configurable runtime-token header (server + SDK)#252
Draft
josjeon wants to merge 2 commits into
Draft
Conversation
Let the runtime-token JWT verifier read its token from a configurable request header (env AGENT_CONTROL_RUNTIME_TOKEN_HEADER), defaulting to Authorization so existing deployments are unaffected. Why: when Agent Control runs behind an API gateway that reserves the Authorization header for its own downstream identity JWT, the gateway overwrites the runtime token on the hot evaluation path and the verifier fails. Pointing the verifier at a dedicated header (e.g. X-Agent-Control-Runtime-Token) lets the two tokens coexist. - LocalJwtVerifyProvider gains a header_name param. On Authorization the Bearer scheme prefix stays required (back-compat); on a dedicated header the raw token is accepted (Bearer optional). The token is still signature-verified, scope-checked, and target-bound after extraction, so the header choice cannot bypass verification. - config.py resolves the header via _resolve_runtime_token_header(); a whitespace-only env value falls back to the default. - Tests: custom-header raw/Bearer acceptance, case-insensitive lookup, no fallback to Authorization, whitespace/blank handling, and app-level E2E through /api/v1/evaluation (gateway JWT on Authorization coexists with the runtime token on a dedicated header). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror the server change in the Python SDK so Option A works end to end. AgentControlClient gains a runtime_token_header param (same env, default Authorization). The Bearer prefix is applied only on Authorization; a dedicated header carries the raw token. - _merge_runtime_headers sends the token on the configured header; _format_runtime_token owns the Bearer-prefix rule. - The runtime token stays the sole credential on an evaluation request: _AgentControlAuth suppresses X-API-Key when a runtime token is present on its dedicated header. The auto-fallback path (no token minted) and the token-exchange POST both still carry X-API-Key, so nothing becomes unauthenticated. - Blank-header handling matches the server: a whitespace-only env value falls back to the default; an explicit blank param is a hard error. - Tests cover custom-header send (raw, no Bearer; Authorization and X-API-Key both absent), env override, defaults, blank/whitespace handling, custom-header auto-fallback keeping X-API-Key, and exchange auth in custom-header mode. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
a542723 to
79e7a19
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.
TL;DR
The runtime-token JWT can now travel on a configurable header instead of always
Authorization. Default is unchanged (Authorization), so existing deployments are unaffected. Set one env var on both the server and the SDK to move it to a dedicated header.The problem
When Agent Control sits behind an API gateway, the gateway often puts its own identity JWT on
Authorization. That overwrites our runtime token on the hot evaluation path, and the verifier rejects the request.The fix
One knob, wired on both sides:
AGENT_CONTROL_RUNTIME_TOKEN_HEADERenvruntime_token_header=paramRules for the header value:
Authorization(default):Bearerprefix required — fully back-compatible.Bearerprefix optional (nothing else competes for that header).Authorization(server and SDK behave identically). An explicit blank param on the SDK is a hard error.Security: this only chooses which header the opaque token is read from. After extraction the token is still signature-verified, scope-checked, and target-bound (
target_type+target_id) exactly as before — the header choice cannot bypass verification. A runtime token presented onAuthorizationis not accepted when the verifier is configured for a dedicated header (no silent fallback).Single credential: in
jwtmode the runtime token is the only credential on an evaluation request. The SDK suppressesX-API-Keywhen a runtime token is present, whether it ridesAuthorizationor a dedicated header. The auto-fallback path (exchange unavailable, no token minted) and the token-exchange call itself still sendX-API-Key, so nothing silently becomes unauthenticated.What changed
Server
auth_framework/providers/local_jwt.py—LocalJwtVerifyProvidergains aheader_nameparam;_extract_bearer_tokenapplies the Bearer rule above.DEFAULT_RUNTIME_TOKEN_HEADERis the single source of truth.auth_framework/config.py— new env resolved by_resolve_runtime_token_header().SDK (
sdks/python)client.py—AgentControlClientgainsruntime_token_header;_merge_runtime_headerssends on the configured header;_format_runtime_tokenowns the Bearer-prefix rule;_AgentControlAuthsuppressesX-API-Keywhen a runtime token is present.Commits
feat(auth): make the runtime-token header configurable— server verifier + config + testsfeat(sdk): send the runtime token on the configurable header— Python SDK + testsTests
/api/v1/evaluationproving a gateway JWT onAuthorizationand the runtime token on a dedicated header coexist.Bearer;AuthorizationandX-API-Keyboth absent), env override, defaults, blank/whitespace handling, custom-header auto-fallback keepingX-API-Key, and exchange auth in custom-header mode.Test plan
ruff check+mypyclean on all changed filesAuthorization) path is unchanged for existing deployments🤖 Generated with Claude Code