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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ In the codex-rs folder where the rust code lives:
repo root to refresh `MODULE.bazel.lock`, and include that lockfile update in the same change.
- After dependency changes, run `just bazel-lock-check` from the repo root so lockfile drift is caught
locally before CI.
- Bazel does not automatically make source-tree files available to compile-time Rust file access. If
you add `include_str!`, `include_bytes!`, `sqlx::migrate!`, or similar build-time file or
directory reads, update the crate's `BUILD.bazel` (`compile_data`, `build_script_data`, or test
data) or Bazel may fail even when Cargo passes.
- Do not create small helper methods that are referenced only once.
- Avoid large modules:
- Prefer adding new modules instead of growing existing ones.
Expand Down
15 changes: 12 additions & 3 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/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ rustls = { version = "0.23", default-features = false, features = [
"ring",
"std",
] }
rustls-native-certs = "0.8.3"
rustls-pki-types = "1.14.0"
schemars = "0.8.22"
seccompiler = "0.5.0"
Expand Down
1 change: 1 addition & 0 deletions codex-rs/backend-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
codex-backend-openapi-models = { path = "../codex-backend-openapi-models" }
codex-client = { workspace = true }
codex-protocol = { workspace = true }
codex-core = { workspace = true }

Expand Down
3 changes: 2 additions & 1 deletion codex-rs/backend-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::types::PaginatedListTaskListItem;
use crate::types::RateLimitStatusPayload;
use crate::types::TurnAttemptsSiblingTurnsResponse;
use anyhow::Result;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_core::auth::CodexAuth;
use codex_core::default_client::get_codex_user_agent;
use codex_protocol::account::PlanType as AccountPlanType;
Expand Down Expand Up @@ -120,7 +121,7 @@ impl Client {
{
base_url = format!("{base_url}/backend-api");
}
let http = reqwest::Client::builder().build()?;
let http = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
let path_style = PathStyle::from_base_url(&base_url);
Ok(Self {
base_url,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/cloud-tasks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ codex-cloud-tasks-client = { path = "../cloud-tasks-client", features = [
"mock",
"online",
] }
codex-client = { workspace = true }
codex-core = { path = "../core" }
codex-login = { path = "../login" }
codex-tui = { path = "../tui" }
Expand Down
5 changes: 3 additions & 2 deletions codex-rs/cloud-tasks/src/env_detect.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use codex_client::build_reqwest_client_with_custom_ca;
use reqwest::header::CONTENT_TYPE;
use reqwest::header::HeaderMap;
use std::collections::HashMap;
Expand Down Expand Up @@ -73,7 +74,7 @@ pub async fn autodetect_environment_id(
};
crate::append_error_log(format!("env: GET {list_url}"));
// Fetch and log the full environments JSON for debugging
let http = reqwest::Client::builder().build()?;
let http = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
let res = http.get(&list_url).headers(headers.clone()).send().await?;
let status = res.status();
let ct = res
Expand Down Expand Up @@ -147,7 +148,7 @@ async fn get_json<T: serde::de::DeserializeOwned>(
url: &str,
headers: &HeaderMap,
) -> anyhow::Result<T> {
let http = reqwest::Client::builder().build()?;
let http = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
let res = http.get(url).headers(headers.clone()).send().await?;
let status = res.status();
let ct = res
Expand Down
20 changes: 14 additions & 6 deletions codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::endpoint::realtime_websocket::protocol::SessionUpdateSession;
use crate::endpoint::realtime_websocket::protocol::parse_realtime_event;
use crate::error::ApiError;
use crate::provider::Provider;
use codex_client::maybe_build_rustls_client_config_with_custom_ca;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use futures::SinkExt;
use futures::StreamExt;
Expand Down Expand Up @@ -474,12 +475,19 @@ impl RealtimeWebsocketClient {
request.headers_mut().extend(headers);

info!("connecting realtime websocket: {ws_url}");
let (stream, response) =
tokio_tungstenite::connect_async_with_config(request, Some(websocket_config()), false)
.await
.map_err(|err| {
ApiError::Stream(format!("failed to connect realtime websocket: {err}"))
})?;
// Realtime websocket TLS should honor the same custom-CA env vars as the rest of Codex's
// outbound HTTPS and websocket traffic.
let connector = maybe_build_rustls_client_config_with_custom_ca()
.map_err(|err| ApiError::Stream(format!("failed to configure websocket TLS: {err}")))?
.map(tokio_tungstenite::Connector::Rustls);
let (stream, response) = tokio_tungstenite::connect_async_tls_with_config(
request,
Some(websocket_config()),
false,
connector,
)
.await
.map_err(|err| ApiError::Stream(format!("failed to connect realtime websocket: {err}")))?;
info!(
ws_url = %ws_url,
status = %response.status(),
Expand Down
12 changes: 11 additions & 1 deletion codex-rs/codex-api/src/endpoint/responses_websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::sse::responses::ResponsesStreamEvent;
use crate::sse::responses::process_responses_event;
use crate::telemetry::WebsocketTelemetry;
use codex_client::TransportError;
use codex_client::maybe_build_rustls_client_config_with_custom_ca;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use futures::SinkExt;
use futures::StreamExt;
Expand All @@ -30,6 +31,7 @@ use tokio::sync::oneshot;
use tokio::time::Instant;
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::connect_async_tls_with_config;
use tokio_tungstenite::tungstenite::Error as WsError;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
Expand Down Expand Up @@ -331,10 +333,18 @@ async fn connect_websocket(
.map_err(|err| ApiError::Stream(format!("failed to build websocket request: {err}")))?;
request.headers_mut().extend(headers);

let response = tokio_tungstenite::connect_async_with_config(
// Secure websocket traffic needs the same custom-CA policy as reqwest-based HTTPS traffic.
// If a Codex-specific CA bundle is configured, build an explicit rustls connector so this
// websocket path does not fall back to tungstenite's default native-roots-only behavior.
let connector = maybe_build_rustls_client_config_with_custom_ca()
.map_err(|err| ApiError::Stream(format!("failed to configure websocket TLS: {err}")))?
.map(tokio_tungstenite::Connector::Rustls);

let response = connect_async_tls_with_config(
request,
Some(websocket_config()),
false, // `false` means "do not disable Nagle", which is tungstenite's recommended default.
connector,
)
.await;

Expand Down
1 change: 1 addition & 0 deletions codex-rs/codex-client/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "codex-client",
crate_name = "codex_client",
compile_data = glob(["tests/fixtures/**"]),
)
7 changes: 7 additions & 0 deletions codex-rs/codex-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,24 @@ http = { workspace = true }
opentelemetry = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream"] }
rustls = { workspace = true }
rustls-native-certs = { workspace = true }
rustls-pki-types = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt", "time", "sync"] }
tracing = { workspace = true }
tracing-opentelemetry = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
zstd = { workspace = true }

[lints]
workspace = true

[dev-dependencies]
codex-utils-cargo-bin = { workspace = true }
opentelemetry_sdk = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
tracing-subscriber = { workspace = true }
29 changes: 29 additions & 0 deletions codex-rs/codex-client/src/bin/custom_ca_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Helper binary for exercising shared custom CA environment handling in tests.
//!
//! The shared reqwest client honors `CODEX_CA_CERTIFICATE` and `SSL_CERT_FILE`, but those
//! environment variables are process-global and unsafe to mutate in parallel test execution. This
//! probe keeps the behavior under test while letting integration tests (`tests/ca_env.rs`) set
//! env vars per-process, proving:
//!
//! - env precedence is respected,
//! - multi-cert PEM bundles load,
//! - error messages guide users when CA files are invalid.
//!
//! The detailed explanation of what "hermetic" means here lives in `codex_client::custom_ca`.
//! This binary exists so the tests can exercise
//! [`codex_client::build_reqwest_client_for_subprocess_tests`] in a separate process without
//! duplicating client-construction logic.

use std::process;

fn main() {
match codex_client::build_reqwest_client_for_subprocess_tests(reqwest::Client::builder()) {
Ok(_) => {
println!("ok");
}
Err(error) => {
eprintln!("{error}");
process::exit(1);
}
}
}
Loading
Loading