mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
cloud_requirements PAC support
This commit is contained in:
@@ -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::*;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user