feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] - #252

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

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

Conversation

@josjeon

@josjeonjosjeon commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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

Two options for resolving the collision

The collision can be fixed on either side of the boundary. This PR is Option A (agent-control side). A parallel spike sketches Option B (gateway side) for comparison (api-gateway !1351).

Option A — agent-control side (this PR)Option B — gateway side
Where the change livesAO server + Python SDKAPI Gateway AuthenticationFilter
MechanismRuntime token moves to a configurable header (e.g. X-Agent-Control-Runtime-Token); gateway Authorization is left aloneGateway preserves the caller's Authorization on an opt-in, path-scoped allow-list
Gateway auth pathuntouchedmodified (guarded, default-off)
AO / SDK changerequirednone
Blast radiusAO onlygateway — contained to configured paths, off by default
Needs platform + architecture/compliance sign-offnoyes
Referencethis PRapi-gateway !1351

Option A keeps the gateway's auth path entirely out of scope — no shared-infrastructure change, no platform/compliance review — which is why it's the front-runner. Option B is captured separately so the "fix it in the gateway" path is a concrete artifact to compare against rather than a hypothetical.

The fix

One knob, wired on both sides:

SideHow to set itEffect
ServerAGENT_CONTROL_RUNTIME_TOKEN_HEADER envVerifier reads the runtime token from this header
SDKsame env, or runtime_token_header= paramClient 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 — PENDING. Not yet run. This is the only end-to-end verification of the "gateway overwrites Authorization" premise; everything above is unit/integration scope. Tracked separately.

🤖 Generated with Claude Code

@josjeonjosjeon changed the title feat(auth): make runtime JWT verifier header configurablefeat: configurable runtime-token header (server + SDK)Jul 14, 2026
@josjeon
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch 2 times, most recently from 79e7a19 to 28f58f7CompareJuly 15, 2026 16:27
jjeonsplunkand others added 2 commits July 15, 2026 09:41
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
josjeonforce-pushed the hybim-741-configurable-runtime-token-header branch from 28f58f7 to 48b69acCompareJuly 15, 2026 16:42
@josjeon
josjeon marked this pull request as ready for review July 15, 2026 16:56
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK)[Need Discussion] feat: configurable runtime-token header (server + SDK)Jul 15, 2026
@josjeonjosjeon changed the title [Need Discussion] feat: configurable runtime-token header (server + SDK)feat: configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
@josjeon
josjeon marked this pull request as draft July 15, 2026 17:01
@josjeonjosjeon changed the title feat: configurable runtime-token header (server + SDK) [Need Discussion] feat: [Option A] configurable runtime-token header (server + SDK) [Need Discussion] Jul 15, 2026
if (
self._api_key
and "Authorization" not in request.headers
and not runtime_token_on_dedicated_header

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the configured request credential when the runtime token is sent on a dedicated header? In a two-hop setup, the outer credential authenticates the gateway while the runtime token authenticates Agent Control, so both are required on the evaluation request. Please update the custom-header test to assert that both headers are sent.

api_key: str | None = None,
api_key_header: str | None = None,
runtime_auth_mode: RuntimeAuthMode | str | None = None,
runtime_token_header: str | None = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the existing positional constructor order? Inserting runtime_token_header before runtime_token_cache changes how existing positional calls bind and can fail when the cache is treated as a header. Making the new option keyword-only after the existing parameters would avoid the compatibility break.

request auth when the exchange endpoint is unavailable. ``jwt``
requires a successful exchange. ``api_key`` and ``none`` keep
evaluation requests on the normal request-auth path.
runtime_token_header: HTTP header name to send the runtime token

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose runtime_token_header through agent_control.init() as well? The high-level flow creates clients internally, so constructor-only configuration otherwise requires setting a process-wide environment variable.

response = client.post(
"/api/v1/evaluation",
headers={
# Simulates the O11y gateway's downstream identity JWT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small OSS wording suggestion: could we refer to this as an upstream gateway rather than an O11y gateway, to keep the committed test generic?

Sign up for freeto 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.

3 participants

@josjeon@abhinav-galileo@jjeonsplunk