core: route Responses API through system proxy (#31335)

## Why

`features.respect_system_proxy` already routes authentication traffic
through the OS proxy APIs, but it does not affect the primary inference
path. That leaves users behind OS-managed proxies unable to send normal
Responses API requests even after login succeeds.

This PR is the first product-path migration onto the route-aware
transport introduced in #31323 and refined in #31331. It also
establishes the construction pattern for later migrations: the effective
feature state is resolved once into a required HTTP client factory
rather than represented by an optional per-call setting.

The scope remains limited to the two HTTP Responses endpoints;
WebSockets, model discovery, memories, realtime, and file uploads remain
follow-up migrations.

## What changed

- Replace the optional proxy marker with an explicit
`OutboundProxyPolicy::{ReqwestDefault, RespectSystemProxy}` and a
required `HttpClientFactory`. The policy has no default, and the
lower-level route-aware reqwest builder is now private.
- Have `Config` construct the factory from the effective feature state
and require every `ModelClient` constructor to receive it. There is no
optional setter or implicit `None` fallback.
- Build HTTP clients for `/responses` and `/responses/compact` with
`ClientRouteClass::Api`, using the complete destination URL so PAC rules
can make URL-specific decisions.
- Layer route-aware selection onto Codex's existing default headers,
Cloudflare cookie store, custom CA handling, and sandbox no-proxy
behavior.
- Add an integration test that loads `features.respect_system_proxy`
through `config.toml`, creates a real Codex session, and verifies that
both a normal Responses turn and remote compaction reach an isolated
local proxy.

## Review guide

1. `http-client/src/outbound_proxy.rs` defines the mandatory
policy/factory boundary and keeps route resolution private.
2. `core/src/config/mod.rs`, `core/src/session/session.rs`, and
`core/src/client.rs` show the compile-time invariant: effective config
creates the factory, and `ModelClient` cannot be constructed without
one.
3. `login/src/auth/default_client.rs` preserves existing default-client
behavior while accepting the required factory for migrated routes.
4. `core/src/client.rs` switches only streaming Responses and remote
compaction HTTP transports to the API route class.
5. `core/tests/suite/responses_api_system_proxy.rs` is the behavioral
regression boundary. Its Linux subprocess deliberately sets the CGI
marker that disables reqwest's implicit environment-proxy handling, so
the test fails if session wiring or either Responses call site falls
back to the default client.

## Test plan

- `cargo check --tests -p codex-http-client -p codex-login -p
codex-core`
- `just test -p codex-login`
- `just test -p codex-core
respect_system_proxy_feature_resolves_enabled`
- Existing `compact_uses_bearer_after_agent_identity_session_fallback`
coverage passes with the new transport construction.
- New Linux integration coverage:
`responses_and_compact_use_enabled_system_proxy`
- `just bazel-lock-check`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31335).
* #31342
* __->__ #31335
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-07-07 03:49:53 +00:00
1 parent b9b934e99b
commit 6afcf26d5d
17 files changed
+166 -38

No files matched your search

+41 -15
View File
@@ -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<reqwest::Client, BuildCustomCaTransp
build_reqwest_client_with_custom_ca(default_reqwest_client_builder())
}
/// Builds the default Codex reqwest client for a concrete outbound route.
///
/// When route-aware proxy handling is disabled, or the client is running inside the Codex
/// sandbox, this preserves the default client's existing proxy behavior. Otherwise it resolves
/// the destination through the shared system/PAC-aware routing policy.
pub fn build_default_reqwest_client_for_route(
http_client_factory: &HttpClientFactory,
request_url: &str,
route_class: ClientRouteClass,
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
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<reqwest::Client, BuildRouteAwareHttpClientError> {
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<reqwest::Client, BuildRouteAwareHttpClientError> {
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);
+6 -5
View File
@@ -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
}
}