Route exec-server HTTP through configured proxy policy (#35023)

## Why

Delegated HTTP requests need to honor the same outbound proxy policy as the
Codex process that starts the exec server.

## What changed

- Pass the configured `HttpClientFactory` through local and remote exec-server
  startup and use route-aware client pools for delegated HTTP and local MCP
  requests.
- Preserve per-request timeouts and follow-or-stop redirect behavior while
  keeping request URLs and sensitive response headers out of diagnostics.

## Testing

- Cover configured system-proxy routing across the exec-server transport.
- Cover both redirect policies and verify that success and failure logs do not
  expose request or response secrets.

GitOrigin-RevId: 4af6aec1d265c4db62dfcb6e1fb076fb31736137
This commit is contained in:
Celia Chen
2026-07-23 22:39:28 +00:00
committed by copyberry
parent fe0d472c4c
commit 1ee8f49175
25 changed files with 569 additions and 85 deletions
+8 -1
View File
@@ -5,6 +5,7 @@ use std::sync::atomic::Ordering;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_exec_server_protocol::RequestId;
use codex_http_client::HttpClientFactory;
use serde_json::to_value;
use std::collections::HashSet;
use tokio::sync::Mutex;
@@ -13,6 +14,7 @@ use tokio_util::task::TaskTracker;
use crate::ExecServerRuntimePaths;
use crate::client::http_client::PendingReqwestHttpBodyStream;
use crate::client::http_client::ReqwestHttpClient;
use crate::client::http_client::ReqwestHttpRequestRunner;
use crate::protocol::CapabilityRootsDiscoverParams;
use crate::protocol::CapabilityRootsDiscoverResponse;
@@ -73,6 +75,7 @@ pub(crate) struct ExecServerHandler {
background_tasks: TaskTracker,
file_system: FileSystemHandler,
runtime_paths: ExecServerRuntimePaths,
http_client: ReqwestHttpClient,
initialize_requested: AtomicBool,
initialized: AtomicBool,
}
@@ -82,6 +85,7 @@ impl ExecServerHandler {
session_registry: Arc<SessionRegistry>,
notifications: RpcNotificationSender,
runtime_paths: ExecServerRuntimePaths,
http_client_factory: HttpClientFactory,
) -> Self {
Self {
session_registry,
@@ -92,6 +96,7 @@ impl ExecServerHandler {
background_tasks: TaskTracker::new(),
file_system: FileSystemHandler::new(runtime_paths.clone()),
runtime_paths,
http_client: ReqwestHttpClient::new(http_client_factory),
initialize_requested: AtomicBool::new(false),
initialized: AtomicBool::new(false),
}
@@ -222,7 +227,9 @@ impl ExecServerHandler {
if stream_response {
self.reserve_http_body_stream(&http_request_id).await?;
}
let response = ReqwestHttpRequestRunner::new(params.timeout_ms, params.redirect_policy)?
let response = self
.http_client
.runner(params.redirect_policy)
.run(params)
.await;
if response.is_err() && stream_response {
@@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tokio::sync::mpsc;
@@ -88,6 +90,7 @@ async fn initialized_handler() -> Arc<ExecServerHandler> {
registry,
RpcNotificationSender::new(outgoing_tx),
test_runtime_paths(),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
));
let initialize_response = handler
.initialize(InitializeParams {
@@ -166,6 +169,7 @@ async fn long_poll_read_fails_after_session_resume() {
Arc::clone(&registry),
RpcNotificationSender::new(first_tx),
test_runtime_paths(),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
));
let initialize_response = first_handler
.initialize(InitializeParams {
@@ -206,6 +210,7 @@ async fn long_poll_read_fails_after_session_resume() {
registry,
RpcNotificationSender::new(second_tx),
test_runtime_paths(),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
));
second_handler
.initialize(InitializeParams {
@@ -239,6 +244,7 @@ async fn active_session_resume_is_rejected() {
Arc::clone(&registry),
RpcNotificationSender::new(first_tx),
test_runtime_paths(),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
));
let initialize_response = first_handler
.initialize(InitializeParams {
@@ -253,6 +259,7 @@ async fn active_session_resume_is_rejected() {
registry,
RpcNotificationSender::new(second_tx),
test_runtime_paths(),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
));
let err = second_handler
.initialize(InitializeParams {
@@ -281,6 +288,7 @@ async fn output_and_exit_are_retained_after_notification_receiver_closes() {
SessionRegistry::new(crate::ExecServerTelemetry::default()),
RpcNotificationSender::new(outgoing_tx),
test_runtime_paths(),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
));
handler
.initialize(InitializeParams {
+17 -1
View File
@@ -21,28 +21,38 @@ use crate::server::registry::build_router;
use crate::server::session_registry::SessionRegistry;
use crate::telemetry::ConnectionTransport;
use crate::telemetry::ExecServerTelemetry;
use codex_http_client::HttpClientFactory;
#[derive(Clone)]
pub(crate) struct ConnectionProcessor {
session_registry: Arc<SessionRegistry>,
runtime_paths: ExecServerRuntimePaths,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
}
impl ConnectionProcessor {
#[cfg(test)]
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
Self::new_with_telemetry(runtime_paths, ExecServerTelemetry::default())
Self::new_with_telemetry(
runtime_paths,
ExecServerTelemetry::default(),
codex_http_client::HttpClientFactory::new(
codex_http_client::OutboundProxyPolicy::ReqwestDefault,
),
)
}
pub(crate) fn new_with_telemetry(
runtime_paths: ExecServerRuntimePaths,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
) -> Self {
Self {
session_registry: SessionRegistry::new(telemetry.clone()),
runtime_paths,
telemetry,
http_client_factory,
}
}
@@ -56,6 +66,7 @@ impl ConnectionProcessor {
Arc::clone(&self.session_registry),
self.runtime_paths.clone(),
self.telemetry.clone(),
self.http_client_factory.clone(),
transport,
)
.await;
@@ -71,6 +82,7 @@ async fn run_connection(
session_registry: Arc<SessionRegistry>,
runtime_paths: ExecServerRuntimePaths,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
transport: ConnectionTransport,
) {
let _connection_metrics = telemetry.connection_started(transport);
@@ -90,6 +102,7 @@ async fn run_connection(
session_registry,
notifications,
runtime_paths,
http_client_factory,
));
let outbound_task = tokio::spawn(async move {
@@ -530,6 +543,9 @@ mod tests {
registry,
test_runtime_paths(),
crate::ExecServerTelemetry::default(),
codex_http_client::HttpClientFactory::new(
codex_http_client::OutboundProxyPolicy::ReqwestDefault,
),
crate::telemetry::ConnectionTransport::Stdio,
));
(client_writer, BufReader::new(client_reader).lines(), task)
+22 -5
View File
@@ -12,6 +12,7 @@ use axum::response::IntoResponse;
use axum::response::Response;
use axum::routing::any;
use axum::routing::get;
use codex_http_client::HttpClientFactory;
use std::io::Write as _;
use std::net::SocketAddr;
use tokio::io;
@@ -83,20 +84,32 @@ pub(crate) async fn run_transport(
listen_url: &str,
runtime_paths: ExecServerRuntimePaths,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match parse_listen_url(listen_url)? {
ExecServerListenTransport::WebSocket(bind_address) => {
run_websocket_listener(bind_address, runtime_paths, telemetry).await
run_websocket_listener(bind_address, runtime_paths, telemetry, http_client_factory)
.await
}
ExecServerListenTransport::Stdio => {
run_stdio_connection(runtime_paths, telemetry, http_client_factory).await
}
ExecServerListenTransport::Stdio => run_stdio_connection(runtime_paths, telemetry).await,
}
}
async fn run_stdio_connection(
runtime_paths: ExecServerRuntimePaths,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
run_stdio_connection_with_io(io::stdin(), io::stdout(), runtime_paths, telemetry).await
run_stdio_connection_with_io(
io::stdin(),
io::stdout(),
runtime_paths,
telemetry,
http_client_factory,
)
.await
}
async fn run_stdio_connection_with_io<R, W>(
@@ -104,12 +117,14 @@ async fn run_stdio_connection_with_io<R, W>(
writer: W,
runtime_paths: ExecServerRuntimePaths,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
R: AsyncRead + Unpin + Send + 'static,
W: AsyncWrite + Unpin + Send + 'static,
{
let processor = ConnectionProcessor::new_with_telemetry(runtime_paths, telemetry);
let processor =
ConnectionProcessor::new_with_telemetry(runtime_paths, telemetry, http_client_factory);
tracing::info!("codex-exec-server listening on stdio");
processor
.run_connection(
@@ -126,10 +141,12 @@ async fn run_websocket_listener(
bind_address: SocketAddr,
runtime_paths: ExecServerRuntimePaths,
telemetry: ExecServerTelemetry,
http_client_factory: HttpClientFactory,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind(bind_address).await?;
let local_addr = listener.local_addr()?;
let processor = ConnectionProcessor::new_with_telemetry(runtime_paths, telemetry);
let processor =
ConnectionProcessor::new_with_telemetry(runtime_paths, telemetry, http_client_factory);
info!("codex-exec-server listening on ws://{local_addr}");
println!("ws://{local_addr}");
std::io::stdout().flush()?;
@@ -62,6 +62,9 @@ async fn stdio_listen_transport_serves_initialize() {
server_writer,
test_runtime_paths(),
crate::ExecServerTelemetry::default(),
codex_http_client::HttpClientFactory::new(
codex_http_client::OutboundProxyPolicy::ReqwestDefault,
),
));
let mut client_lines = BufReader::new(client_reader).lines();