Skip to content

feat: configurable runtime-token header (server + SDK)#252

Draft
josjeon wants to merge 2 commits into
agentcontrol:mainfrom
josjeon:hybim-741-configurable-runtime-token-header
Draft

feat: configurable runtime-token header (server + SDK)#252
josjeon wants to merge 2 commits into
agentcontrol:mainfrom
josjeon:hybim-741-configurable-runtime-token-header

Conversation

@josjeon

@josjeon josjeon commented Jul 14, 2026

Copy link
Copy Markdown

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.

AGENT_CONTROL_RUNTIME_TOKEN_HEADER = X-Agent-Control-Runtime-Token

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.

BEFORE  (collision on Authorization)

  SDK ──Authorization: Bearer <runtime-token>──▶ ┌─────────┐
                                                 │ Gateway │ overwrites Authorization
                                                 └────┬────┘ with its own identity JWT
                                                      │
                          Authorization: Bearer <gateway-jwt>
                                                      ▼
                                                ┌─────────┐
                                                │   AC    │ runtime token is gone → 401
                                                └─────────┘

AFTER  (two headers, no collision)

  SDK ──Authorization:                 (free for the gateway)──▶ ┌─────────┐
      └─X-Agent-Control-Runtime-Token: <runtime-token>─────────▶│ Gateway │ sets its own
                                                                 └────┬────┘ Authorization JWT
                          Authorization: Bearer <gateway-jwt>        │
                          X-Agent-Control-Runtime-Token: <token>─────┤ (passed through)
                                                                     ▼
                                                               ┌─────────┐
                                                               │   AC    │ reads runtime token
                                                               └─────────┘ from its own header → OK

The fix

One knob, wired on both sides:

Side How to set it Effect
Server AGENT_CONTROL_RUNTIME_TOKEN_HEADER env Verifier reads the runtime token from this header
SDK same env, or runtime_token_header= param Client sends the runtime token on this header

Rules for the header value:

  • Authorization (default): Bearer prefix required — fully back-compatible.
  • Dedicated header: raw token, Bearer prefix optional (nothing else competes for that header).
  • Unset or whitespace-only env → falls back to 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 on Authorization is not accepted when the verifier is configured for a dedicated header (no silent fallback).

Single credential: in jwt mode the runtime token is the only credential on an evaluation request. The SDK suppresses X-API-Key when a runtime token is present, whether it rides Authorization or a dedicated header. The auto-fallback path (exchange unavailable, no token minted) and the token-exchange call itself still send X-API-Key, so nothing silently becomes unauthenticated.

What changed

Server

  • auth_framework/providers/local_jwt.pyLocalJwtVerifyProvider gains a header_name param; _extract_bearer_token applies the Bearer rule above. DEFAULT_RUNTIME_TOKEN_HEADER is the single source of truth.
  • auth_framework/config.py — new env resolved by _resolve_runtime_token_header().

SDK (sdks/python)

  • client.pyAgentControlClient gains runtime_token_header; _merge_runtime_headers sends on the configured header; _format_runtime_token owns the Bearer-prefix rule; _AgentControlAuth suppresses X-API-Key when a runtime token is present.

Commits

  1. feat(auth): make the runtime-token header configurable — server verifier + config + tests
  2. feat(sdk): send the runtime token on the configurable header — Python SDK + tests

Tests

  • Server — custom-header raw/Bearer acceptance, case-insensitive lookup, no-fallback-to-Authorization, whitespace/blank handling, config wiring; app-level E2E through /api/v1/evaluation proving a gateway JWT on Authorization and the runtime token on a dedicated header coexist.
  • SDK — 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.

Test plan

  • Server auth suites — 134 passed
  • SDK suite — 38 passed
  • ruff check + mypy clean on all changed files
  • Reviewer: confirm the default (Authorization) path is unchanged for existing deployments
  • lab0 E2E through the real gateway (tracked separately)

🤖 Generated with Claude Code

@josjeon josjeon changed the title feat(auth): make runtime JWT verifier header configurable feat: configurable runtime-token header (server + SDK) Jul 14, 2026
jjeonsplunk and others added 2 commits July 14, 2026 15:56
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>
@josjeon josjeon force-pushed the hybim-741-configurable-runtime-token-header branch from a542723 to 79e7a19 Compare July 14, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants