Files
codex/codex-rs/model-provider/src/auth.rs
efrazer-oai c08177f7d0 refactor: load agent identity runtime eagerly (#19763)
## Summary

AgentIdentity auth previously registered the process task lazily behind
a `OnceCell`. That meant the auth object could be constructed before its
runtime task binding was known.

This PR makes AgentIdentity auth load the runtime task at auth load time
and stores the resulting process task id directly on the auth object.
The model-provider call path can then read a concrete task id instead of
handling a missing lazy value.

## Stack

1. [refactor: make auth loading
async](https://github.com/openai/codex/pull/19762) (merged)
2. **This PR:** [refactor: load AgentIdentity runtime
eagerly](https://github.com/openai/codex/pull/19763)
3. [fix: configure AgentIdentity AuthAPI base
URL](https://github.com/openai/codex/pull/19904)
4. [feat: verify AgentIdentity JWTs with
JWKS](https://github.com/openai/codex/pull/19764)

## Important call sites

| Area | Change |
| --- | --- |
| `AgentIdentityAuth::load` | Registers the process task during auth
loading and stores `process_task_id`. |
| `CodexAuth::from_agent_identity_jwt` | Awaits AgentIdentity auth
loading. |
| model-provider auth | Reads a concrete `process_task_id` instead of an
optional lazy value. |
| AgentIdentity auth tests | Mock task registration now covers eager
runtime allocation. |

## Design decisions

AgentIdentity auth now treats task registration as part of constructing
a usable auth object. That matches how callers use the value: once auth
is present, the model-provider path expects the task-scoped assertion
data to be ready.

## Testing

Tests: targeted Rust auth test compilation, formatter, scoped Clippy
fix, and Bazel lock check.
2026-04-27 21:09:26 -07:00

138 lines
4.4 KiB
Rust

use std::sync::Arc;
use codex_agent_identity::AgentIdentityKey;
use codex_agent_identity::AgentTaskAuthorizationTarget;
use codex_agent_identity::authorization_header_for_agent_task;
use codex_api::AuthProvider;
use codex_api::SharedAuthProvider;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderInfo;
use http::HeaderMap;
use http::HeaderValue;
use crate::bearer_auth_provider::BearerAuthProvider;
#[derive(Clone, Debug)]
struct AgentIdentityAuthProvider {
auth: codex_login::auth::AgentIdentityAuth,
}
impl AuthProvider for AgentIdentityAuthProvider {
fn add_auth_headers(&self, headers: &mut HeaderMap) {
let record = self.auth.record();
let header_value = authorization_header_for_agent_task(
AgentIdentityKey {
agent_runtime_id: &record.agent_runtime_id,
private_key_pkcs8_base64: &record.agent_private_key,
},
AgentTaskAuthorizationTarget {
agent_runtime_id: &record.agent_runtime_id,
task_id: self.auth.process_task_id(),
},
)
.map_err(std::io::Error::other);
if let Ok(header_value) = header_value
&& let Ok(header) = HeaderValue::from_str(&header_value)
{
let _ = headers.insert(http::header::AUTHORIZATION, header);
}
if let Ok(header) = HeaderValue::from_str(self.auth.account_id()) {
let _ = headers.insert("ChatGPT-Account-ID", header);
}
if self.auth.is_fedramp_account() {
let _ = headers.insert("X-OpenAI-Fedramp", HeaderValue::from_static("true"));
}
}
}
// Some providers are meant to send no auth headers. Examples include local OSS
// providers and custom test providers with `requires_openai_auth = false`.
#[derive(Clone, Debug)]
struct UnauthenticatedAuthProvider;
impl AuthProvider for UnauthenticatedAuthProvider {
fn add_auth_headers(&self, _headers: &mut HeaderMap) {}
}
pub fn unauthenticated_auth_provider() -> SharedAuthProvider {
Arc::new(UnauthenticatedAuthProvider)
}
/// Returns the provider-scoped auth manager when this provider uses command-backed auth.
///
/// Providers without custom auth continue using the caller-supplied base manager, when present.
pub(crate) fn auth_manager_for_provider(
auth_manager: Option<Arc<AuthManager>>,
provider: &ModelProviderInfo,
) -> Option<Arc<AuthManager>> {
match provider.auth.clone() {
Some(config) => Some(AuthManager::external_bearer_only(config)),
None => auth_manager,
}
}
pub(crate) fn resolve_provider_auth(
auth: Option<&CodexAuth>,
provider: &ModelProviderInfo,
) -> codex_protocol::error::Result<SharedAuthProvider> {
if let Some(auth) = bearer_auth_for_provider(provider)? {
return Ok(Arc::new(auth));
}
Ok(match auth {
Some(auth) => auth_provider_from_auth(auth),
None => unauthenticated_auth_provider(),
})
}
fn bearer_auth_for_provider(
provider: &ModelProviderInfo,
) -> codex_protocol::error::Result<Option<BearerAuthProvider>> {
if let Some(api_key) = provider.api_key()? {
return Ok(Some(BearerAuthProvider::new(api_key)));
}
if let Some(token) = provider.experimental_bearer_token.clone() {
return Ok(Some(BearerAuthProvider::new(token)));
}
Ok(None)
}
/// Builds request-header auth for a first-party Codex auth snapshot.
pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
match auth {
CodexAuth::AgentIdentity(auth) => {
Arc::new(AgentIdentityAuthProvider { auth: auth.clone() })
}
CodexAuth::ApiKey(_) | CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
Arc::new(BearerAuthProvider {
token: auth.get_token().ok(),
account_id: auth.get_account_id(),
is_fedramp_account: auth.is_fedramp_account(),
})
}
}
}
#[cfg(test)]
mod tests {
use codex_model_provider_info::WireApi;
use codex_model_provider_info::create_oss_provider_with_base_url;
use super::*;
#[test]
fn unauthenticated_auth_provider_adds_no_headers() {
let provider =
create_oss_provider_with_base_url("http://localhost:11434/v1", WireApi::Responses);
let auth = resolve_provider_auth(/*auth*/ None, &provider).expect("auth should resolve");
assert!(auth.to_auth_headers().is_empty());
}
}