From a1bbf86ab7a45f1d6ca2af35d5eb17b63493a8b7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 6 Jul 2026 19:34:06 -0700 Subject: [PATCH] core: route Responses API through system proxy --- codex-rs/Cargo.lock | 1 + codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/client.rs | 32 ++++- codex-rs/core/src/client_tests.rs | 5 + codex-rs/core/src/config/config_tests.rs | 4 + codex-rs/core/src/config/mod.rs | 12 ++ codex-rs/core/src/session/session.rs | 1 + codex-rs/core/src/session/tests.rs | 5 + codex-rs/core/tests/responses_headers.rs | 3 + codex-rs/core/tests/suite/client.rs | 2 + .../core/tests/suite/client_websockets.rs | 1 + codex-rs/core/tests/suite/mod.rs | 1 + .../tests/suite/responses_api_system_proxy.rs | 130 ++++++++++++++++++ codex-rs/http-client/src/lib.rs | 4 +- codex-rs/http-client/src/outbound_proxy.rs | 62 +++++++-- .../http-client/src/outbound_proxy_tests.rs | 3 +- codex-rs/login/src/auth/default_client.rs | 56 ++++++-- codex-rs/login/src/outbound_proxy.rs | 11 +- codex-rs/memories/write/src/runtime.rs | 1 + 19 files changed, 297 insertions(+), 38 deletions(-) create mode 100644 codex-rs/core/tests/suite/responses_api_system_proxy.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9d89f8eb1f..0d4d3c33bf 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2647,6 +2647,7 @@ dependencies = [ "codex-git-utils", "codex-home", "codex-hooks", + "codex-http-client", "codex-image-generation-extension", "codex-install-context", "codex-login", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 4435a2eb63..c617684f78 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -49,6 +49,7 @@ codex-shell-command = { workspace = true } codex-execpolicy = { workspace = true } codex-git-utils = { workspace = true } codex-hooks = { workspace = true } +codex-http-client = { workspace = true } codex-install-context = { workspace = true } codex-network-proxy = { workspace = true } codex-otel = { workspace = true } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 43a758b0e8..d5918d5209 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -62,10 +62,13 @@ use codex_api::auth_header_telemetry; use codex_api::build_session_headers; use codex_api::create_text_param_for_request; use codex_api::response_create_client_metadata; +use codex_http_client::ClientRouteClass; +use codex_http_client::HttpClientFactory; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::RefreshTokenError; use codex_login::UnauthorizedRecovery; +use codex_login::default_client::build_default_reqwest_client_for_route; use codex_login::default_client::build_reqwest_client; use codex_otel::SessionTelemetry; use codex_otel::current_span_w3c_trace_context; @@ -248,6 +251,7 @@ pub struct ModelClient { state: Arc, agent_identity_policy: AgentIdentityAuthPolicy, prompt_cache_key_override: Option, + http_client_factory: HttpClientFactory, } /// A turn-scoped streaming session created from a [`ModelClient`]. @@ -396,7 +400,9 @@ impl ModelClient { /// Creates a new session-scoped `ModelClient`. /// /// All arguments are expected to be stable for the lifetime of a Codex session. Per-turn values - /// are passed to [`ModelClientSession::stream`] (and other turn-scoped methods) explicitly. + /// are passed to [`ModelClientSession::stream`] (and other turn-scoped methods) explicitly. The + /// HTTP client factory must come from the effective session configuration so every transport + /// observes the resolved outbound proxy policy. pub fn new( auth_manager: Option>, agent_identity_policy: AgentIdentityAuthPolicy, @@ -410,6 +416,7 @@ impl ModelClient { beta_features_header: Option, item_ids_enabled: bool, attestation_provider: Option>, + http_client_factory: HttpClientFactory, ) -> Self { let model_provider = create_model_provider(provider_info, auth_manager); let codex_api_key_env_enabled = model_provider @@ -439,6 +446,7 @@ impl ModelClient { }), agent_identity_policy, prompt_cache_key_override: None, + http_client_factory, } } @@ -532,7 +540,8 @@ impl ModelClient { return Ok(Vec::new()); } let client_setup = self.current_client_setup().await?; - let transport = ReqwestTransport::new(build_reqwest_client()); + let transport = + self.build_responses_transport(&client_setup.api_provider, RESPONSES_COMPACT_ENDPOINT)?; let request_telemetry = Self::build_request_telemetry( session_telemetry, AuthRequestTelemetryContext::new( @@ -938,6 +947,21 @@ impl ModelClient { }) } + fn build_responses_transport( + &self, + api_provider: &ApiProvider, + endpoint: &str, + ) -> Result { + let request_url = api_provider.url_for_path(endpoint); + let client = build_default_reqwest_client_for_route( + &self.http_client_factory, + &request_url, + ClientRouteClass::Api, + ) + .map_err(std::io::Error::from)?; + Ok(ReqwestTransport::new(client)) + } + pub(crate) async fn prewarm_auth(&self) -> Result<()> { self.current_client_setup().await.map(|_| ()) } @@ -1372,7 +1396,9 @@ impl ModelClientSession { let mut pending_retry = PendingUnauthorizedRetry::default(); loop { let client_setup = self.client.current_client_setup().await?; - let transport = ReqwestTransport::new(build_reqwest_client()); + let transport = self + .client + .build_responses_transport(&client_setup.api_provider, RESPONSES_ENDPOINT)?; let request_auth_context = AuthRequestTelemetryContext::new( client_setup.auth.as_ref().map(CodexAuth::auth_mode), client_setup.api_auth.as_ref(), diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 82c464625d..b23b239eb0 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -19,6 +19,8 @@ use codex_api::AgentIdentityTelemetry; use codex_api::ApiError; use codex_api::ResponseEvent; use codex_api::TransportError; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; use codex_login::AuthCredentialsStoreMode; use codex_login::AuthKeyringBackendKind; use codex_login::AuthManager; @@ -104,6 +106,7 @@ fn test_model_client_with_thread_id( /*beta_features_header*/ None, /*item_ids_enabled*/ false, /*attestation_provider*/ None, + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), ) } @@ -148,6 +151,7 @@ async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow:: /*beta_features_header*/ None, /*item_ids_enabled*/ false, /*attestation_provider*/ None, + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), ); let prompt = Prompt { input: vec![ResponseItem::Message { @@ -824,6 +828,7 @@ fn model_client_with_counting_attestation( Some(Arc::new(CountingAttestationProvider { calls: attestation_calls.clone(), })), + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), ); (model_client, attestation_calls) } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 6156b9b9ea..40ebc64a0b 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1590,6 +1590,10 @@ respect_system_proxy = true .await?; assert!(config.respect_system_proxy); + assert_eq!( + config.http_client_factory().outbound_proxy_policy(), + codex_http_client::OutboundProxyPolicy::RespectSystemProxy + ); Ok(()) } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 54fbb97f69..8c0a165564 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -70,6 +70,8 @@ use codex_features::MultiAgentV2ConfigToml; use codex_features::NetworkProxyConfigToml; use codex_features::TokenBudgetConfigToml; use codex_git_utils::resolve_root_git_project_for_trust; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; use codex_install_context::InstallContext; use codex_login::AuthManagerConfig; use codex_login::AuthRouteConfig; @@ -1481,6 +1483,16 @@ impl Config { .then(AuthRouteConfig::respect_system_proxy) } + /// Creates the HTTP client factory resolved from the effective feature configuration. + pub fn http_client_factory(&self) -> HttpClientFactory { + let outbound_proxy_policy = if self.respect_system_proxy { + OutboundProxyPolicy::RespectSystemProxy + } else { + OutboundProxyPolicy::ReqwestDefault + }; + HttpClientFactory::new(outbound_proxy_policy) + } + /// Build the plugin-manager input from the effective config. pub fn plugins_config_input(&self) -> PluginsConfigInput { PluginsConfigInput::new( diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 2261eaa495..d11673b973 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1112,6 +1112,7 @@ impl Session { Self::build_model_client_beta_features_header(config.as_ref()), /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), attestation_provider, + config.http_client_factory(), ) .with_prompt_cache_key_override( crate::guardian::prompt_cache_key_override_for_review_session( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 6cc223cd7d..c5cd78672a 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -33,6 +33,8 @@ use codex_core_skills::HostSkillsSnapshot; use core_test_support::test_codex::local_selections; use codex_features::Feature; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; use codex_login::CodexAuth; use codex_login::auth::AgentIdentityAuthPolicy; use codex_model_provider_info::ModelProviderInfo; @@ -491,6 +493,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession { /*beta_features_header*/ None, /*item_ids_enabled*/ false, /*attestation_provider*/ None, + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), ) .new_session() } @@ -5461,6 +5464,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { Session::build_model_client_beta_features_header(config.as_ref()), /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, + config.http_client_factory(), ), code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new( codex_code_mode::InProcessCodeModeSessionProvider, @@ -7587,6 +7591,7 @@ where Session::build_model_client_beta_features_header(config.as_ref()), /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, + config.http_client_factory(), ), code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new( codex_code_mode::InProcessCodeModeSessionProvider, diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 5b7c2c65b8..bf01e27b28 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -132,6 +132,7 @@ async fn responses_stream_includes_subagent_header_on_review() { /*beta_features_header*/ None, /*item_ids_enabled*/ false, /*attestation_provider*/ None, + config.http_client_factory(), ); let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source); let mut client_session = client.new_session(); @@ -266,6 +267,7 @@ async fn responses_stream_includes_subagent_header_on_other() { /*beta_features_header*/ None, /*item_ids_enabled*/ false, /*attestation_provider*/ None, + config.http_client_factory(), ); let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source); let mut client_session = client.new_session(); @@ -386,6 +388,7 @@ async fn responses_respects_model_info_overrides_from_config() { /*beta_features_header*/ None, /*item_ids_enabled*/ false, /*attestation_provider*/ None, + config.http_client_factory(), ); let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source); let mut client_session = client.new_session(); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index c87539f940..9f7e6405aa 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1335,6 +1335,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth /*beta_features_header*/ None, /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, + config.http_client_factory(), ); let responses_metadata = test_turn_responses_metadata(&client, thread_id); let mut client_session = client.new_session(); @@ -2949,6 +2950,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { /*beta_features_header*/ None, /*item_ids_enabled*/ false, /*attestation_provider*/ None, + config.http_client_factory(), ); let responses_metadata = test_turn_responses_metadata(&client, thread_id); let mut client_session = client.new_session(); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index d5218cdcab..737c8e2982 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -2252,6 +2252,7 @@ async fn websocket_harness_with_provider_options( /*beta_features_header*/ None, /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, + config.http_client_factory(), ); WebsocketTestHarness { diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 3e27463370..1b0de3b264 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -99,6 +99,7 @@ mod request_permissions_tool; mod request_plugin_install; mod request_user_input; mod responses_api_proxy_headers; +mod responses_api_system_proxy; mod responses_lite; mod resume; mod resume_warning; diff --git a/codex-rs/core/tests/suite/responses_api_system_proxy.rs b/codex-rs/core/tests/suite/responses_api_system_proxy.rs new file mode 100644 index 0000000000..148f303c9a --- /dev/null +++ b/codex-rs/core/tests/suite/responses_api_system_proxy.rs @@ -0,0 +1,130 @@ +//! Verifies that the effective system-proxy feature reaches both HTTP Responses API paths. +//! +//! The test uses two processes because proxy environment variables are process-global: +//! +//! - The parent owns a mock proxy and launches this test again with an isolated proxy environment. +//! - The child loads the feature through `config.toml` and targets an unreachable origin. +//! +//! The child also sets the CGI marker that makes reqwest ignore its implicit `HTTP_PROXY` +//! handling. Consequently, requests reach the parent only when Codex explicitly selects the +//! configured proxy through `HttpClientFactory`. Falling back to reqwest's default client in +//! either Responses path makes the child fail against the unreachable origin. + +use anyhow::Result; +use codex_config::CONFIG_TOML_FILE; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::mount_compact_json_once; +use core_test_support::responses::mount_sse_once; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::skip_if_no_network; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use std::process::Command; + +const SUBPROCESS_ENV: &str = "CODEX_RESPONSES_SYSTEM_PROXY_TEST_CHILD"; +const SUBPROCESS_SENTINEL: &str = "responses system proxy child ran"; +const TEST_NAME: &str = + "suite::responses_api_system_proxy::responses_and_compact_use_enabled_system_proxy"; +const TARGET_BASE_URL: &str = "http://responses-api.invalid/v1"; + +#[cfg_attr( + not(target_os = "linux"), + ignore = "system proxy environment fallback is Linux-specific" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn responses_and_compact_use_enabled_system_proxy() -> Result<()> { + if std::env::var_os(SUBPROCESS_ENV).is_none() { + return run_test_in_subprocess().await; + } + + // Child process: exercise the real config -> session -> Responses transport path. + eprintln!("{SUBPROCESS_SENTINEL}"); + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let mut builder = test_codex() + .with_pre_build_hook(|codex_home| { + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + "[features]\nrespect_system_proxy = true\n", + ) + .expect("system-proxy feature config should be written"); + }) + .with_config(|config| { + config.model_provider.base_url = Some(TARGET_BASE_URL.to_string()); + }); + let test = builder.build_with_auto_env(&server).await?; + assert!(test.config.respect_system_proxy); + + test.submit_turn("exercise the Responses API proxy route") + .await?; + test.codex.submit(Op::Compact).await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + Ok(()) +} + +async fn run_test_in_subprocess() -> Result<()> { + skip_if_no_network!(Ok(())); + + let proxy = start_mock_server().await; + let responses_mock = mount_sse_once( + &proxy, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + let compact_mock = mount_compact_json_once( + &proxy, + serde_json::json!({ + "output": [ResponseItem::Compaction { + id: None, + encrypted_content: "encrypted compacted history".to_string(), + internal_chat_message_metadata_passthrough: None, + }] + }), + ) + .await; + + // Parent process: own the proxy while the child runs with isolated environment variables. + let output = Command::new(std::env::current_exe()?) + .args(["--exact", TEST_NAME, "--nocapture"]) + .env(SUBPROCESS_ENV, "1") + .env("HTTP_PROXY", proxy.uri()) + // Reqwest treats `REQUEST_METHOD` as a CGI marker and ignores uppercase `HTTP_PROXY`. + // Codex's explicit route resolver still reads it, which distinguishes the two clients. + .env("REQUEST_METHOD", "GET") + .env_remove("http_proxy") + .env_remove("ALL_PROXY") + .env_remove("all_proxy") + .env_remove("NO_PROXY") + .env_remove("no_proxy") + .output()?; + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "system-proxy subprocess failed\nstdout:\n{}\nstderr:\n{stderr}", + String::from_utf8_lossy(&output.stdout), + ); + assert!( + stderr.contains(SUBPROCESS_SENTINEL), + // `--exact` exits successfully even when it selects no tests. Require proof that the + // child branch actually executed so a renamed test cannot produce a false positive. + "system-proxy subprocess did not execute the expected test\nstderr:\n{stderr}" + ); + + assert_eq!(responses_mock.single_request().path(), "/v1/responses"); + assert_eq!( + compact_mock.single_request().path(), + "/v1/responses/compact" + ); + Ok(()) +} diff --git a/codex-rs/http-client/src/lib.rs b/codex-rs/http-client/src/lib.rs index 5f90435452..07e579ecb3 100644 --- a/codex-rs/http-client/src/lib.rs +++ b/codex-rs/http-client/src/lib.rs @@ -25,9 +25,9 @@ pub use crate::error::StreamError; pub use crate::error::TransportError; pub use crate::outbound_proxy::BuildRouteAwareHttpClientError; pub use crate::outbound_proxy::ClientRouteClass; -pub use crate::outbound_proxy::OutboundProxyConfig; +pub use crate::outbound_proxy::HttpClientFactory; +pub use crate::outbound_proxy::OutboundProxyPolicy; pub use crate::outbound_proxy::RouteFailureClass; -pub use crate::outbound_proxy::build_reqwest_client_for_route; pub use crate::request::EncodedJsonBody; pub use crate::request::PreparedRequestBody; pub use crate::request::Request; diff --git a/codex-rs/http-client/src/outbound_proxy.rs b/codex-rs/http-client/src/outbound_proxy.rs index e643698a8c..5506f3d40b 100644 --- a/codex-rs/http-client/src/outbound_proxy.rs +++ b/codex-rs/http-client/src/outbound_proxy.rs @@ -84,14 +84,54 @@ impl fmt::Display for RouteFailureClass { } } -/// Marker enabling fixed system/PAC/WPAD, environment, then direct routing. -/// Resolved endpoints and platform details remain internal to the client builder. +/// Resolved outbound proxy behavior for HTTP clients. +/// +/// Callers must choose a policy explicitly so omitting feature resolution cannot silently select +/// legacy behavior. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct OutboundProxyConfig; +pub enum OutboundProxyPolicy { + /// Preserve reqwest's built-in proxy behavior. + ReqwestDefault, + /// Resolve system/PAC/WPAD settings, then environment settings, then direct routing. + RespectSystemProxy, +} -impl OutboundProxyConfig { - pub const fn respect_system_proxy() -> Self { - Self +/// Builds route-specific HTTP clients using one resolved outbound proxy policy. +/// +/// Construct this once from the effective application configuration and carry it with the +/// session or component that owns outbound requests. Individual request paths should supply only +/// their destination and route class rather than resolving feature state themselves. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HttpClientFactory { + outbound_proxy_policy: OutboundProxyPolicy, +} + +impl HttpClientFactory { + /// Creates a factory from the outbound proxy policy resolved by the application. + pub const fn new(outbound_proxy_policy: OutboundProxyPolicy) -> Self { + Self { + outbound_proxy_policy, + } + } + + /// Returns the outbound proxy policy used for clients built by this factory. + pub const fn outbound_proxy_policy(&self) -> OutboundProxyPolicy { + self.outbound_proxy_policy + } + + /// Builds a reqwest client for a concrete outbound route. + pub fn build_reqwest_client( + &self, + builder: reqwest::ClientBuilder, + request_url: &str, + route_class: ClientRouteClass, + ) -> Result { + build_reqwest_client_for_route( + builder, + request_url, + route_class, + self.outbound_proxy_policy, + ) } } @@ -120,18 +160,18 @@ impl From for io::Error { /// a route is selected are returned without trying another route. Ordered PAC candidates are /// currently collapsed to one route on both Windows and macOS; later proxy or `DIRECT` candidates /// are not retried after a connection failure. -pub fn build_reqwest_client_for_route( +fn build_reqwest_client_for_route( builder: reqwest::ClientBuilder, request_url: &str, route_class: ClientRouteClass, - config: Option<&OutboundProxyConfig>, + outbound_proxy_policy: OutboundProxyPolicy, ) -> Result { let builder = configure_proxy_for_route( &ProcessEnv, builder, request_url, route_class, - config, + outbound_proxy_policy, resolve_system_proxy, )?; build_reqwest_client_with_custom_ca(builder).map_err(Into::into) @@ -142,10 +182,10 @@ fn configure_proxy_for_route( builder: reqwest::ClientBuilder, request_url: &str, route_class: ClientRouteClass, - config: Option<&OutboundProxyConfig>, + outbound_proxy_policy: OutboundProxyPolicy, resolve_system_proxy: impl FnOnce(&str, &RequestOrigin) -> SystemProxyDecision, ) -> Result { - if config.is_none() { + if matches!(outbound_proxy_policy, OutboundProxyPolicy::ReqwestDefault) { return Ok(builder); } let origin = RequestOrigin::parse(request_url); diff --git a/codex-rs/http-client/src/outbound_proxy_tests.rs b/codex-rs/http-client/src/outbound_proxy_tests.rs index 60a1d56396..734d5483ec 100644 --- a/codex-rs/http-client/src/outbound_proxy_tests.rs +++ b/codex-rs/http-client/src/outbound_proxy_tests.rs @@ -79,13 +79,12 @@ async fn enabled_environment_proxy_routes_request_through_proxy() { values: HashMap::from([("HTTP_PROXY".to_string(), format!("http://{proxy_addr}"))]), }; let request_url = "http://enabled-proxy.test/proxy-check"; - let config = OutboundProxyConfig::respect_system_proxy(); let builder = configure_proxy_for_route( &env, reqwest::Client::builder().timeout(Duration::from_secs(2)), request_url, ClientRouteClass::Auth, - Some(&config), + OutboundProxyPolicy::RespectSystemProxy, |_, _| SystemProxyDecision::Unavailable { failure: RouteFailureClass::ProxyResolutionUnavailable, }, diff --git a/codex-rs/login/src/auth/default_client.rs b/codex-rs/login/src/auth/default_client.rs index 1146a89d96..089e78340b 100644 --- a/codex-rs/login/src/auth/default_client.rs +++ b/codex-rs/login/src/auth/default_client.rs @@ -8,8 +8,9 @@ use codex_http_client::BuildCustomCaTransportError; use codex_http_client::BuildRouteAwareHttpClientError; use codex_http_client::ClientRouteClass; use codex_http_client::HttpClient; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; pub use codex_http_client::RequestBuilder as CodexRequestBuilder; -use codex_http_client::build_reqwest_client_for_route; use codex_http_client::build_reqwest_client_with_custom_ca; use codex_http_client::with_chatgpt_cloudflare_cookie_store; use codex_terminal_detection::user_agent; @@ -235,6 +236,35 @@ pub fn try_build_reqwest_client() -> Result Result { + if matches!( + http_client_factory.outbound_proxy_policy(), + OutboundProxyPolicy::ReqwestDefault + ) { + return Ok(build_reqwest_client()); + } + if is_sandboxed() { + // Preserve the sandbox's existing no-proxy policy; sandboxed command egress is routed + // separately through network-proxy. + return Ok(build_reqwest_client()); + } + + http_client_factory.build_reqwest_client( + default_reqwest_client_builder(), + request_url, + route_class, + ) +} + fn default_reqwest_client_builder() -> reqwest::ClientBuilder { let mut builder = reqwest::Client::builder().default_headers(default_headers()); if is_sandboxed() { @@ -248,11 +278,10 @@ pub(crate) fn build_raw_auth_reqwest_client( endpoint: &str, auth_route_config: Option<&AuthRouteConfig>, ) -> Result { - build_reqwest_client_for_route( + auth_http_client_factory(auth_route_config).build_reqwest_client( reqwest::Client::builder(), endpoint, ClientRouteClass::Auth, - auth_route_config.map(AuthRouteConfig::route_config), ) } @@ -261,20 +290,10 @@ pub(crate) fn build_default_auth_reqwest_client( endpoint: &str, auth_route_config: Option<&AuthRouteConfig>, ) -> Result { - let Some(route_config) = auth_route_config.map(AuthRouteConfig::route_config) else { - return Ok(build_reqwest_client()); - }; - - if is_sandboxed() { - // Preserve the sandbox's existing no-proxy policy; sandboxed command egress is routed - // separately through network-proxy. - return Ok(build_reqwest_client()); - } - build_reqwest_client_for_route( - default_reqwest_client_builder(), + build_default_reqwest_client_for_route( + &auth_http_client_factory(auth_route_config), endpoint, ClientRouteClass::Auth, - Some(route_config), ) } @@ -286,6 +305,13 @@ pub(crate) fn create_default_auth_client( build_default_auth_reqwest_client(endpoint, auth_route_config).map(HttpClient::new) } +fn auth_http_client_factory(auth_route_config: Option<&AuthRouteConfig>) -> HttpClientFactory { + auth_route_config.map_or_else( + || HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + |config| config.http_client_factory().clone(), + ) +} + pub fn default_headers() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert("originator", originator().header_value); diff --git a/codex-rs/login/src/outbound_proxy.rs b/codex-rs/login/src/outbound_proxy.rs index 66441e3637..79fcf76de1 100644 --- a/codex-rs/login/src/outbound_proxy.rs +++ b/codex-rs/login/src/outbound_proxy.rs @@ -1,4 +1,5 @@ -use codex_http_client::OutboundProxyConfig; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; /// Auth-layer adapter around client-owned proxy policy. /// @@ -6,17 +7,17 @@ use codex_http_client::OutboundProxyConfig; /// client layer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthRouteConfig { - route_config: OutboundProxyConfig, + http_client_factory: HttpClientFactory, } impl AuthRouteConfig { pub fn respect_system_proxy() -> Self { Self { - route_config: OutboundProxyConfig::respect_system_proxy(), + http_client_factory: HttpClientFactory::new(OutboundProxyPolicy::RespectSystemProxy), } } - pub(crate) fn route_config(&self) -> &OutboundProxyConfig { - &self.route_config + pub(crate) fn http_client_factory(&self) -> &HttpClientFactory { + &self.http_client_factory } } diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 6c7ad3978f..c2dc60243e 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -262,6 +262,7 @@ impl MemoryStartupContext { /*beta_features_header*/ None, config.features.enabled(Feature::ItemIds), /*attestation_provider*/ None, + config.http_client_factory(), ); let mut client_session = model_client.new_session();