Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
32 changes: 29 additions & 3 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -248,6 +251,7 @@ pub struct ModelClient {
state: Arc<ModelClientState>,
agent_identity_policy: AgentIdentityAuthPolicy,
prompt_cache_key_override: Option<String>,
http_client_factory: HttpClientFactory,
}

/// A turn-scoped streaming session created from a [`ModelClient`].
Expand Down Expand Up @@ -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<Arc<AuthManager>>,
agent_identity_policy: AgentIdentityAuthPolicy,
Expand All @@ -410,6 +416,7 @@ impl ModelClient {
beta_features_header: Option<String>,
item_ids_enabled: bool,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
http_client_factory: HttpClientFactory,
) -> Self {
let model_provider = create_model_provider(provider_info, auth_manager);
let codex_api_key_env_enabled = model_provider
Expand Down Expand Up @@ -439,6 +446,7 @@ impl ModelClient {
}),
agent_identity_policy,
prompt_cache_key_override: None,
http_client_factory,
}
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -938,6 +947,21 @@ impl ModelClient {
})
}

fn build_responses_transport(
&self,
api_provider: &ApiProvider,
endpoint: &str,
) -> Result<ReqwestTransport> {
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(|_| ())
}
Expand Down Expand Up @@ -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(),
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
)
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down
12 changes: 12 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/tests/responses_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/core/tests/suite/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/tests/suite/client_websockets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/http-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 51 additions & 11 deletions codex-rs/http-client/src/outbound_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

/// 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
}

impl OutboundProxyConfig {
pub const fn respect_system_proxy() -> Self {
Self
/// 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<reqwest::Client, BuildRouteAwareHttpClientError> {
build_reqwest_client_for_route(
builder,
request_url,
route_class,
self.outbound_proxy_policy,
)
}
}

Expand Down Expand Up @@ -120,18 +160,18 @@ impl From<BuildRouteAwareHttpClientError> 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<reqwest::Client, BuildRouteAwareHttpClientError> {
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)
Expand All @@ -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<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
if config.is_none() {
if matches!(outbound_proxy_policy, OutboundProxyPolicy::ReqwestDefault) {
return Ok(builder);
}
let origin = RequestOrigin::parse(request_url);
Expand Down
3 changes: 1 addition & 2 deletions codex-rs/http-client/src/outbound_proxy_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Loading
Loading