diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c45011..64ce52e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 dedicated hydrated status, so it is no longer indistinguishable from a real successful execution. A hydrated row also ranks with active work rather than completed successes (#2648). +- TUI HTTP clients now install the Rustls ring crypto provider before building + `reqwest` clients, covering engine, runtime API, tool, MCP, config, and skill + download paths. This keeps the no-provider TLS build from panicking during + tests or embedded startup paths that do not enter through the main binary. +- Prompt byte-stability tests now pin their temporary home and skills + environment under the shared test-env lock so global skill directories cannot + perturb deterministic prompt bytes during parallel test runs. ### Community diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 73c45011..64ce52e0 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -140,6 +140,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 dedicated hydrated status, so it is no longer indistinguishable from a real successful execution. A hydrated row also ranks with active work rather than completed successes (#2648). +- TUI HTTP clients now install the Rustls ring crypto provider before building + `reqwest` clients, covering engine, runtime API, tool, MCP, config, and skill + download paths. This keeps the no-provider TLS build from panicking during + tests or embedded startup paths that do not enter through the main binary. +- Prompt byte-stability tests now pin their temporary home and skills + environment under the shared test-env lock so global skill directories cannot + perturb deterministic prompt bytes during parallel test runs. ### Community diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index e53f1877..81745b8c 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -625,7 +625,7 @@ impl DeepSeekClient { base_url: &str, ) -> Result { let headers = build_default_headers(api_key, extra_headers, api_provider, base_url)?; - let mut builder = reqwest::Client::builder() + let mut builder = crate::tls::reqwest_client_builder() .default_headers(headers) .user_agent(concat!( "Mozilla/5.0 (compatible; codewhale/", diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 6d7f17ac..3b3bf862 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -5202,7 +5202,7 @@ fn refresh_kimi_oauth_token(refresh_token: &str) -> Result .or_else(|_| std::env::var("KIMI_OAUTH_HOST")) .unwrap_or_else(|_| "https://auth.kimi.com".to_string()); let url = format!("{}/api/oauth/token", oauth_host.trim_end_matches('/')); - let client = reqwest::blocking::Client::builder() + let client = crate::tls::reqwest_blocking_client_builder() .timeout(Duration::from_secs(15)) .build() .context("Failed to build Kimi OAuth refresh client")?; diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index d5632bef..0e7a1a6e 100644 --- a/crates/tui/src/config_ui.rs +++ b/crates/tui/src/config_ui.rs @@ -405,7 +405,7 @@ pub async fn start_web_editor(app: &App, config: &Config) -> Result = Some(app_snapshot); loop { tokio::time::sleep(Duration::from_millis(750)).await; diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 79cfb9b1..27960fe8 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -621,6 +621,8 @@ impl Engine { /// Create a new engine with the given configuration pub fn new(config: EngineConfig, api_config: &Config) -> (Self, EngineHandle) { + crate::tls::ensure_rustls_crypto_provider(); + if let Some(objective) = normalized_goal_objective(config.goal_objective.as_deref()) { sync_goal_state_from_host(&config.goal_state, Some(&objective), None, false); } diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 751d6e4f..3b024b2c 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -78,6 +78,7 @@ mod task_manager; #[cfg(test)] mod test_support; mod theme_qa_audit; +mod tls; mod tool_output_receipts; mod tools; mod tui; @@ -111,7 +112,7 @@ fn configure_windows_console_utf8() { fn configure_windows_console_utf8() {} fn install_rustls_crypto_provider() { - let _ = rustls::crypto::ring::default_provider().install_default(); + crate::tls::ensure_rustls_crypto_provider(); } #[derive(Parser, Debug)] diff --git a/crates/tui/src/mcp.rs b/crates/tui/src/mcp.rs index 038bfe16..c25fbe32 100644 --- a/crates/tui/src/mcp.rs +++ b/crates/tui/src/mcp.rs @@ -1319,8 +1319,8 @@ impl McpConnection { // local Clash / Shadowsocks tunnel, etc. previously had MCP // HTTP traffic bypass the proxy entirely while every other // tool on the box (curl, npm, …) used it. - let mut client_builder = - reqwest::Client::builder().timeout(Duration::from_secs(connect_timeout_secs)); + let mut client_builder = crate::tls::reqwest_client_builder() + .timeout(Duration::from_secs(connect_timeout_secs)); let env_proxy_url = std::env::var("HTTPS_PROXY") .or_else(|_| std::env::var("https_proxy")) .or_else(|_| std::env::var("HTTP_PROXY")) @@ -2942,7 +2942,7 @@ mod tests { fn test_http_client() -> reqwest::Client { let _ = rustls::crypto::ring::default_provider().install_default(); - reqwest::Client::new() + crate::tls::reqwest_client() } async fn lock_mcp_loopback_tests() -> tokio::sync::MutexGuard<'static, ()> { diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index f505338f..850e7944 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -2566,7 +2566,7 @@ mod tests { // in the cached prefix must produce identical bytes given identical // inputs across calls. - use crate::test_support::assert_byte_identical; + use crate::test_support::{EnvVarGuard, assert_byte_identical}; #[test] fn compose_prompt_is_byte_stable_across_calls() { @@ -2592,7 +2592,12 @@ mod tests { // identical bytes. This pins the most representative production // surface (engine.rs builds the system prompt via this fn or // its sibling _and_skills variant on every turn). + let _env_guard = crate::test_support::lock_test_env(); let tmp = tempdir().expect("tempdir"); + let home = tmp.path().join("home"); + let _home = EnvVarGuard::set("HOME", home.as_os_str()); + let _userprofile = EnvVarGuard::set("USERPROFILE", home.as_os_str()); + let _skills_dir = EnvVarGuard::remove("DEEPSEEK_SKILLS_DIR"); let workspace = tmp.path(); for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] { diff --git a/crates/tui/src/runtime_api.rs b/crates/tui/src/runtime_api.rs index 6d604b15..8dbf52c2 100644 --- a/crates/tui/src/runtime_api.rs +++ b/crates/tui/src/runtime_api.rs @@ -2721,7 +2721,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let health: serde_json::Value = client .get(format!("http://{addr}/health")) @@ -2786,7 +2786,7 @@ mod tests { else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let health = client .get(format!("http://{addr}/health")) @@ -2825,7 +2825,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let workspace: serde_json::Value = client .get(format!("http://{addr}/v1/workspace/status")) @@ -2949,7 +2949,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .post(format!("http://{addr}/v1/stream")) @@ -2966,7 +2966,7 @@ mod tests { let Some((addr, runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let created: serde_json::Value = client .post(format!("http://{addr}/v1/threads")) @@ -3238,7 +3238,7 @@ mod tests { let Some((addr, runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let created: serde_json::Value = client .post(format!("http://{addr}/v1/threads")) @@ -3364,7 +3364,7 @@ mod tests { let Some((addr, runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let created: serde_json::Value = client .post(format!("http://{addr}/v1/threads")) @@ -3587,7 +3587,7 @@ mod tests { let Some((addr, runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); // Create a thread and install a mock engine so /v1/stream doesn't call the real API. let created: serde_json::Value = client @@ -3704,7 +3704,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .get(format!("http://{addr}/v1/sessions/nonexistent_id")) @@ -3721,7 +3721,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let get_resp = client .get(format!("http://{addr}/v1/sessions/invalid%20id")) @@ -3753,7 +3753,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .post(format!( @@ -3808,7 +3808,7 @@ mod tests { else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .post(format!( @@ -3849,7 +3849,7 @@ mod tests { else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let created: serde_json::Value = client .post(format!("http://{addr}/v1/threads")) @@ -3956,7 +3956,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .post(format!("http://{addr}/v1/sessions")) @@ -3974,7 +3974,7 @@ mod tests { let Some((addr, runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let created: serde_json::Value = client .post(format!("http://{addr}/v1/threads")) @@ -4086,7 +4086,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .delete(format!("http://{addr}/v1/sessions/nonexistent-id")) .send() @@ -4121,7 +4121,7 @@ mod tests { let _ = axum::serve(listener, router).await; }); - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); // The user-supplied origin is allowed. let resp = client @@ -4191,7 +4191,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let created: serde_json::Value = client .post(format!("http://{addr}/v1/threads")) @@ -4277,7 +4277,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); // Two threads — keep one active, archive the other. let active: serde_json::Value = client @@ -4388,7 +4388,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let body: serde_json::Value = client .get(format!("http://{addr}/v1/usage")) @@ -4447,7 +4447,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let info: serde_json::Value = client .get(format!("http://{addr}/v1/runtime/info")) .send() @@ -4478,7 +4478,7 @@ mod tests { else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let disabled = client.get(format!("http://{addr}/mobile")).send().await?; assert_eq!(disabled.status(), StatusCode::NOT_FOUND); handle.abort(); @@ -4517,7 +4517,7 @@ mod tests { else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let unauthorized = client.get(format!("http://{addr}/mobile")).send().await?; assert_eq!(unauthorized.status(), StatusCode::UNAUTHORIZED); @@ -4552,7 +4552,7 @@ mod tests { else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let page = client .get(format!("http://{addr}/mobile")) @@ -4577,7 +4577,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .post(format!("http://{addr}/v1/approvals/no_such_id")) .json(&json!({ "decision": "allow" })) @@ -4594,7 +4594,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .post(format!("http://{addr}/v1/approvals/whatever")) .json(&json!({ "decision": "yolo" })) @@ -4611,7 +4611,7 @@ mod tests { let Some((addr, runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let rx = runtime_threads.register_pending_approval_for_test("ext_id"); let resp = client @@ -4640,7 +4640,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let body: serde_json::Value = client .get(format!("http://{addr}/v1/skills")) .send() @@ -4663,7 +4663,7 @@ mod tests { let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else { return Ok(()); }; - let client = reqwest::Client::new(); + let client = crate::tls::reqwest_client(); let resp = client .post(format!("http://{addr}/v1/skills/no-such-skill")) .json(&json!({ "enabled": false })) diff --git a/crates/tui/src/sandbox/opensandbox.rs b/crates/tui/src/sandbox/opensandbox.rs index b2ffa4c3..5a49eb62 100644 --- a/crates/tui/src/sandbox/opensandbox.rs +++ b/crates/tui/src/sandbox/opensandbox.rs @@ -54,7 +54,7 @@ impl OpenSandboxBackend { /// `Authorization: Bearer ` when set. `timeout_secs` controls the /// HTTP request timeout. pub fn new(base_url: String, api_key: Option, timeout_secs: u64) -> Result { - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_secs(timeout_secs)) .build() .context("failed to construct HTTP client for OpenSandbox backend")?; diff --git a/crates/tui/src/skills/install.rs b/crates/tui/src/skills/install.rs index 8262aa37..ace55ed4 100644 --- a/crates/tui/src/skills/install.rs +++ b/crates/tui/src/skills/install.rs @@ -45,6 +45,11 @@ use thiserror::Error; use crate::network_policy::{Decision, NetworkPolicy, host_from_url}; +fn reqwest_client() -> reqwest::Client { + let _ = rustls::crypto::ring::default_provider().install_default(); + reqwest::Client::new() +} + /// Cache directory for registry-synced skills. /// /// Lives at `~/.codewhale/cache/skills/` so it's separate from user-installed @@ -497,7 +502,9 @@ pub async fn fetch_registry( Decision::Deny => return Ok(RegistryFetchResult::Denied(host)), Decision::Prompt => return Ok(RegistryFetchResult::NeedsApproval(host)), } - let body = reqwest::get(registry_url) + let body = reqwest_client() + .get(registry_url) + .send() .await .with_context(|| format!("failed to fetch registry {registry_url}"))? .error_for_status() @@ -665,7 +672,7 @@ async fn sync_one_skill( .flatten(); // Build the request — add If-None-Match if we have a cached ETag. - let client = reqwest::Client::new(); + let client = reqwest_client(); let mut req = client.get(url); if let Some(ref meta) = existing_meta && let Some(ref etag) = meta.etag @@ -981,7 +988,9 @@ enum DownloadAttempt { /// would push the buffer over `max_size * 4` (the *4 accounts for compression; /// the unpack step still enforces `max_size` on the *uncompressed* bytes). async fn download_with_cap(url: &str, max_size: u64) -> Result { - let resp = reqwest::get(url) + let resp = reqwest_client() + .get(url) + .send() .await .with_context(|| format!("failed to GET {url}"))?; let status = resp.status(); diff --git a/crates/tui/src/tls.rs b/crates/tui/src/tls.rs new file mode 100644 index 00000000..448f6e51 --- /dev/null +++ b/crates/tui/src/tls.rs @@ -0,0 +1,19 @@ +pub(crate) fn ensure_rustls_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + +#[allow(dead_code)] +pub(crate) fn reqwest_client() -> reqwest::Client { + ensure_rustls_crypto_provider(); + reqwest::Client::new() +} + +pub(crate) fn reqwest_client_builder() -> reqwest::ClientBuilder { + ensure_rustls_crypto_provider(); + reqwest::Client::builder() +} + +pub(crate) fn reqwest_blocking_client_builder() -> reqwest::blocking::ClientBuilder { + ensure_rustls_crypto_provider(); + reqwest::blocking::Client::builder() +} diff --git a/crates/tui/src/tools/fetch_url.rs b/crates/tui/src/tools/fetch_url.rs index 194392af..a013b0ea 100644 --- a/crates/tui/src/tools/fetch_url.rs +++ b/crates/tui/src/tools/fetch_url.rs @@ -163,7 +163,7 @@ impl ToolSpec for FetchUrlTool { let resp = loop { let dns_pinning = validate_fetch_target(¤t_url, context).await?; - let mut client_builder = reqwest::Client::builder() + let mut client_builder = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .user_agent(USER_AGENT) .redirect(reqwest::redirect::Policy::none()); diff --git a/crates/tui/src/tools/finance.rs b/crates/tui/src/tools/finance.rs index 02331f31..95d705d2 100644 --- a/crates/tui/src/tools/finance.rs +++ b/crates/tui/src/tools/finance.rs @@ -151,7 +151,7 @@ impl FinanceTool { pub fn new() -> Self { Self { endpoints: FinanceEndpoints::default(), - client: Client::builder() + client: crate::tls::reqwest_client_builder() .user_agent(USER_AGENT) .build() .expect("failed to build HTTP client"), @@ -165,7 +165,7 @@ impl FinanceTool { quote_base: quote_base.into(), chart_base: chart_base.into(), }, - client: Client::builder() + client: crate::tls::reqwest_client_builder() .user_agent(USER_AGENT) .build() .expect("failed to build HTTP client"), diff --git a/crates/tui/src/tools/web_run.rs b/crates/tui/src/tools/web_run.rs index f1495d36..73c17612 100644 --- a/crates/tui/src/tools/web_run.rs +++ b/crates/tui/src/tools/web_run.rs @@ -774,7 +774,7 @@ async fn run_search( timeout_ms: u64, domains: &[String], ) -> Result<(Vec, String, Option), ToolError> { - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .user_agent(USER_AGENT) .build() @@ -970,7 +970,7 @@ async fn run_image_search( timeout_ms: u64, domains: &[String], ) -> Result<(Vec, Option), ToolError> { - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .user_agent(USER_AGENT) .build() @@ -1123,7 +1123,7 @@ fn check_network_policy(url: &str, context: &ToolContext) -> Result<(), ToolErro } async fn fetch_page(url: &str, timeout_ms: u64) -> Result { - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .user_agent(USER_AGENT) .build() diff --git a/crates/tui/src/tools/web_search.rs b/crates/tui/src/tools/web_search.rs index f5f8595f..5984d791 100644 --- a/crates/tui/src/tools/web_search.rs +++ b/crates/tui/src/tools/web_search.rs @@ -242,7 +242,7 @@ impl ToolSpec for WebSearchTool { } let decider = context.network_policy.as_ref(); - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .user_agent(USER_AGENT) .build() @@ -382,7 +382,7 @@ impl WebSearchTool { ) })?; - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .build() .map_err(|e| { @@ -479,7 +479,7 @@ impl WebSearchTool { ) })?; - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .build() .map_err(|e| { @@ -588,7 +588,7 @@ impl WebSearchTool { .or(env_key.as_deref()) .unwrap_or(METASO_DEFAULT_API_KEY); - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .build() .map_err(|e| { @@ -693,7 +693,7 @@ impl WebSearchTool { ) })?; - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_millis(timeout_ms)) .build() .map_err(|e| { @@ -778,7 +778,7 @@ impl WebSearchTool { // when it exceeds 90_000 ms. let effective_timeout = timeout_ms.max(90_000); - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .connect_timeout(Duration::from_secs(15)) .timeout(Duration::from_millis(effective_timeout)) .tcp_keepalive(Some(Duration::from_secs(30))) diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 8985b2fb..f957a10b 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -1080,7 +1080,7 @@ const BALANCE_FETCH_COOLDOWN: Duration = Duration::from_secs(60); /// Shared `reqwest::Client` for balance fetches so connection pools are /// reused across successive background polls. static BALANCE_CLIENT: LazyLock<::reqwest::Client> = LazyLock::new(|| { - ::reqwest::Client::builder() + crate::tls::reqwest_client_builder() .timeout(Duration::from_secs(10)) .build() .unwrap_or_default() diff --git a/crates/tui/src/vision/tools.rs b/crates/tui/src/vision/tools.rs index f80ad396..94911b24 100644 --- a/crates/tui/src/vision/tools.rs +++ b/crates/tui/src/vision/tools.rs @@ -23,7 +23,7 @@ pub struct ImageAnalyzeTool { impl ImageAnalyzeTool { #[must_use] pub fn new(config: VisionModelConfig) -> Self { - let client = reqwest::Client::builder() + let client = crate::tls::reqwest_client_builder() .timeout(Duration::from_secs(120)) .build() .expect("Failed to build HTTP client"); diff --git a/docs/V0_9_0_EXECUTION_MAP.md b/docs/V0_9_0_EXECUTION_MAP.md index 513ae876..dc6d3d8c 100644 --- a/docs/V0_9_0_EXECUTION_MAP.md +++ b/docs/V0_9_0_EXECUTION_MAP.md @@ -155,6 +155,7 @@ v0.9 branch so the remaining Windows/manual checks are explicit. | #2755 roll back provider after auth failure | Draft / forwarded | Snapshot+rollback of provider/model on auth failure (#2754). Design is sound and tested, but author opened it as draft noting they could not reproduce the live Moonshot auth failure end-to-end. Help-forward: needs maintainer validation against a real provider auth failure (engine respawn + model restore). Credit @cyq1017. | | #2756 Xiaomi MiMo Token Plan region docs | Mergeable / locally harvested | Docs-only; verified accurate against branch `resolve_xiaomi_mimo_base_url` (tp- keys default to `token-plan-sgp`, pay-as-you-go to `api.xiaomimimo.com`, CN requires explicit `base_url`). Conflict on the CONFIGURATION.md provider bullet resolved by keeping the branch `path_suffix` bullet and adopting the PR's accurate base_url wording. Credit @xyuai; comment/close after branch is public. Fixes #2735. | | #2757 hydrated deferred-tool render | Mergeable / locally harvested | Harvested in full (6 files): deferred-tool first-use schema hydration now renders as "tool loaded — retry required" via `ToolStatus::Hydrated` instead of "run done". Local correction: hydrated rows rank with active work (rank 1) not completed successes; kept the contributor's hydration detection (sole emitter always sets `executed=false`, consistent with the engine's own check, so the missing-field default was not changed). `cargo test -p codewhale-tui --bin codewhale-tui --locked hydrat` (6 pass), clippy clean. Credit @mvanhorn; comment/close after branch is public. Fixes #2648. | +| Local verification sweep stabilizer | Added after the full workspace verification sweep found test-only no-provider TLS panics and prompt byte instability. | Shared TUI Rustls provider helpers now wrap `reqwest` client construction across engine, runtime API, tool, MCP, config, and skill paths; the skill-installer integration include keeps its own local helper. Prompt byte-stability tests pin home and skills env under the shared test-env lock. Evidence: `cargo fmt --all -- --check`, `git diff --check`, `./scripts/release/check-versions.sh`, `cargo clippy --workspace --all-features --locked -- -D warnings`, focused skill/finance/goal/MCP reruns, and `cargo test --workspace --all-features --locked` all passed locally. | ## Issue Reduction Strategy