fix(tui): install rustls provider before HTTP clients

Install the ring rustls provider through a shared TUI helper and route reqwest client construction through it so no-provider TLS builds do not panic in engine, runtime API, tool, MCP, config, and test paths.

Keep the skill-installer integration include compatible with a local helper, and pin prompt byte-stability tests to an isolated home/skills environment under the shared env lock.

Verification: cargo fmt --all -- --check; git diff --check; ./scripts/release/check-versions.sh; cargo clippy --workspace --all-features --locked -- -D warnings; cargo test --workspace --all-features --locked; focused skill_install, finance, goal-tool, and MCP reruns.
This commit is contained in:
Hunter B
2026-06-04 18:50:20 -07:00
parent 70adeeeae6
commit de86cc1860
20 changed files with 106 additions and 55 deletions
+7
View File
@@ -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
+7
View File
@@ -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
+1 -1
View File
@@ -625,7 +625,7 @@ impl DeepSeekClient {
base_url: &str,
) -> Result<reqwest::Client> {
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/",
+1 -1
View File
@@ -5202,7 +5202,7 @@ fn refresh_kimi_oauth_token(refresh_token: &str) -> Result<KimiOAuthCredential>
.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")?;
+1 -1
View File
@@ -405,7 +405,7 @@ pub async fn start_web_editor(app: &App, config: &Config) -> Result<WebConfigSes
let poll_tx = tx.clone();
let poll_url = format!("{url}/api/session");
let poll_task = tokio::spawn(async move {
let client = reqwest::Client::new();
let client = crate::tls::reqwest_client();
let mut last: Option<ConfigUiDocument> = Some(app_snapshot);
loop {
tokio::time::sleep(Duration::from_millis(750)).await;
+2
View File
@@ -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);
}
+2 -1
View File
@@ -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)]
+3 -3
View File
@@ -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, ()> {
+6 -1
View File
@@ -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] {
+29 -29
View File
@@ -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 }))
+1 -1
View File
@@ -54,7 +54,7 @@ impl OpenSandboxBackend {
/// `Authorization: Bearer <key>` when set. `timeout_secs` controls the
/// HTTP request timeout.
pub fn new(base_url: String, api_key: Option<String>, timeout_secs: u64) -> Result<Self> {
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")?;
+12 -3
View File
@@ -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<DownloadAttempt> {
let resp = reqwest::get(url)
let resp = reqwest_client()
.get(url)
.send()
.await
.with_context(|| format!("failed to GET {url}"))?;
let status = resp.status();
+19
View File
@@ -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()
}
+1 -1
View File
@@ -163,7 +163,7 @@ impl ToolSpec for FetchUrlTool {
let resp = loop {
let dns_pinning = validate_fetch_target(&current_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());
+2 -2
View File
@@ -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"),
+3 -3
View File
@@ -774,7 +774,7 @@ async fn run_search(
timeout_ms: u64,
domains: &[String],
) -> Result<(Vec<SearchEntry>, String, Option<String>), 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<ImageResultEntry>, Option<String>), 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<WebPage, ToolError> {
let client = reqwest::Client::builder()
let client = crate::tls::reqwest_client_builder()
.timeout(Duration::from_millis(timeout_ms))
.user_agent(USER_AGENT)
.build()
+6 -6
View File
@@ -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)))
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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");
+1
View File
@@ -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