mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
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:
1 parent
b9b934e99b
commit
6afcf26d5d
17 files changed
+166
-38
No files matched your search
Generated
+1
@@ -2647,6 +2647,7 @@ dependencies = [
|
|||||||
"codex-git-utils",
|
"codex-git-utils",
|
||||||
"codex-home",
|
"codex-home",
|
||||||
"codex-hooks",
|
"codex-hooks",
|
||||||
|
"codex-http-client",
|
||||||
"codex-image-generation-extension",
|
"codex-image-generation-extension",
|
||||||
"codex-install-context",
|
"codex-install-context",
|
||||||
"codex-login",
|
"codex-login",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ codex-shell-command = { workspace = true }
|
|||||||
codex-execpolicy = { workspace = true }
|
codex-execpolicy = { workspace = true }
|
||||||
codex-git-utils = { workspace = true }
|
codex-git-utils = { workspace = true }
|
||||||
codex-hooks = { workspace = true }
|
codex-hooks = { workspace = true }
|
||||||
|
codex-http-client = { workspace = true }
|
||||||
codex-install-context = { workspace = true }
|
codex-install-context = { workspace = true }
|
||||||
codex-network-proxy = { workspace = true }
|
codex-network-proxy = { workspace = true }
|
||||||
codex-otel = { workspace = true }
|
codex-otel = { workspace = true }
|
||||||
|
|||||||
@@ -63,10 +63,13 @@ use codex_api::auth_header_telemetry;
|
|||||||
use codex_api::build_session_headers;
|
use codex_api::build_session_headers;
|
||||||
use codex_api::create_text_param_for_request;
|
use codex_api::create_text_param_for_request;
|
||||||
use codex_api::response_create_client_metadata;
|
use codex_api::response_create_client_metadata;
|
||||||
|
use codex_http_client::ClientRouteClass;
|
||||||
|
use codex_http_client::HttpClientFactory;
|
||||||
use codex_login::AuthManager;
|
use codex_login::AuthManager;
|
||||||
use codex_login::CodexAuth;
|
use codex_login::CodexAuth;
|
||||||
use codex_login::RefreshTokenError;
|
use codex_login::RefreshTokenError;
|
||||||
use codex_login::UnauthorizedRecovery;
|
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_login::default_client::build_reqwest_client;
|
||||||
use codex_otel::SessionTelemetry;
|
use codex_otel::SessionTelemetry;
|
||||||
use codex_otel::current_span_w3c_trace_context;
|
use codex_otel::current_span_w3c_trace_context;
|
||||||
@@ -250,6 +253,7 @@ pub struct ModelClient {
|
|||||||
state: Arc<ModelClientState>,
|
state: Arc<ModelClientState>,
|
||||||
agent_identity_policy: AgentIdentityAuthPolicy,
|
agent_identity_policy: AgentIdentityAuthPolicy,
|
||||||
prompt_cache_key_override: Option<String>,
|
prompt_cache_key_override: Option<String>,
|
||||||
|
http_client_factory: HttpClientFactory,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A turn-scoped streaming session created from a [`ModelClient`].
|
/// A turn-scoped streaming session created from a [`ModelClient`].
|
||||||
@@ -402,7 +406,9 @@ impl ModelClient {
|
|||||||
/// Creates a new session-scoped `ModelClient`.
|
/// Creates a new session-scoped `ModelClient`.
|
||||||
///
|
///
|
||||||
/// All arguments are expected to be stable for the lifetime of a Codex session. Per-turn values
|
/// 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(
|
pub fn new(
|
||||||
auth_manager: Option<Arc<AuthManager>>,
|
auth_manager: Option<Arc<AuthManager>>,
|
||||||
agent_identity_policy: AgentIdentityAuthPolicy,
|
agent_identity_policy: AgentIdentityAuthPolicy,
|
||||||
@@ -417,6 +423,7 @@ impl ModelClient {
|
|||||||
item_ids_enabled: bool,
|
item_ids_enabled: bool,
|
||||||
concurrent_reasoning_summaries_enabled: bool,
|
concurrent_reasoning_summaries_enabled: bool,
|
||||||
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||||
|
http_client_factory: HttpClientFactory,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let model_provider = create_model_provider(provider_info, auth_manager);
|
let model_provider = create_model_provider(provider_info, auth_manager);
|
||||||
let codex_api_key_env_enabled = model_provider
|
let codex_api_key_env_enabled = model_provider
|
||||||
@@ -447,6 +454,7 @@ impl ModelClient {
|
|||||||
}),
|
}),
|
||||||
agent_identity_policy,
|
agent_identity_policy,
|
||||||
prompt_cache_key_override: None,
|
prompt_cache_key_override: None,
|
||||||
|
http_client_factory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +548,8 @@ impl ModelClient {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let client_setup = self.current_client_setup().await?;
|
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(
|
let request_telemetry = Self::build_request_telemetry(
|
||||||
session_telemetry,
|
session_telemetry,
|
||||||
AuthRequestTelemetryContext::new(
|
AuthRequestTelemetryContext::new(
|
||||||
@@ -952,6 +961,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<()> {
|
pub(crate) async fn prewarm_auth(&self) -> Result<()> {
|
||||||
self.current_client_setup().await.map(|_| ())
|
self.current_client_setup().await.map(|_| ())
|
||||||
}
|
}
|
||||||
@@ -1386,7 +1410,9 @@ impl ModelClientSession {
|
|||||||
let mut pending_retry = PendingUnauthorizedRetry::default();
|
let mut pending_retry = PendingUnauthorizedRetry::default();
|
||||||
loop {
|
loop {
|
||||||
let client_setup = self.client.current_client_setup().await?;
|
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(
|
let request_auth_context = AuthRequestTelemetryContext::new(
|
||||||
client_setup.auth.as_ref().map(CodexAuth::auth_mode),
|
client_setup.auth.as_ref().map(CodexAuth::auth_mode),
|
||||||
client_setup.api_auth.as_ref(),
|
client_setup.api_auth.as_ref(),
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ use codex_api::AgentIdentityTelemetry;
|
|||||||
use codex_api::ApiError;
|
use codex_api::ApiError;
|
||||||
use codex_api::ResponseEvent;
|
use codex_api::ResponseEvent;
|
||||||
use codex_api::TransportError;
|
use codex_api::TransportError;
|
||||||
|
use codex_http_client::HttpClientFactory;
|
||||||
|
use codex_http_client::OutboundProxyPolicy;
|
||||||
use codex_login::AuthCredentialsStoreMode;
|
use codex_login::AuthCredentialsStoreMode;
|
||||||
use codex_login::AuthKeyringBackendKind;
|
use codex_login::AuthKeyringBackendKind;
|
||||||
use codex_login::AuthManager;
|
use codex_login::AuthManager;
|
||||||
@@ -105,6 +107,7 @@ fn test_model_client_with_thread_id(
|
|||||||
/*item_ids_enabled*/ false,
|
/*item_ids_enabled*/ false,
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +153,7 @@ async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow::
|
|||||||
/*item_ids_enabled*/ false,
|
/*item_ids_enabled*/ false,
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||||
);
|
);
|
||||||
let prompt = Prompt {
|
let prompt = Prompt {
|
||||||
input: vec![ResponseItem::Message {
|
input: vec![ResponseItem::Message {
|
||||||
@@ -827,6 +831,7 @@ fn model_client_with_counting_attestation(
|
|||||||
Some(Arc::new(CountingAttestationProvider {
|
Some(Arc::new(CountingAttestationProvider {
|
||||||
calls: attestation_calls.clone(),
|
calls: attestation_calls.clone(),
|
||||||
})),
|
})),
|
||||||
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||||
);
|
);
|
||||||
(model_client, attestation_calls)
|
(model_client, attestation_calls)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1590,6 +1590,10 @@ respect_system_proxy = true
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert!(config.respect_system_proxy);
|
assert!(config.respect_system_proxy);
|
||||||
|
assert_eq!(
|
||||||
|
config.http_client_factory().outbound_proxy_policy(),
|
||||||
|
codex_http_client::OutboundProxyPolicy::RespectSystemProxy
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ use codex_features::MultiAgentV2ConfigToml;
|
|||||||
use codex_features::NetworkProxyConfigToml;
|
use codex_features::NetworkProxyConfigToml;
|
||||||
use codex_features::TokenBudgetConfigToml;
|
use codex_features::TokenBudgetConfigToml;
|
||||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
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_install_context::InstallContext;
|
||||||
use codex_login::AuthManagerConfig;
|
use codex_login::AuthManagerConfig;
|
||||||
use codex_login::AuthRouteConfig;
|
use codex_login::AuthRouteConfig;
|
||||||
@@ -1481,6 +1483,16 @@ impl Config {
|
|||||||
.then(AuthRouteConfig::respect_system_proxy)
|
.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.
|
/// Build the plugin-manager input from the effective config.
|
||||||
pub fn plugins_config_input(&self) -> PluginsConfigInput {
|
pub fn plugins_config_input(&self) -> PluginsConfigInput {
|
||||||
PluginsConfigInput::new(
|
PluginsConfigInput::new(
|
||||||
|
|||||||
@@ -1115,6 +1115,7 @@ impl Session {
|
|||||||
.features
|
.features
|
||||||
.enabled(Feature::ConcurrentReasoningSummaries),
|
.enabled(Feature::ConcurrentReasoningSummaries),
|
||||||
attestation_provider,
|
attestation_provider,
|
||||||
|
config.http_client_factory(),
|
||||||
)
|
)
|
||||||
.with_prompt_cache_key_override(
|
.with_prompt_cache_key_override(
|
||||||
crate::guardian::prompt_cache_key_override_for_review_session(
|
crate::guardian::prompt_cache_key_override_for_review_session(
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ use codex_core_skills::HostSkillsSnapshot;
|
|||||||
use core_test_support::test_codex::local_selections;
|
use core_test_support::test_codex::local_selections;
|
||||||
|
|
||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
|
use codex_http_client::HttpClientFactory;
|
||||||
|
use codex_http_client::OutboundProxyPolicy;
|
||||||
use codex_login::CodexAuth;
|
use codex_login::CodexAuth;
|
||||||
use codex_login::auth::AgentIdentityAuthPolicy;
|
use codex_login::auth::AgentIdentityAuthPolicy;
|
||||||
use codex_model_provider_info::ModelProviderInfo;
|
use codex_model_provider_info::ModelProviderInfo;
|
||||||
@@ -492,6 +494,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
|
|||||||
/*item_ids_enabled*/ false,
|
/*item_ids_enabled*/ false,
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||||
)
|
)
|
||||||
.new_session()
|
.new_session()
|
||||||
}
|
}
|
||||||
@@ -5466,6 +5469,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
|||||||
.features
|
.features
|
||||||
.enabled(Feature::ConcurrentReasoningSummaries),
|
.enabled(Feature::ConcurrentReasoningSummaries),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
),
|
),
|
||||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new(
|
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new(
|
||||||
codex_code_mode::InProcessCodeModeSessionProvider,
|
codex_code_mode::InProcessCodeModeSessionProvider,
|
||||||
@@ -7596,6 +7600,7 @@ where
|
|||||||
.features
|
.features
|
||||||
.enabled(Feature::ConcurrentReasoningSummaries),
|
.enabled(Feature::ConcurrentReasoningSummaries),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
),
|
),
|
||||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new(
|
code_mode_service: crate::tools::code_mode::CodeModeService::new(Arc::new(
|
||||||
codex_code_mode::InProcessCodeModeSessionProvider,
|
codex_code_mode::InProcessCodeModeSessionProvider,
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
|||||||
/*item_ids_enabled*/ false,
|
/*item_ids_enabled*/ false,
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
);
|
);
|
||||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
||||||
let mut client_session = client.new_session();
|
let mut client_session = client.new_session();
|
||||||
@@ -268,6 +269,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
|
|||||||
/*item_ids_enabled*/ false,
|
/*item_ids_enabled*/ false,
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
);
|
);
|
||||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
||||||
let mut client_session = client.new_session();
|
let mut client_session = client.new_session();
|
||||||
@@ -389,6 +391,7 @@ async fn responses_respects_model_info_overrides_from_config() {
|
|||||||
/*item_ids_enabled*/ false,
|
/*item_ids_enabled*/ false,
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
);
|
);
|
||||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
|
||||||
let mut client_session = client.new_session();
|
let mut client_session = client.new_session();
|
||||||
|
|||||||
@@ -1339,6 +1339,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
|
|||||||
.features
|
.features
|
||||||
.enabled(Feature::ConcurrentReasoningSummaries),
|
.enabled(Feature::ConcurrentReasoningSummaries),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
);
|
);
|
||||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id);
|
let responses_metadata = test_turn_responses_metadata(&client, thread_id);
|
||||||
let mut client_session = client.new_session();
|
let mut client_session = client.new_session();
|
||||||
@@ -3007,6 +3008,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
|||||||
/*item_ids_enabled*/ false,
|
/*item_ids_enabled*/ false,
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
);
|
);
|
||||||
let responses_metadata = test_turn_responses_metadata(&client, thread_id);
|
let responses_metadata = test_turn_responses_metadata(&client, thread_id);
|
||||||
let mut client_session = client.new_session();
|
let mut client_session = client.new_session();
|
||||||
|
|||||||
@@ -2284,6 +2284,7 @@ async fn websocket_harness_with_provider_options(
|
|||||||
.features
|
.features
|
||||||
.enabled(Feature::ConcurrentReasoningSummaries),
|
.enabled(Feature::ConcurrentReasoningSummaries),
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
);
|
);
|
||||||
|
|
||||||
WebsocketTestHarness {
|
WebsocketTestHarness {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ pub use crate::error::StreamError;
|
|||||||
pub use crate::error::TransportError;
|
pub use crate::error::TransportError;
|
||||||
pub use crate::outbound_proxy::BuildRouteAwareHttpClientError;
|
pub use crate::outbound_proxy::BuildRouteAwareHttpClientError;
|
||||||
pub use crate::outbound_proxy::ClientRouteClass;
|
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::RouteFailureClass;
|
||||||
pub use crate::outbound_proxy::build_reqwest_client_for_route;
|
|
||||||
pub use crate::request::EncodedJsonBody;
|
pub use crate::request::EncodedJsonBody;
|
||||||
pub use crate::request::PreparedRequestBody;
|
pub use crate::request::PreparedRequestBody;
|
||||||
pub use crate::request::Request;
|
pub use crate::request::Request;
|
||||||
|
|||||||
@@ -84,14 +84,54 @@ impl fmt::Display for RouteFailureClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker enabling fixed system/PAC/WPAD, environment, then direct routing.
|
/// Resolved outbound proxy behavior for HTTP clients.
|
||||||
/// Resolved endpoints and platform details remain internal to the client builder.
|
///
|
||||||
|
/// Callers must choose a policy explicitly so omitting feature resolution cannot silently select
|
||||||
|
/// legacy behavior.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[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 {
|
/// Builds route-specific HTTP clients using one resolved outbound proxy policy.
|
||||||
pub const fn respect_system_proxy() -> Self {
|
///
|
||||||
Self
|
/// 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<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||||
|
build_reqwest_client_for_route(
|
||||||
|
builder,
|
||||||
|
request_url,
|
||||||
|
route_class,
|
||||||
|
self.outbound_proxy_policy,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,18 +160,18 @@ impl From<BuildRouteAwareHttpClientError> for io::Error {
|
|||||||
/// a route is selected are returned without trying another route. Ordered PAC candidates are
|
/// 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
|
/// currently collapsed to one route on both Windows and macOS; later proxy or `DIRECT` candidates
|
||||||
/// are not retried after a connection failure.
|
/// are not retried after a connection failure.
|
||||||
pub fn build_reqwest_client_for_route(
|
fn build_reqwest_client_for_route(
|
||||||
builder: reqwest::ClientBuilder,
|
builder: reqwest::ClientBuilder,
|
||||||
request_url: &str,
|
request_url: &str,
|
||||||
route_class: ClientRouteClass,
|
route_class: ClientRouteClass,
|
||||||
config: Option<&OutboundProxyConfig>,
|
outbound_proxy_policy: OutboundProxyPolicy,
|
||||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||||
let builder = configure_proxy_for_route(
|
let builder = configure_proxy_for_route(
|
||||||
&ProcessEnv,
|
&ProcessEnv,
|
||||||
builder,
|
builder,
|
||||||
request_url,
|
request_url,
|
||||||
route_class,
|
route_class,
|
||||||
config,
|
outbound_proxy_policy,
|
||||||
resolve_system_proxy,
|
resolve_system_proxy,
|
||||||
)?;
|
)?;
|
||||||
build_reqwest_client_with_custom_ca(builder).map_err(Into::into)
|
build_reqwest_client_with_custom_ca(builder).map_err(Into::into)
|
||||||
@@ -142,10 +182,10 @@ fn configure_proxy_for_route(
|
|||||||
builder: reqwest::ClientBuilder,
|
builder: reqwest::ClientBuilder,
|
||||||
request_url: &str,
|
request_url: &str,
|
||||||
route_class: ClientRouteClass,
|
route_class: ClientRouteClass,
|
||||||
config: Option<&OutboundProxyConfig>,
|
outbound_proxy_policy: OutboundProxyPolicy,
|
||||||
resolve_system_proxy: impl FnOnce(&str, &RequestOrigin) -> SystemProxyDecision,
|
resolve_system_proxy: impl FnOnce(&str, &RequestOrigin) -> SystemProxyDecision,
|
||||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||||
if config.is_none() {
|
if matches!(outbound_proxy_policy, OutboundProxyPolicy::ReqwestDefault) {
|
||||||
return Ok(builder);
|
return Ok(builder);
|
||||||
}
|
}
|
||||||
let origin = RequestOrigin::parse(request_url);
|
let origin = RequestOrigin::parse(request_url);
|
||||||
|
|||||||
@@ -79,13 +79,12 @@ async fn enabled_environment_proxy_routes_request_through_proxy() {
|
|||||||
values: HashMap::from([("HTTP_PROXY".to_string(), format!("http://{proxy_addr}"))]),
|
values: HashMap::from([("HTTP_PROXY".to_string(), format!("http://{proxy_addr}"))]),
|
||||||
};
|
};
|
||||||
let request_url = "http://enabled-proxy.test/proxy-check";
|
let request_url = "http://enabled-proxy.test/proxy-check";
|
||||||
let config = OutboundProxyConfig::respect_system_proxy();
|
|
||||||
let builder = configure_proxy_for_route(
|
let builder = configure_proxy_for_route(
|
||||||
&env,
|
&env,
|
||||||
reqwest::Client::builder().timeout(Duration::from_secs(2)),
|
reqwest::Client::builder().timeout(Duration::from_secs(2)),
|
||||||
request_url,
|
request_url,
|
||||||
ClientRouteClass::Auth,
|
ClientRouteClass::Auth,
|
||||||
Some(&config),
|
OutboundProxyPolicy::RespectSystemProxy,
|
||||||
|_, _| SystemProxyDecision::Unavailable {
|
|_, _| SystemProxyDecision::Unavailable {
|
||||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use codex_http_client::BuildCustomCaTransportError;
|
|||||||
use codex_http_client::BuildRouteAwareHttpClientError;
|
use codex_http_client::BuildRouteAwareHttpClientError;
|
||||||
use codex_http_client::ClientRouteClass;
|
use codex_http_client::ClientRouteClass;
|
||||||
use codex_http_client::HttpClient;
|
use codex_http_client::HttpClient;
|
||||||
|
use codex_http_client::HttpClientFactory;
|
||||||
|
use codex_http_client::OutboundProxyPolicy;
|
||||||
pub use codex_http_client::RequestBuilder as CodexRequestBuilder;
|
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::build_reqwest_client_with_custom_ca;
|
||||||
use codex_http_client::with_chatgpt_cloudflare_cookie_store;
|
use codex_http_client::with_chatgpt_cloudflare_cookie_store;
|
||||||
use codex_terminal_detection::user_agent;
|
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())
|
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 {
|
fn default_reqwest_client_builder() -> reqwest::ClientBuilder {
|
||||||
let mut builder = reqwest::Client::builder().default_headers(default_headers());
|
let mut builder = reqwest::Client::builder().default_headers(default_headers());
|
||||||
if is_sandboxed() {
|
if is_sandboxed() {
|
||||||
@@ -248,11 +278,10 @@ pub(crate) fn build_raw_auth_reqwest_client(
|
|||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
auth_route_config: Option<&AuthRouteConfig>,
|
auth_route_config: Option<&AuthRouteConfig>,
|
||||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||||
build_reqwest_client_for_route(
|
auth_http_client_factory(auth_route_config).build_reqwest_client(
|
||||||
reqwest::Client::builder(),
|
reqwest::Client::builder(),
|
||||||
endpoint,
|
endpoint,
|
||||||
ClientRouteClass::Auth,
|
ClientRouteClass::Auth,
|
||||||
auth_route_config.map(AuthRouteConfig::route_config),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,20 +290,10 @@ pub(crate) fn build_default_auth_reqwest_client(
|
|||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
auth_route_config: Option<&AuthRouteConfig>,
|
auth_route_config: Option<&AuthRouteConfig>,
|
||||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||||
let Some(route_config) = auth_route_config.map(AuthRouteConfig::route_config) else {
|
build_default_reqwest_client_for_route(
|
||||||
return Ok(build_reqwest_client());
|
&auth_http_client_factory(auth_route_config),
|
||||||
};
|
|
||||||
|
|
||||||
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(),
|
|
||||||
endpoint,
|
endpoint,
|
||||||
ClientRouteClass::Auth,
|
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)
|
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 {
|
pub fn default_headers() -> HeaderMap {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert("originator", originator().header_value);
|
headers.insert("originator", originator().header_value);
|
||||||
|
|||||||
@@ -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.
|
/// Auth-layer adapter around client-owned proxy policy.
|
||||||
///
|
///
|
||||||
@@ -6,17 +7,17 @@ use codex_http_client::OutboundProxyConfig;
|
|||||||
/// client layer.
|
/// client layer.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct AuthRouteConfig {
|
pub struct AuthRouteConfig {
|
||||||
route_config: OutboundProxyConfig,
|
http_client_factory: HttpClientFactory,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthRouteConfig {
|
impl AuthRouteConfig {
|
||||||
pub fn respect_system_proxy() -> Self {
|
pub fn respect_system_proxy() -> Self {
|
||||||
Self {
|
Self {
|
||||||
route_config: OutboundProxyConfig::respect_system_proxy(),
|
http_client_factory: HttpClientFactory::new(OutboundProxyPolicy::RespectSystemProxy),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn route_config(&self) -> &OutboundProxyConfig {
|
pub(crate) fn http_client_factory(&self) -> &HttpClientFactory {
|
||||||
&self.route_config
|
&self.http_client_factory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,6 +263,7 @@ impl MemoryStartupContext {
|
|||||||
config.features.enabled(Feature::ItemIds),
|
config.features.enabled(Feature::ItemIds),
|
||||||
/*concurrent_reasoning_summaries_enabled*/ false,
|
/*concurrent_reasoning_summaries_enabled*/ false,
|
||||||
/*attestation_provider*/ None,
|
/*attestation_provider*/ None,
|
||||||
|
config.http_client_factory(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut client_session = model_client.new_session();
|
let mut client_session = model_client.new_session();
|
||||||
|
|||||||
Reference in new issue
Block a user