cloud_requirements PAC support

This commit is contained in:
canvrno-oai
2026-06-22 18:04:29 -07:00
parent 097602b11f
commit 90732871d6
6 changed files with 112 additions and 30 deletions
+51 -18
View File
@@ -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<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(),
@@ -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<String>, auth: &CodexAuth) -> Result<Self> {
@@ -422,10 +438,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)
@@ -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::*;
+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()
.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;
@@ -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())
);
}
+2 -1
View File
@@ -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,
);
+5
View File
@@ -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,
+1 -1
View File
@@ -16,7 +16,7 @@ impl AuthRouteConfig {
}
}
pub(crate) fn route_config(&self) -> &OutboundProxyConfig {
pub fn route_config(&self) -> &OutboundProxyConfig {
&self.route_config
}
}