mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
refactor: route Codex auth through AuthProvider (#18811)
## Summary This PR moves Codex backend request authentication from direct bearer-token handling to `AuthProvider`. The new `codex-auth-provider` crate defines the shared request-auth trait. `CodexAuth::provider()` returns a provider that can apply all headers needed for the selected auth mode. This lets ChatGPT token auth and AgentIdentity auth share the same callsite path: - ChatGPT token auth applies bearer auth plus account/FedRAMP headers where needed. - AgentIdentity auth applies AgentAssertion plus account/FedRAMP headers where needed. Reference old stack: https://github.com/openai/codex/pull/17387/changes ## Callsite Migration | Area | Change | | --- | --- | | backend-client | accepts an `AuthProvider` instead of a raw token/header | | chatgpt client/connectors | applies auth through `CodexAuth::provider()` | | cloud tasks | keeps Codex-backend gating, applies auth through provider | | cloud requirements | uses Codex-backend auth checks and provider headers | | app-server remote control | applies provider headers for backend calls | | MCP Apps/connectors | gates on `uses_codex_backend()` and keys caches from generic account getters | | model refresh | treats AgentIdentity as Codex-backend auth | | OpenAI file upload path | rejects non-Codex-backend auth before applying headers | | core client setup | keeps model-provider auth flow and allows AgentIdentity through provider-backed OpenAI auth | ## Stack 1. https://github.com/openai/codex/pull/18757: full revert 2. https://github.com/openai/codex/pull/18871: isolated Agent Identity crate 3. https://github.com/openai/codex/pull/18785: explicit AgentIdentity auth mode and startup task allocation 4. This PR: migrate Codex backend auth callsites through AuthProvider 5. https://github.com/openai/codex/pull/18904: accept AgentIdentity JWTs and load `CODEX_AGENT_IDENTITY` ## Testing Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
This commit is contained in:
1 parent
a9f75e5cda
commit
5882f3f95e
55 files changed
+551
-490
No files matched your search
@@ -1,12 +1,73 @@
|
||||
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 = self
|
||||
.auth
|
||||
.process_task_id()
|
||||
.ok_or_else(|| std::io::Error::other("agent identity process task is not initialized"))
|
||||
.and_then(|task_id| {
|
||||
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,
|
||||
},
|
||||
)
|
||||
.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.
|
||||
@@ -20,45 +81,63 @@ pub(crate) fn auth_manager_for_provider(
|
||||
}
|
||||
}
|
||||
|
||||
fn bearer_auth_provider_from_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> codex_protocol::error::Result<BearerAuthProvider> {
|
||||
if let Some(api_key) = provider.api_key()? {
|
||||
return Ok(BearerAuthProvider {
|
||||
token: Some(api_key),
|
||||
account_id: None,
|
||||
is_fedramp_account: false,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(token) = provider.experimental_bearer_token.clone() {
|
||||
return Ok(BearerAuthProvider {
|
||||
token: Some(token),
|
||||
account_id: None,
|
||||
is_fedramp_account: false,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(auth) = auth {
|
||||
let token = auth.get_token()?;
|
||||
Ok(BearerAuthProvider {
|
||||
token: Some(token),
|
||||
account_id: auth.get_account_id(),
|
||||
is_fedramp_account: auth.is_fedramp_account(),
|
||||
})
|
||||
} else {
|
||||
Ok(BearerAuthProvider {
|
||||
token: None,
|
||||
account_id: None,
|
||||
is_fedramp_account: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_provider_auth(
|
||||
auth: Option<&CodexAuth>,
|
||||
provider: &ModelProviderInfo,
|
||||
) -> codex_protocol::error::Result<SharedAuthProvider> {
|
||||
Ok(Arc::new(bearer_auth_provider_from_auth(auth, provider)?))
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user