From 8cd829a70a7d89125f01d2fd7e4153e0a71e6c4e 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 | 24 +++++++ codex-rs/cloud-config/src/bundle_loader.rs | 3 +- codex-rs/core/src/config/config_tests.rs | 41 ++++++++++++ codex-rs/core/src/config/mod.rs | 6 +- codex-rs/login/src/auth/manager.rs | 5 ++ codex-rs/login/src/outbound_proxy.rs | 38 +++-------- codex-rs/tui/src/session_archive_commands.rs | 40 +++++++----- 9 files changed, 189 insertions(+), 73 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 52275eb4be..b15aec7e0f 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -8,6 +8,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; @@ -145,23 +148,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(), @@ -169,7 +185,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 { @@ -425,10 +441,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) @@ -646,6 +659,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..5f83ac497b 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() + .and_then(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..77d1e922df --- /dev/null +++ b/codex-rs/cloud-config/src/backend_tests.rs @@ -0,0 +1,24 @@ +use super::*; +use codex_config::types::AuthCredentialsStoreMode; +use codex_login::AuthRouteConfig; +use pretty_assertions::assert_eq; + +#[tokio::test] +async fn cloud_bundle_client_preserves_auto_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::auto()), + ) + .await; + + let client = BackendBundleClient::from_auth_manager( + "https://chatgpt.com/backend-api/".to_string(), + &auth_manager, + ); + + assert_eq!(client.auth_route_config, Some(AuthRouteConfig::auto())); +} diff --git a/codex-rs/cloud-config/src/bundle_loader.rs b/codex-rs/cloud-config/src/bundle_loader.rs index d509ede769..4c94abc196 100644 --- a/codex-rs/cloud-config/src/bundle_loader.rs +++ b/codex-rs/cloud-config/src/bundle_loader.rs @@ -23,9 +23,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/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index bbefa53ac4..c264464762 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1468,6 +1468,47 @@ mode = "system" mode: Some(SystemProxyFeatureModeToml::System), }) ); + + for (requirement_key, expected_mode) in [ + ("system_proxy_mode_auto", SystemProxyFeatureModeToml::Auto), + ("system_proxy_mode_env", SystemProxyFeatureModeToml::Env), + ( + "system_proxy_mode_system", + SystemProxyFeatureModeToml::System, + ), + ( + "system_proxy_mode_direct", + SystemProxyFeatureModeToml::Direct, + ), + ] { + let mode_requirement = Sourced::new( + FeatureRequirementsToml { + entries: BTreeMap::from([(requirement_key.to_string(), true)]), + }, + RequirementSource::Unknown, + ); + assert_eq!( + resolve_bootstrap_system_proxy_config(&ConfigToml::default(), Some(&mode_requirement),)?, + Some(SystemProxyFeatureConfigToml { + enabled: Some(true), + mode: Some(expected_mode), + }) + ); + } + + let direct = Sourced::new( + FeatureRequirementsToml { + entries: BTreeMap::from([("system_proxy_mode_direct".to_string(), true)]), + }, + RequirementSource::Unknown, + ); + assert_eq!( + resolve_bootstrap_system_proxy_config(&configured, Some(&direct))?, + Some(SystemProxyFeatureConfigToml { + enabled: Some(true), + mode: Some(SystemProxyFeatureModeToml::Direct), + }) + ); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 504510b53b..aa35848eab 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2535,10 +2535,14 @@ pub fn resolve_bootstrap_system_proxy_config( FeatureConfigSource::default(), FeatureOverrides::default(), ); + let system_proxy_mode_requirement = + managed_features::system_proxy_mode_requirement(feature_requirements)?; let features = ManagedFeatures::from_configured(configured_features, feature_requirements.cloned())?; Ok(resolved_system_proxy_config( - cfg, &features, /*mode_requirement*/ None, + cfg, + &features, + system_proxy_mode_requirement, )) } diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 9708a5a0a1..7dfe9c5d8d 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -1827,6 +1827,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 e05ccf2026..071b028000 100644 --- a/codex-rs/login/src/outbound_proxy.rs +++ b/codex-rs/login/src/outbound_proxy.rs @@ -30,7 +30,8 @@ impl AuthRouteConfig { Self::from_outbound_proxy_mode(OutboundProxyMode::Direct) } - pub(crate) fn route_config(&self) -> Option<&OutboundProxyConfig> { + /// Returns the shared outbound proxy policy for route-aware clients. + pub fn route_config(&self) -> Option<&OutboundProxyConfig> { self.route_config.as_ref() } @@ -52,17 +53,11 @@ pub fn auth_route_config_from_system_proxy_config( } } -/// Returns the auth route config for the explicit platform system-proxy startup path. +/// Returns the auth route config for the system-proxy startup path. pub fn bootstrap_auth_route_config_from_system_proxy_config( system_proxy: Option<&SystemProxyFeatureConfigToml>, ) -> Option { - let system_proxy = system_proxy?; - if system_proxy.mode != Some(SystemProxyFeatureModeToml::System) { - return None; - } - - // Legacy startup auth builders only need the explicit platform system-proxy selection here. - Some(auth_route_config_from_system_proxy_config(system_proxy)) + system_proxy.map(auth_route_config_from_system_proxy_config) } #[cfg(test)] @@ -79,34 +74,21 @@ mod tests { None ); - let system_config = AuthRouteConfig { - route_config: Some(OutboundProxyConfig::new(OutboundProxyMode::System)), - }; let cases = [ - (None, AuthRouteConfig::auto(), None), - ( - Some(SystemProxyFeatureMode::Auto), - AuthRouteConfig::auto(), - None, - ), - ( - Some(SystemProxyFeatureMode::Env), - AuthRouteConfig::env(), - None, - ), + (None, AuthRouteConfig::auto()), + (Some(SystemProxyFeatureMode::Auto), AuthRouteConfig::auto()), + (Some(SystemProxyFeatureMode::Env), AuthRouteConfig::env()), ( Some(SystemProxyFeatureMode::Direct), AuthRouteConfig::direct(), - None, ), ( Some(SystemProxyFeatureMode::System), - system_config.clone(), - Some(system_config), + AuthRouteConfig::system(), ), ]; - for (mode, expected_config, expected_startup_config) in cases { + for (mode, expected_config) in cases { let system_proxy = SystemProxyFeatureConfigToml { enabled: Some(true), mode, @@ -117,7 +99,7 @@ mod tests { ); assert_eq!( bootstrap_auth_route_config_from_system_proxy_config(Some(&system_proxy)), - expected_startup_config + Some(expected_config) ); } } diff --git a/codex-rs/tui/src/session_archive_commands.rs b/codex-rs/tui/src/session_archive_commands.rs index 7de1e577c7..25563e3163 100644 --- a/codex-rs/tui/src/session_archive_commands.rs +++ b/codex-rs/tui/src/session_archive_commands.rs @@ -11,19 +11,21 @@ use crate::Cli; use crate::app_server_session::AppServerSession; use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::ConfigOverrides; -use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options; +use crate::legacy_core::config::load_config_toml_and_requirements_with_cli_and_load_options; +use crate::legacy_core::config::resolve_bootstrap_system_proxy_config; use crate::legacy_core::config::resolve_oss_provider; use crate::legacy_core::config::resolve_profile_v2_config_path; use codex_app_server_protocol::Thread as AppServerThread; use codex_app_server_protocol::ThreadListParams; use codex_app_server_protocol::ThreadSortKey; use codex_arg0::Arg0DispatchPaths; -use codex_cloud_config::cloud_config_bundle_loader_for_storage; +use codex_cloud_config::cloud_config_bundle_loader_for_storage_with_auth_route_config; use codex_config::CloudConfigBundleLoader; use codex_config::ConfigLoadOptions; use codex_config::LoaderOverrides; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecServerRuntimePaths; +use codex_login::bootstrap_auth_route_config_from_system_proxy_config; use codex_protocol::ThreadId; use codex_utils_cli::CliConfigOverrides; use codex_utils_home_dir::find_codex_home; @@ -310,27 +312,35 @@ async fn start_app_server_for_archive_command( loader_overrides.user_config_profile = Some(profile_v2.clone()); } - let config_toml = load_config_as_toml_with_cli_and_load_options( - codex_home.as_path(), - config_cwd.as_ref(), - cli_kv_overrides.clone(), - ConfigLoadOptions { - loader_overrides: loader_overrides.clone(), - strict_config, - cloud_config_bundle: CloudConfigBundleLoader::default(), - }, - ) - .await - .wrap_err("failed to load config.toml")?; + let (config_toml, bootstrap_requirements) = + load_config_toml_and_requirements_with_cli_and_load_options( + codex_home.as_path(), + config_cwd.as_ref(), + cli_kv_overrides.clone(), + ConfigLoadOptions { + loader_overrides: loader_overrides.clone(), + strict_config, + cloud_config_bundle: CloudConfigBundleLoader::default(), + }, + ) + .await + .wrap_err("failed to load config.toml")?; let chatgpt_base_url = config_toml .chatgpt_base_url .clone() .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()); - let cloud_config_bundle = cloud_config_bundle_loader_for_storage( + let system_proxy_config = resolve_bootstrap_system_proxy_config( + &config_toml, + bootstrap_requirements.feature_requirements.as_ref(), + )?; + let auth_route_config = + bootstrap_auth_route_config_from_system_proxy_config(system_proxy_config.as_ref()); + let cloud_config_bundle = cloud_config_bundle_loader_for_storage_with_auth_route_config( codex_home.to_path_buf(), /*enable_codex_api_key_env*/ false, config_toml.cli_auth_credentials_store.unwrap_or_default(), chatgpt_base_url, + auth_route_config, ) .await;