Skip to content

feat(auth): configurable runtime-token header (server + SDK) to avoid gateway Authorization collision [HYBIM-866] - #253

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

feat(auth): configurable runtime-token header (server + SDK) to avoid gateway Authorization collision [HYBIM-866]#253
josjeon wants to merge 7 commits into
agentcontrol:mainfrom
josjeon:hybim-741-runtime-token-configurable-header

Conversation

@josjeon

@josjeon josjeon commented Jul 23, 2026

Copy link
Copy Markdown

Problem

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 (HYBIM-866, under epic HYBIM-741). The runtime token and the gateway's identity JWT both want the Authorization header.

Fix

Make the runtime token ride a configurable header on both sides, selected by AGENT_CONTROL_RUNTIME_TOKEN_HEADER (default Authorization). 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 keeps Authorization for its identity JWT — no collision.

  • Authorization: keeps the mandatory Bearer scheme (existing contract).
  • Dedicated header: carries the raw token (no Bearer prefix).
  • Default unchanged: with the env unset, behavior is byte-identical to today.

Server (verify side)

  • auth_framework/providers/local_jwt.py: LocalJwtVerifyProvider takes a header_name (default Authorization) and reads the token from it. Bearer required only on Authorization; raw token accepted on a dedicated header.
  • auth_framework/config.py: _resolve_runtime_token_header() reads AGENT_CONTROL_RUNTIME_TOKEN_HEADER (blank → default), passed into the provider when runtime mode is jwt.

SDK (send side)

  • sdks/python/.../client.py: AgentControlClient gains a runtime_token_header param (+ same env var). Sends the runtime token on the configured header — raw on a dedicated header, Bearer on Authorization (single _format_runtime_token helper is the sole authority for that rule).
  • The API key is preserved as the outer gateway credential: when the runtime token rides a dedicated header, the API key still rides its own header (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.
  • High-level SDK: agent_control.init() accepts runtime_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)

# server
AGENT_CONTROL_RUNTIME_AUTH_MODE=jwt
AGENT_CONTROL_RUNTIME_TOKEN_SECRET=<secret>
AGENT_CONTROL_RUNTIME_TOKEN_HEADER=X-Agent-Control-Runtime-Token

# SDK (must match the server header)
AgentControlClient(..., runtime_token_header="X-Agent-Control-Runtime-Token")
# or agent_control.init(..., runtime_token_header="X-Agent-Control-Runtime-Token")
# or AGENT_CONTROL_RUNTIME_TOKEN_HEADER=X-Agent-Control-Runtime-Token

Tests

  • Server (test_auth_framework.py): default Bearer path; default rejects raw on Authorization; dedicated header reads raw token and coexists with a gateway Authorization JWT; Bearer also accepted on dedicated header; missing-header error names the configured header; blank header_name rejected; env resolver (unset → default, set → trimmed, whitespace → default).
  • Server, app-level (test_runtime_token_exchange_endpoint.py): end-to-end through /api/v1/evaluation exercising config wiring + Operation.RUNTIME_USE routing — runtime token on a dedicated header with an outer Authorization gateway JWT is accepted; a token presented on Authorization is rejected (401) when a custom header is configured.
  • SDK (test_client.py): header resolution (param/env/default, blank rejected, whitespace-env fallback); raw token on dedicated header with Authorization free while the API key is preserved on its own header; default sends Bearer on Authorization; auto-mode fallback keeps the API key when the exchange is unavailable.
  • SDK, high-level (test_init_validation.py): init() stores runtime_token_header in session state; defaults to None when unset; validates the header up front (blank and bad field-name rejected before state is mutated); a positional call through target_id proves the new param is appended last and does not shift any existing slot (controls_file onward).

Security notes

  • Header isolation: with a dedicated header configured, the verifier reads only that header — a token presented on Authorization is ignored, so it can't be smuggled past the gateway boundary.
  • No token leakage: auth error messages reference the header name only, never the token value.
  • Signature / scope / target-binding checks (verify_runtime_token) are unchanged.

Notes / scope

  • Backwards compatible: unset env → identical behavior. No change to the API-key or none runtime modes.
  • Design follows the O11y api-service precedent: support both auth methods, gateway stays neutral, opt-in, default preserved.
  • Server-side tests require the repo's Postgres test fixture (run in CI); the SDK suite runs standalone.
  • Validated end-to-end on a devstack: with the SDK sending the token on X-Agent-Control-Runtime-Token and 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 overwriting Authorization).
  • Gateway forwarding of the custom header is confirmed with the gateway owners; the real gateway-collision validation on lab0 is pending (blocked on getting the branch image into Artifactory as ao-agent-control).

@josjeon
josjeon force-pushed the hybim-741-runtime-token-configurable-header branch 4 times, most recently from 19dfb5a to 6c4e9c7 Compare July 23, 2026 22:15
@josjeon josjeon changed the title feat(auth): configurable runtime-token header to avoid gateway Authorization collision [HYBIM-741] feat(auth): configurable runtime-token header (server + SDK) to avoid gateway Authorization collision [HYBIM-741] Jul 23, 2026
@josjeon
josjeon force-pushed the hybim-741-runtime-token-configurable-header branch from 6c4e9c7 to a1d2410 Compare July 23, 2026 22:21
Comment thread sdks/python/src/agent_control/client.py Outdated
Comment thread sdks/python/src/agent_control/client.py Outdated
Comment thread sdks/python/src/agent_control/client.py
Comment thread server/tests/test_auth_framework.py
@josjeon
josjeon marked this pull request as ready for review July 28, 2026 18:58
@josjeon josjeon changed the title feat(auth): configurable runtime-token header (server + SDK) to avoid gateway Authorization collision [HYBIM-741] feat(auth): configurable runtime-token header (server + SDK) to avoid gateway Authorization collision [HYBIM-866] Jul 29, 2026
Comment thread sdks/python/src/agent_control/__init__.py Outdated
@josjeon
josjeon requested a review from abhinav-galileo August 6, 2026 16:09
Comment thread sdks/python/src/agent_control/__init__.py
Comment thread sdks/python/src/agent_control/client.py
Comment thread server/src/agent_control_server/auth_framework/config.py Outdated
Comment thread sdks/python/tests/test_init_validation.py
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>
@josjeon
josjeon requested a review from abhinav-galileo August 7, 2026 20:06
jjeonsplunk and others added 7 commits August 7, 2026 13:10
…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
josjeon force-pushed the hybim-741-runtime-token-configurable-header branch from 1f122bb to 53a6f6e Compare August 7, 2026 20:10
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.

4 participants