Integrate runtime authentication into model requests (#57171)

## What changed

- Add opt-in runtime authentication through `ModelClient::with_runtime_auth`, requiring the provider and runtime manager to share the selected OpenAI credentials.
- Keep runtime identity, credentials, routing, and transport selection consistent for each request and its retries, signing a fresh assertion for each attempt.
- Apply authentication asynchronously to WebSocket handshakes, replace stale authentication headers, and reconnect when the runtime changes.
- Reject account, organization, or project selectors that conflict with the registered runtime, and preserve gateway OAuth composition.

## Testing

Add coverage for dual-header and assertion-only authentication, HTTP retries, runtime replacement and account switches, stale WebSocket headers, selector mismatches, gateway OAuth failures, transport revocation, and Realtime compatibility.

GitOrigin-RevId: 45b9efc2c80481406a0f6f6396a8cdd21d933c12
This commit is contained in:
cooper-oai
2026-10-26 18:06:07 +00:00
committed by copyberry
parent 2d4c131524
commit cf29971d93
29 changed files with 1192 additions and 207 deletions
+47 -45
View File
@@ -4,14 +4,13 @@ use crate::ActorInfo;
use crate::RegistrationError;
use crate::RuntimeAgentInstance;
use crate::RuntimeAuthError;
use crate::RuntimeAuthManager;
use codex_api::AuthError;
use codex_api::AuthHeaderTelemetry;
use codex_api::AuthHeadersFuture;
use codex_api::AuthProvider;
use codex_api::AuthProviderFuture;
use codex_features::RuntimeAuthMode;
use codex_http_client::CHATGPT_ACCOUNT_ID_HEADER;
use codex_http_client::HttpClientFactory;
use codex_http_client::Request;
use codex_login::AuthChangeState;
use codex_login::CodexAuth;
use http::HeaderMap;
@@ -41,50 +40,12 @@ impl std::fmt::Debug for RuntimeAuth {
}
}
/// Auth adapter for OpenAI clients. Share its manager across requests and threads;
/// never select this provider for a third-party endpoint.
pub struct RuntimeAuthProvider {
manager: Arc<RuntimeAuthManager>,
/// Request-scoped provider: retries sign fresh assertions from the same selected runtime/credential.
struct RuntimeRequestAuth {
auth: RuntimeAuth,
mode: RuntimeAuthMode,
}
impl RuntimeAuthProvider {
pub fn new(manager: Arc<RuntimeAuthManager>, mode: RuntimeAuthMode) -> Self {
Self { manager, mode }
}
}
impl AuthProvider for RuntimeAuthProvider {
fn add_auth_headers(&self, _headers: &mut HeaderMap) -> Result<(), AuthError> {
Err(AuthError::Build(
"authentication requires async header resolution".to_string(),
))
}
fn resolve_auth_headers(&self) -> AuthHeadersFuture<'_> {
Box::pin(async move {
let auth = self.manager.auth().await?;
auth.auth_headers(self.mode)
})
}
fn apply_auth(&self, mut request: Request) -> AuthProviderFuture<'_> {
Box::pin(async move {
let headers = self.resolve_auth_headers().await?;
for name in [
AUTHORIZATION.as_str(),
AGENT_ASSERTION,
"chatgpt-account-id",
"x-openai-fedramp",
] {
request.headers.remove(name);
}
request.headers.extend(headers);
Ok(request)
})
}
}
impl RuntimeAuth {
pub fn runtime(&self) -> &Arc<RuntimeAgentInstance> {
&self.runtime
@@ -102,6 +63,11 @@ impl RuntimeAuth {
self.change_state
}
/// Use this selection for a request and its retries; select auth again for the next request.
pub fn into_auth_provider(self, mode: RuntimeAuthMode) -> codex_api::SharedAuthProvider {
Arc::new(RuntimeRequestAuth { auth: self, mode })
}
fn auth_headers(&self, mode: RuntimeAuthMode) -> Result<HeaderMap, AuthError> {
let assertion = self
.runtime()
@@ -125,7 +91,7 @@ impl RuntimeAuth {
}
if let ActorInfo::Chatgpt { account_id, .. } = self.runtime().actor().info() {
headers.insert(
"chatgpt-account-id",
CHATGPT_ACCOUNT_ID_HEADER,
HeaderValue::from_str(account_id)
.map_err(|_| AuthError::Build("invalid runtime account header".to_string()))?,
);
@@ -137,6 +103,29 @@ impl RuntimeAuth {
}
}
impl AuthProvider for RuntimeRequestAuth {
fn add_auth_headers(&self, headers: &mut HeaderMap) -> Result<(), AuthError> {
headers.extend(self.auth.auth_headers(self.mode)?);
Ok(())
}
fn auth_header_telemetry(&self) -> Result<AuthHeaderTelemetry, AuthError> {
Ok(AuthHeaderTelemetry {
attached: true,
name: Some("authorization"),
})
}
fn apply_auth_headers(&self, headers: HeaderMap) -> AuthHeadersFuture<'_> {
Box::pin(async move {
Ok(replace_auth_headers(
headers,
self.resolve_auth_headers().await?,
))
})
}
}
fn secret_header(value: &str) -> Result<HeaderValue, AuthError> {
let mut header = HeaderValue::from_str(value)
.map_err(|_| AuthError::Build("invalid runtime authentication header".to_string()))?;
@@ -173,3 +162,16 @@ impl From<RuntimeAuthError> for AuthError {
}
}
}
fn replace_auth_headers(mut headers: HeaderMap, auth: HeaderMap) -> HeaderMap {
for name in [
AUTHORIZATION.as_str(),
AGENT_ASSERTION,
CHATGPT_ACCOUNT_ID_HEADER,
"x-openai-fedramp",
] {
headers.remove(name);
}
headers.extend(auth);
headers
}