cloud_requirements PAC support

This commit is contained in:
canvrno-oai
2026-06-12 10:46:05 -07:00
parent 99a4b71934
commit 8cd829a70a
9 changed files with 189 additions and 73 deletions
+51 -18
View File
@@ -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<String>) -> Result<Self> {
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<String>,
auth: &CodexAuth,
outbound_proxy_config: Option<&OutboundProxyConfig>,
) -> Result<Self> {
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<String>, auth: &CodexAuth) -> Result<Self> {
@@ -425,10 +441,7 @@ impl Client {
pub async fn get_config_bundle(
&self,
) -> std::result::Result<ConfigBundleResponse, RequestError> {
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::<ConfigBundleResponse>(&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::*;
+26 -10
View File
@@ -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<AuthRouteConfig>,
}
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<CloudConfigBundle, BundleRequestError> {
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;
@@ -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()));
}
+2 -1
View File
@@ -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,
);
+41
View File
@@ -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(())
}
+5 -1
View File
@@ -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,
))
}
+5
View File
@@ -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,
+10 -28
View File
@@ -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<AuthRouteConfig> {
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)
);
}
}
+25 -15
View File
@@ -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;