From 90732871d6f140ed62205288bc9080f65f6acc08 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Fri, 12 Jun 2026 09:44:39 -0700 Subject: [PATCH] cloud_requirements PAC support --- codex-rs/backend-client/src/client.rs | 69 ++++++++++++++++------ codex-rs/cloud-config/src/backend.rs | 36 +++++++---- codex-rs/cloud-config/src/backend_tests.rs | 27 +++++++++ codex-rs/cloud-config/src/bundle_loader.rs | 3 +- codex-rs/login/src/auth/manager.rs | 5 ++ codex-rs/login/src/outbound_proxy.rs | 2 +- 6 files changed, 112 insertions(+), 30 deletions(-) create mode 100644 codex-rs/cloud-config/src/backend_tests.rs diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index be444b6247..b718851437 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -9,6 +9,9 @@ use crate::types::TokenUsageProfile; use crate::types::TurnAttemptsSiblingTurnsResponse; use anyhow::Result; use codex_api::SharedAuthProvider; +use codex_client::ClientRouteClass; +use codex_client::OutboundProxyConfig; +use codex_client::build_reqwest_client_for_route; use codex_client::build_reqwest_client_with_custom_ca; use codex_client::with_chatgpt_cloudflare_cookie_store; use codex_login::CodexAuth; @@ -149,23 +152,36 @@ impl fmt::Debug for Client { impl Client { pub fn new(base_url: impl Into) -> Result { - let mut base_url = base_url.into(); - // Normalize common ChatGPT hostnames to include /backend-api so we hit the WHAM paths. - // Also trim trailing slashes for consistent URL building. - while base_url.ends_with('/') { - base_url.pop(); - } - if (base_url.starts_with("https://chatgpt.com") - || base_url.starts_with("https://chat.openai.com")) - && !base_url.contains("/backend-api") - { - base_url = format!("{base_url}/backend-api"); - } + let base_url = normalize_base_url(base_url.into()); let http = build_reqwest_client_with_custom_ca(with_chatgpt_cloudflare_cookie_store( reqwest::Client::builder(), ))?; + Ok(Self::with_http_client(base_url, http)) + } + + /// Creates an authenticated client routed for the cloud config bundle endpoint. + pub fn from_auth_for_config_bundle( + base_url: impl Into, + auth: &CodexAuth, + outbound_proxy_config: Option<&OutboundProxyConfig>, + ) -> Result { + let base_url = normalize_base_url(base_url.into()); let path_style = PathStyle::from_base_url(&base_url); - Ok(Self { + let request_url = config_bundle_url(&base_url, path_style); + let http = build_reqwest_client_for_route( + with_chatgpt_cloudflare_cookie_store(reqwest::Client::builder()), + &request_url, + ClientRouteClass::Api, + outbound_proxy_config, + )?; + Ok(Self::with_http_client(base_url, http) + .with_user_agent(get_codex_user_agent()) + .with_auth_provider(codex_model_provider::auth_provider_from_auth(auth))) + } + + fn with_http_client(base_url: String, http: reqwest::Client) -> Self { + let path_style = PathStyle::from_base_url(&base_url); + Self { base_url, http, auth_provider: codex_model_provider::unauthenticated_auth_provider(), @@ -173,7 +189,7 @@ impl Client { chatgpt_account_id: None, chatgpt_account_is_fedramp: false, path_style, - }) + } } pub fn from_auth(base_url: impl Into, auth: &CodexAuth) -> Result { @@ -422,10 +438,7 @@ impl Client { pub async fn get_config_bundle( &self, ) -> std::result::Result { - let url = match self.path_style { - PathStyle::CodexApi => format!("{}/api/codex/config/bundle", self.base_url), - PathStyle::ChatGptApi => format!("{}/wham/config/bundle", self.base_url), - }; + let url = config_bundle_url(&self.base_url, self.path_style); let req = self.http.get(&url).headers(self.headers()); let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?; self.decode_json::(&url, &ct, &body) @@ -664,6 +677,26 @@ impl Client { } } +fn normalize_base_url(mut base_url: String) -> String { + while base_url.ends_with('/') { + base_url.pop(); + } + if (base_url.starts_with("https://chatgpt.com") + || base_url.starts_with("https://chat.openai.com")) + && !base_url.contains("/backend-api") + { + base_url = format!("{base_url}/backend-api"); + } + base_url +} + +fn config_bundle_url(base_url: &str, path_style: PathStyle) -> String { + match path_style { + PathStyle::CodexApi => format!("{base_url}/api/codex/config/bundle"), + PathStyle::ChatGptApi => format!("{base_url}/wham/config/bundle"), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/cloud-config/src/backend.rs b/codex-rs/cloud-config/src/backend.rs index cb99316a70..4076de005d 100644 --- a/codex-rs/cloud-config/src/backend.rs +++ b/codex-rs/cloud-config/src/backend.rs @@ -6,6 +6,8 @@ use codex_config::CloudConfigFragment; use codex_config::CloudConfigTomlBundle; use codex_config::CloudRequirementsFragment; use codex_config::CloudRequirementsTomlBundle; +use codex_login::AuthManager; +use codex_login::AuthRouteConfig; use codex_login::CodexAuth; use std::future::Future; @@ -46,24 +48,34 @@ pub(crate) trait BundleClient: Send + Sync { pub(crate) struct BackendBundleClient { base_url: String, + auth_route_config: Option, } impl BackendBundleClient { - pub(crate) fn new(base_url: String) -> Self { - Self { base_url } + pub(crate) fn from_auth_manager(base_url: String, auth_manager: &AuthManager) -> Self { + Self { + base_url, + auth_route_config: auth_manager.auth_route_config().cloned(), + } } } impl BundleClient for BackendBundleClient { async fn get_bundle(&self, auth: &CodexAuth) -> Result { - let client = BackendClient::from_auth(self.base_url.clone(), auth) - .inspect_err(|err| { - tracing::warn!( - error = %err, - "Failed to construct backend client for cloud config bundle" - ); - }) - .map_err(|_| BundleRequestError::Retryable(RetryableFailureKind::BackendClientInit))?; + let client = BackendClient::from_auth_for_config_bundle( + self.base_url.clone(), + auth, + self.auth_route_config + .as_ref() + .map(AuthRouteConfig::route_config), + ) + .inspect_err(|err| { + tracing::warn!( + error = %err, + "Failed to construct backend client for cloud config bundle" + ); + }) + .map_err(|_| BundleRequestError::Retryable(RetryableFailureKind::BackendClientInit))?; let response = client .get_config_bundle() @@ -134,3 +146,7 @@ fn requirements_fragment_from_delivered( contents: fragment.contents, } } + +#[cfg(test)] +#[path = "backend_tests.rs"] +mod tests; diff --git a/codex-rs/cloud-config/src/backend_tests.rs b/codex-rs/cloud-config/src/backend_tests.rs new file mode 100644 index 0000000000..76930cb099 --- /dev/null +++ b/codex-rs/cloud-config/src/backend_tests.rs @@ -0,0 +1,27 @@ +use super::*; +use codex_config::types::AuthCredentialsStoreMode; +use codex_login::AuthRouteConfig; +use pretty_assertions::assert_eq; + +#[tokio::test] +async fn cloud_bundle_client_preserves_system_proxy_config() { + let codex_home = tempfile::tempdir().expect("create temp dir"); + let auth_manager = AuthManager::new_with_auth_route_config( + codex_home.path().to_path_buf(), + /*enable_codex_api_key_env*/ false, + AuthCredentialsStoreMode::File, + Some("https://chatgpt.com/backend-api/".to_string()), + Some(AuthRouteConfig::respect_system_proxy()), + ) + .await; + + let client = BackendBundleClient::from_auth_manager( + "https://chatgpt.com/backend-api/".to_string(), + &auth_manager, + ); + + assert_eq!( + client.auth_route_config, + Some(AuthRouteConfig::respect_system_proxy()) + ); +} diff --git a/codex-rs/cloud-config/src/bundle_loader.rs b/codex-rs/cloud-config/src/bundle_loader.rs index e4266c6195..ae2d680438 100644 --- a/codex-rs/cloud-config/src/bundle_loader.rs +++ b/codex-rs/cloud-config/src/bundle_loader.rs @@ -24,9 +24,10 @@ pub fn cloud_config_bundle_loader( chatgpt_base_url: String, codex_home: PathBuf, ) -> CloudConfigBundleLoader { + let backend_client = BackendBundleClient::from_auth_manager(chatgpt_base_url, &auth_manager); let service = CloudConfigBundleService::new( auth_manager, - Arc::new(BackendBundleClient::new(chatgpt_base_url)), + Arc::new(backend_client), codex_home, CLOUD_CONFIG_BUNDLE_TIMEOUT, ); diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 26580a672d..a4a29e6f86 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -2196,6 +2196,11 @@ impl AuthManager { self.enable_codex_api_key_env } + /// Returns the route-selection policy used by auth-owned clients. + pub fn auth_route_config(&self) -> Option<&AuthRouteConfig> { + self.auth_route_config.as_ref() + } + /// Convenience constructor returning an `Arc` wrapper. pub async fn shared( codex_home: PathBuf, diff --git a/codex-rs/login/src/outbound_proxy.rs b/codex-rs/login/src/outbound_proxy.rs index bf7dfbeb4d..f5523bb616 100644 --- a/codex-rs/login/src/outbound_proxy.rs +++ b/codex-rs/login/src/outbound_proxy.rs @@ -16,7 +16,7 @@ impl AuthRouteConfig { } } - pub(crate) fn route_config(&self) -> &OutboundProxyConfig { + pub fn route_config(&self) -> &OutboundProxyConfig { &self.route_config } }