diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ce08f95f99..01b99bc363 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2451,10 +2451,10 @@ dependencies = [ "codex-tui", "codex-utils-cli", "crossterm", + "http 1.4.0", "owo-colors", "pretty_assertions", "ratatui", - "reqwest 0.12.28", "serde", "serde_json", "supports-color 3.0.2", diff --git a/codex-rs/cloud-tasks/Cargo.toml b/codex-rs/cloud-tasks/Cargo.toml index d842a95a94..be3ec552c8 100644 --- a/codex-rs/cloud-tasks/Cargo.toml +++ b/codex-rs/cloud-tasks/Cargo.toml @@ -27,9 +27,9 @@ codex-model-provider = { workspace = true } codex-tui = { workspace = true } codex-utils-cli = { workspace = true } crossterm = { workspace = true, features = ["event-stream"] } +http = { workspace = true } owo-colors = { workspace = true, features = ["supports-colors"] } ratatui = { workspace = true } -reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } supports-color = { workspace = true } diff --git a/codex-rs/cloud-tasks/src/env_detect.rs b/codex-rs/cloud-tasks/src/env_detect.rs index 6137f27ea8..f4b0aea1a8 100644 --- a/codex-rs/cloud-tasks/src/env_detect.rs +++ b/codex-rs/cloud-tasks/src/env_detect.rs @@ -1,6 +1,6 @@ -use codex_http_client::build_reqwest_client_with_custom_ca; -use reqwest::header::CONTENT_TYPE; -use reqwest::header::HeaderMap; +use codex_http_client::RouteAwareClientPool; +use http::header::CONTENT_TYPE; +use http::header::HeaderMap; use std::collections::HashMap; use tracing::info; use tracing::warn; @@ -23,6 +23,7 @@ pub struct AutodetectSelection { } pub async fn autodetect_environment_id( + http: &RouteAwareClientPool, base_url: &str, headers: &HeaderMap, desired_label: Option, @@ -45,7 +46,7 @@ pub async fn autodetect_environment_id( ) }; crate::append_error_log(format!("env: GET {url}")); - match get_json::>(&url, headers).await { + match get_json::>(http, &url, headers).await { Ok(mut list) => { crate::append_error_log(format!( "env: by-repo returned {} env(s) for {owner}/{repo}", @@ -74,8 +75,13 @@ 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 = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?; - let res = http.get(&list_url).headers(headers.clone()).send().await?; + let res = http + .client_for_url(&list_url) + .await? + .get(&list_url) + .headers(headers.clone()) + .send() + .await?; let status = res.status(); let ct = res .headers() @@ -145,11 +151,17 @@ fn pick_environment_row( } async fn get_json( + http: &RouteAwareClientPool, url: &str, headers: &HeaderMap, ) -> anyhow::Result { - let http = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?; - let res = http.get(url).headers(headers.clone()).send().await?; + let res = http + .client_for_url(url) + .await? + .get(url) + .headers(headers.clone()) + .send() + .await?; let status = res.status(); let ct = res .headers() @@ -254,6 +266,7 @@ fn parse_owner_repo(url: &str) -> Option<(String, String)> { /// List environments for the current repo(s) with a fallback to the global list. /// Returns a de-duplicated, sorted set suitable for the TUI modal. pub async fn list_environments( + http: &RouteAwareClientPool, base_url: &str, headers: &HeaderMap, ) -> anyhow::Result> { @@ -274,7 +287,7 @@ pub async fn list_environments( base_url, "github", owner, repo ) }; - match get_json::>(&url, headers).await { + match get_json::>(http, &url, headers).await { Ok(list) => { info!("env_tui: by-repo {}:{} -> {} envs", owner, repo, list.len()); for e in list { @@ -312,7 +325,7 @@ pub async fn list_environments( } else { format!("{base_url}/api/codex/environments") }; - match get_json::>(&list_url, headers).await { + match get_json::>(http, &list_url, headers).await { Ok(list) => { info!("env_tui: global list -> {} envs", list.len()); for e in list { diff --git a/codex-rs/cloud-tasks/src/lib.rs b/codex-rs/cloud-tasks/src/lib.rs index 525b2e5f51..b6ea9c5bdf 100644 --- a/codex-rs/cloud-tasks/src/lib.rs +++ b/codex-rs/cloud-tasks/src/lib.rs @@ -12,6 +12,10 @@ use chrono::Utc; use codex_cloud_tasks_client::TaskStatus; use codex_git_utils::current_branch_name; use codex_git_utils::default_branch_name; +use codex_http_client::ClientRouteClass; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; +use codex_http_client::RouteAwareClientPool; use codex_login::default_client::get_codex_user_agent; use owo_colors::OwoColorize; use owo_colors::Stream; @@ -38,6 +42,7 @@ struct ApplyJob { struct BackendContext { backend: Arc, base_url: String, + environment_http: RouteAwareClientPool, } async fn init_backend(user_agent_suffix: &str) -> anyhow::Result { @@ -56,11 +61,17 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result return Ok(BackendContext { backend: Arc::new(codex_cloud_tasks_mock_client::MockClient), base_url, + environment_http: RouteAwareClientPool::new( + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + ClientRouteClass::Api, + ), }); } let ua = get_codex_user_agent(); let (auth_manager, http_client_factory) = util::load_auth_manager(Some(base_url.clone())).await; + let environment_http = + RouteAwareClientPool::new(http_client_factory.clone(), ClientRouteClass::Api); let mut http = codex_cloud_tasks_client::HttpClient::new(base_url.clone(), http_client_factory)? .with_user_agent(ua); @@ -105,6 +116,7 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result Ok(BackendContext { backend: Arc::new(http), base_url, + environment_http, }) } @@ -192,7 +204,8 @@ async fn resolve_environment_id(ctx: &BackendContext, requested: &str) -> anyhow } let normalized = util::normalize_base_url(&ctx.base_url); let headers = util::build_chatgpt_headers().await; - let environments = crate::env_detect::list_environments(&normalized, &headers).await?; + let environments = + crate::env_detect::list_environments(&ctx.environment_http, &normalized, &headers).await?; if environments.is_empty() { return Err(anyhow!( "no cloud environments are available for this workspace" @@ -759,8 +772,11 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an .try_init(); info!("Launching Cloud Tasks list UI"); - let BackendContext { backend, .. } = init_backend("codex_cloud_tasks_tui").await?; - let backend = backend; + let BackendContext { + backend, + base_url, + environment_http, + } = init_backend("codex_cloud_tasks_tui").await?; // Terminal setup use crossterm::ExecutableCommand; @@ -840,34 +856,25 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an }); } // Fetch environment list in parallel so the header can show friendly names quickly. - { - let tx = tx.clone(); - tokio::spawn(async move { - let base_url = util::normalize_base_url( - &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") - .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), - ); - let headers = util::build_chatgpt_headers().await; - let res = crate::env_detect::list_environments(&base_url, &headers).await; - let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); - }); - } + spawn_environment_load(tx.clone(), base_url.clone(), environment_http.clone()); // Try to auto-detect a likely environment id on startup and refresh if found. // Do this concurrently so the initial list shows quickly; on success we refetch with filter. { let tx = tx.clone(); + let base_url = base_url.clone(); + let environment_http = environment_http.clone(); tokio::spawn(async move { - let base_url = util::normalize_base_url( - &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") - .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), - ); + let base_url = util::normalize_base_url(&base_url); // Build headers: UA + ChatGPT auth if available let headers = util::build_chatgpt_headers().await; // Run autodetect. If it fails, we keep using "All". let res = crate::env_detect::autodetect_environment_id( - &base_url, &headers, /*desired_label*/ None, + &environment_http, + &base_url, + &headers, + /*desired_label*/ None, ) .await; let _ = tx.send(app::AppEvent::EnvironmentAutodetected(res)); @@ -1081,18 +1088,11 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an } // Proactively fetch environments to resolve a friendly name for the header. app.env_loading = true; - { - let tx = tx.clone(); - tokio::spawn(async move { - let base_url = crate::util::normalize_base_url( - &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") - .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), - ); - let headers = crate::util::build_chatgpt_headers().await; - let res = crate::env_detect::list_environments(&base_url, &headers).await; - let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); - }); - } + spawn_environment_load( + tx.clone(), + base_url.clone(), + environment_http.clone(), + ); let _ = frame_tx.send(Instant::now()); } } @@ -1468,13 +1468,11 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an } needs_redraw = true; if should_fetch { - let tx = tx.clone(); - tokio::spawn(async move { - let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string())); - let headers = crate::util::build_chatgpt_headers().await; - let res = crate::env_detect::list_environments(&base_url, &headers).await; - let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); - }); + spawn_environment_load( + tx.clone(), + base_url.clone(), + environment_http.clone(), + ); } // Render after opening env modal to show it instantly. render_if_needed(&mut terminal, &mut app, &mut needs_redraw)?; @@ -1654,16 +1652,11 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an if app.environments.is_empty() { app.env_loading = true; app.env_error = None; } needs_redraw = true; if app.environments.is_empty() { - let tx = tx.clone(); - tokio::spawn(async move { - let base_url = crate::util::normalize_base_url( - &std::env::var("CODEX_CLOUD_TASKS_BASE_URL") - .unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string()), - ); - let headers = crate::util::build_chatgpt_headers().await; - let res = crate::env_detect::list_environments(&base_url, &headers).await; - let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); - }); + spawn_environment_load( + tx.clone(), + base_url.clone(), + environment_http.clone(), + ); } } KeyCode::Left => { @@ -1833,13 +1826,11 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an if should_fetch { app.env_loading = true; app.env_error = None; } needs_redraw = true; if should_fetch { - let tx = tx.clone(); - tokio::spawn(async move { - let base_url = crate::util::normalize_base_url(&std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string())); - let headers = crate::util::build_chatgpt_headers().await; - let res = crate::env_detect::list_environments(&base_url, &headers).await; - let _ = tx.send(app::AppEvent::EnvironmentsLoaded(res)); - }); + spawn_environment_load( + tx.clone(), + base_url.clone(), + environment_http.clone(), + ); } } KeyCode::Char('n') => { @@ -2021,6 +2012,19 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option) -> an Ok(()) } +fn spawn_environment_load( + tx: UnboundedSender, + base_url: String, + http: RouteAwareClientPool, +) { + tokio::spawn(async move { + let base_url = util::normalize_base_url(&base_url); + let headers = util::build_chatgpt_headers().await; + let result = crate::env_detect::list_environments(&http, &base_url, &headers).await; + let _ = tx.send(app::AppEvent::EnvironmentsLoaded(result)); + }); +} + // extract_chatgpt_account_id moved to util.rs /// Build plain-text conversation lines: a labeled user prompt followed by assistant messages. diff --git a/codex-rs/cloud-tasks/src/util.rs b/codex-rs/cloud-tasks/src/util.rs index aa197c14af..aff4cfe552 100644 --- a/codex-rs/cloud-tasks/src/util.rs +++ b/codex-rs/cloud-tasks/src/util.rs @@ -1,7 +1,7 @@ use chrono::DateTime; use chrono::Local; use chrono::Utc; -use reqwest::header::HeaderMap; +use http::header::HeaderMap; use codex_core::config::Config; use codex_http_client::HttpClientFactory; @@ -70,8 +70,8 @@ pub async fn load_auth_manager( /// Build headers for ChatGPT-backed requests: `User-Agent`, optional `Authorization`, /// and optional `ChatGPT-Account-Id`. pub async fn build_chatgpt_headers() -> HeaderMap { - use reqwest::header::HeaderValue; - use reqwest::header::USER_AGENT; + use http::header::HeaderValue; + use http::header::USER_AGENT; set_user_agent_suffix("codex_cloud_tasks_tui"); let ua = codex_login::default_client::get_codex_user_agent(); diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index e420271c5c..293960d4ec 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -243,7 +243,6 @@ deny = [ "codex-api", "codex-app-server", "codex-app-server-daemon", - "codex-cloud-tasks", "codex-core", "codex-core-plugins", "codex-exec-server",