diff --git a/codex-rs/codex-client/src/outbound_proxy.rs b/codex-rs/codex-client/src/outbound_proxy.rs index deb1acebdd..9ec2a6d28e 100644 --- a/codex-rs/codex-client/src/outbound_proxy.rs +++ b/codex-rs/codex-client/src/outbound_proxy.rs @@ -254,13 +254,6 @@ impl RequestOrigin { .or_else(|| default_port_for_scheme(&scheme))?; Some(Self { scheme, host, port }) } - - fn cache_key(&self, include_auto_detect: bool) -> String { - format!( - "{}://{}:{}:auto_detect={include_auto_detect}", - self.scheme, self.host, self.port - ) - } } fn default_port_for_scheme(scheme: &str) -> Option { @@ -292,12 +285,12 @@ fn resolve_system_proxy( origin: &RequestOrigin, include_auto_detect: bool, ) -> SystemProxyDecision { - if let Some(decision) = cached_system_proxy_decision(origin, include_auto_detect) { + if let Some(decision) = cached_system_proxy_decision(request_url, include_auto_detect) { return decision; } let decision = resolve_platform_system_proxy(request_url, origin, include_auto_detect); - cache_system_proxy_decision(origin, include_auto_detect, decision.clone()); + cache_system_proxy_decision(request_url, include_auto_detect, decision.clone()); decision } @@ -332,12 +325,12 @@ static SYSTEM_PROXY_CACHE: OnceLock Option { let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new())); let mut cache = cache.lock().ok()?; - let key = origin.cache_key(include_auto_detect); + let key = system_proxy_cache_key(request_url, include_auto_detect); let cached = cache.get(&key)?; if cached.expires_at > Instant::now() { return Some(cached.decision.clone()); @@ -347,14 +340,14 @@ fn cached_system_proxy_decision( } fn cache_system_proxy_decision( - origin: &RequestOrigin, + request_url: &str, include_auto_detect: bool, decision: SystemProxyDecision, ) { let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new())); if let Ok(mut cache) = cache.lock() { cache.insert( - origin.cache_key(include_auto_detect), + system_proxy_cache_key(request_url, include_auto_detect), CachedSystemProxyDecision { decision, expires_at: Instant::now() + SYSTEM_PROXY_CACHE_TTL, @@ -363,6 +356,10 @@ fn cache_system_proxy_decision( } } +fn system_proxy_cache_key(request_url: &str, include_auto_detect: bool) -> String { + format!("{request_url}:auto_detect={include_auto_detect}") +} + fn conventional_proxy_env_present() -> bool { proxy_env_value("HTTPS_PROXY").is_some() || proxy_env_value("HTTP_PROXY").is_some() @@ -628,4 +625,12 @@ mod tests { assert!(no_proxy_matches_origin("auth.openai.com:443", &origin)); assert!(!no_proxy_matches_origin("auth.openai.com:8443", &origin)); } + + #[test] + fn system_proxy_cache_key_preserves_url_specific_pac_decisions() { + assert_ne!( + system_proxy_cache_key("https://auth.openai.com/oauth/token", false), + system_proxy_cache_key("https://auth.openai.com/oauth/revoke", false) + ); + } } diff --git a/codex-rs/login/src/auth/agent_identity.rs b/codex-rs/login/src/auth/agent_identity.rs index bae0d371e3..881dfa005c 100644 --- a/codex-rs/login/src/auth/agent_identity.rs +++ b/codex-rs/login/src/auth/agent_identity.rs @@ -1,10 +1,11 @@ use codex_agent_identity::AgentIdentityKey; +use codex_agent_identity::agent_task_registration_url; use codex_agent_identity::register_agent_task; use codex_client::OutboundProxyConfig; use codex_protocol::account::PlanType as AccountPlanType; use std::env; -use crate::default_client::build_auth_reqwest_client_with_proxy_config; +use crate::default_client::build_default_reqwest_client_for_auth_route; use super::storage::AgentIdentityAuthRecord; @@ -27,8 +28,10 @@ impl AgentIdentityAuth { outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> std::io::Result { let agent_identity_authapi_base_url = agent_identity_authapi_base_url(); - let client = build_auth_reqwest_client_with_proxy_config( - &agent_identity_authapi_base_url, + let task_registration_url = + agent_task_registration_url(&agent_identity_authapi_base_url, &record.agent_runtime_id); + let client = build_default_reqwest_client_for_auth_route( + &task_registration_url, outbound_proxy_config, )?; let process_task_id = diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs index cf3ac7ba27..5111c57f9e 100644 --- a/codex-rs/login/src/auth/default_client.rs +++ b/codex-rs/login/src/auth/default_client.rs @@ -4,6 +4,7 @@ //! Use [`crate::default_client`] or [`codex_login::default_client`] from other crates in this //! workspace. +use codex_client::BuildCustomCaTransportError; use codex_client::BuildProxiedHttpClientError; use codex_client::CodexHttpClient; pub use codex_client::CodexRequestBuilder; @@ -39,7 +40,6 @@ pub static USER_AGENT_SUFFIX: LazyLock>> = LazyLock::new(|| pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs"; pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"; pub const RESIDENCY_HEADER_NAME: &str = "x-openai-internal-codex-residency"; -const DEFAULT_AUTH_ROUTE_URL: &str = "https://auth.openai.com/oauth/token"; pub use codex_config::ResidencyRequirement; @@ -198,13 +198,6 @@ pub fn create_client() -> CodexHttpClient { CodexHttpClient::new(inner) } -pub fn create_client_with_proxy_config( - outbound_proxy_config: Option<&OutboundProxyConfig>, -) -> CodexHttpClient { - let inner = build_reqwest_client_with_proxy_config(outbound_proxy_config); - CodexHttpClient::new(inner) -} - /// Builds the default reqwest client used for ordinary Codex HTTP traffic. /// /// This starts from the standard Codex user agent, default headers, and sandbox-specific proxy @@ -212,13 +205,7 @@ pub fn create_client_with_proxy_config( /// `SSL_CERT_FILE`. The function remains infallible for compatibility with existing call sites, so /// a custom-CA or builder failure is logged and falls back to `reqwest::Client::new()`. pub fn build_reqwest_client() -> reqwest::Client { - build_reqwest_client_with_proxy_config(/*outbound_proxy_config*/ None) -} - -pub fn build_reqwest_client_with_proxy_config( - outbound_proxy_config: Option<&OutboundProxyConfig>, -) -> reqwest::Client { - try_build_reqwest_client_with_proxy_config(outbound_proxy_config).unwrap_or_else(|error| { + try_build_reqwest_client().unwrap_or_else(|error| { tracing::warn!(error = %error, "failed to build default reqwest client"); with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder()) .build() @@ -236,25 +223,16 @@ pub fn build_reqwest_client_with_proxy_config( /// /// Callers that need a structured CA-loading failure instead of the legacy logged fallback can use /// this method directly. -pub fn try_build_reqwest_client() -> Result { - try_build_reqwest_client_with_proxy_config(/*outbound_proxy_config*/ None) +pub fn try_build_reqwest_client() -> Result { + build_reqwest_client_with_custom_ca(default_reqwest_client_builder()) } -pub fn try_build_reqwest_client_with_proxy_config( - outbound_proxy_config: Option<&OutboundProxyConfig>, -) -> Result { +fn default_reqwest_client_builder() -> reqwest::ClientBuilder { let mut builder = reqwest::Client::builder().default_headers(default_headers()); - let sandboxed = is_sandboxed(); - if sandboxed { + if is_sandboxed() { builder = builder.no_proxy(); } - builder = with_chatgpt_cloudflare_cookie_store(builder); - - if sandboxed { - return build_reqwest_client_with_custom_ca(builder).map_err(Into::into); - } - - build_auth_reqwest_client_with_builder(builder, DEFAULT_AUTH_ROUTE_URL, outbound_proxy_config) + with_chatgpt_cloudflare_cookie_store(builder) } pub(crate) fn build_auth_reqwest_client_with_proxy_config( @@ -268,6 +246,26 @@ pub(crate) fn build_auth_reqwest_client_with_proxy_config( ) } +/// Builds the standard Codex reqwest client for a known auth destination. +pub(crate) fn build_default_reqwest_client_for_auth_route( + endpoint: &str, + outbound_proxy_config: Option<&OutboundProxyConfig>, +) -> Result { + let builder = default_reqwest_client_builder(); + if is_sandboxed() { + return build_reqwest_client_with_custom_ca(builder).map_err(Into::into); + } + build_auth_reqwest_client_with_builder(builder, endpoint, outbound_proxy_config) +} + +pub(crate) fn create_client_for_auth_route( + endpoint: &str, + outbound_proxy_config: Option<&OutboundProxyConfig>, +) -> Result { + build_default_reqwest_client_for_auth_route(endpoint, outbound_proxy_config) + .map(CodexHttpClient::new) +} + fn build_auth_reqwest_client_with_builder( builder: reqwest::ClientBuilder, endpoint: &str, diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 56278f92ca..380c3172ce 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -17,6 +17,7 @@ use std::sync::atomic::Ordering; use tokio::sync::Semaphore; use tokio::sync::watch; +use codex_agent_identity::agent_identity_jwks_url; use codex_agent_identity::decode_agent_identity_jwt; use codex_agent_identity::fetch_agent_identity_jwks; use codex_app_server_protocol::AuthMode; @@ -32,14 +33,12 @@ pub use crate::auth::storage::AuthDotJson; use crate::auth::storage::AuthStorageBackend; use crate::auth::storage::create_auth_storage; use crate::auth::util::try_parse_error_message; -use crate::default_client::build_auth_reqwest_client_with_proxy_config; -use crate::default_client::create_client; -use crate::default_client::create_client_with_proxy_config; +use crate::default_client::build_default_reqwest_client_for_auth_route; +use crate::default_client::create_client_for_auth_route; use crate::outbound_proxy::outbound_proxy_config_from_network_config; use crate::token_data::TokenData; use crate::token_data::parse_chatgpt_jwt_claims; use crate::token_data::parse_jwt_expiration; -use codex_client::CodexHttpClient; use codex_client::OutboundProxyConfig; use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::NetworkConfigToml; @@ -84,7 +83,6 @@ pub struct ChatgptAuthTokens { #[derive(Debug, Clone)] struct ChatgptAuthState { auth_dot_json: Arc>>, - client: CodexHttpClient, } const TOKEN_REFRESH_INTERVAL: i64 = 8; @@ -210,7 +208,6 @@ impl CodexAuth { outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> std::io::Result { let auth_mode = auth_dot_json.resolved_mode(); - let client = create_client_with_proxy_config(outbound_proxy_config); if auth_mode == ApiAuthMode::ApiKey { let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else { return Err(std::io::Error::other("API key auth is missing a key.")); @@ -234,7 +231,6 @@ impl CodexAuth { let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode); let state = ChatgptAuthState { auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))), - client, }; match auth_mode { @@ -467,10 +463,8 @@ impl CodexAuth { agent_identity: None, }; - let client = create_client(); let state = ChatgptAuthState { auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))), - client, }; let dummy_auth_id = NEXT_DUMMY_AUTH_ID.fetch_add(1, Ordering::Relaxed); let storage = create_auth_storage( @@ -500,10 +494,6 @@ impl ChatgptAuth { fn storage(&self) -> &Arc { &self.storage } - - fn client(&self) -> &CodexHttpClient { - &self.state.client - } } pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY"; @@ -538,8 +528,8 @@ async fn verified_agent_identity_record( outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> std::io::Result { AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?; - let client = - build_auth_reqwest_client_with_proxy_config(chatgpt_base_url, outbound_proxy_config)?; + let jwks_url = agent_identity_jwks_url(chatgpt_base_url); + let client = build_default_reqwest_client_for_auth_route(&jwks_url, outbound_proxy_config)?; let jwks = fetch_agent_identity_jwks(&client, chatgpt_base_url) .await .map_err(std::io::Error::other)?; @@ -902,7 +892,7 @@ fn persist_tokens( // The caller is responsible for persisting any returned tokens. async fn request_chatgpt_token_refresh( refresh_token: String, - client: &CodexHttpClient, + outbound_proxy_config: Option<&OutboundProxyConfig>, ) -> Result { let refresh_request = RefreshRequest { client_id: CLIENT_ID, @@ -911,6 +901,8 @@ async fn request_chatgpt_token_refresh( }; let endpoint = refresh_token_endpoint(); + let client = create_client_for_auth_route(&endpoint, outbound_proxy_config) + .map_err(|err| RefreshTokenError::Transient(err.into()))?; // Use shared client factory to include standard headers let response = client @@ -2020,7 +2012,9 @@ impl AuthManager { auth: &ChatgptAuth, refresh_token: String, ) -> Result<(), RefreshTokenError> { - let refresh_response = request_chatgpt_token_refresh(refresh_token, auth.client()).await?; + let refresh_response = + request_chatgpt_token_refresh(refresh_token, self.outbound_proxy_config.as_ref()) + .await?; persist_tokens( auth.storage(), diff --git a/codex-rs/login/src/auth/revoke.rs b/codex-rs/login/src/auth/revoke.rs index 306be5b86e..086b8684f4 100644 --- a/codex-rs/login/src/auth/revoke.rs +++ b/codex-rs/login/src/auth/revoke.rs @@ -18,7 +18,7 @@ use super::manager::REVOKE_TOKEN_URL; use super::manager::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR; use super::storage::AuthDotJson; use super::util::try_parse_error_message; -use crate::default_client::create_client_with_proxy_config; +use crate::default_client::create_client_for_auth_route; use crate::token_data::TokenData; const REVOKE_HTTP_TIMEOUT: Duration = Duration::from_secs(10); @@ -61,8 +61,8 @@ pub(crate) async fn revoke_auth_tokens_with_proxy_config( return Ok(()); }; - let client = create_client_with_proxy_config(outbound_proxy_config); let endpoint = revoke_token_endpoint(); + let client = create_client_for_auth_route(&endpoint, outbound_proxy_config)?; revoke_oauth_token(&client, endpoint.as_str(), token, kind, REVOKE_HTTP_TIMEOUT).await }