From 5dcd3ff34c7f14b3a698e6078fbab65c96200f22 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Thu, 18 Jun 2026 20:51:09 -0400 Subject: [PATCH] move ApiClient to goose-providers, lifting out TlsConfig (#9869) --- Cargo.lock | 91 ++++++----------- crates/goose-cli/Cargo.toml | 2 + crates/goose-providers/Cargo.toml | 20 ++++ .../src}/api_client.rs | 97 ++++--------------- crates/goose-providers/src/lib.rs | 1 + crates/goose-server/Cargo.toml | 2 + crates/goose/Cargo.toml | 10 +- .../goose/src/config/declarative_providers.rs | 16 +-- crates/goose/src/config/mod.rs | 1 + crates/goose/src/config/signup_nanogpt/mod.rs | 9 +- crates/goose/src/config/tls.rs | 38 ++++++++ crates/goose/src/dictation/providers.rs | 11 ++- crates/goose/src/providers/amp_acp.rs | 4 +- crates/goose/src/providers/anthropic.rs | 28 ++++-- crates/goose/src/providers/avian.rs | 4 +- crates/goose/src/providers/azure.rs | 7 +- crates/goose/src/providers/base.rs | 5 +- crates/goose/src/providers/bedrock.rs | 8 +- crates/goose/src/providers/chatgpt_codex.rs | 8 +- crates/goose/src/providers/claude_acp.rs | 4 +- crates/goose/src/providers/claude_code.rs | 1 + crates/goose/src/providers/codex.rs | 1 + crates/goose/src/providers/codex_acp.rs | 4 +- crates/goose/src/providers/copilot_acp.rs | 4 +- crates/goose/src/providers/cursor_agent.rs | 8 +- crates/goose/src/providers/databricks.rs | 38 ++------ crates/goose/src/providers/databricks_v2.rs | 23 ++--- crates/goose/src/providers/gcpvertexai.rs | 8 +- crates/goose/src/providers/gemini_cli.rs | 8 +- crates/goose/src/providers/gemini_oauth.rs | 8 +- crates/goose/src/providers/githubcopilot.rs | 14 ++- crates/goose/src/providers/google.rs | 12 ++- crates/goose/src/providers/huggingface.rs | 17 +++- crates/goose/src/providers/init.rs | 5 +- crates/goose/src/providers/kimicode.rs | 8 +- crates/goose/src/providers/litellm.rs | 16 ++- crates/goose/src/providers/local_inference.rs | 1 + crates/goose/src/providers/mod.rs | 4 +- crates/goose/src/providers/nanogpt.rs | 28 ++++-- crates/goose/src/providers/ollama.rs | 25 +++-- crates/goose/src/providers/openai.rs | 44 ++++++--- .../goose/src/providers/openai_compatible.rs | 3 +- crates/goose/src/providers/openrouter.rs | 10 +- crates/goose/src/providers/pi_acp.rs | 4 +- .../goose/src/providers/provider_registry.rs | 47 ++++++--- crates/goose/src/providers/sagemaker_tgi.rs | 8 +- crates/goose/src/providers/snowflake.rs | 11 ++- crates/goose/src/providers/testprovider.rs | 1 + crates/goose/src/providers/tetrate.rs | 10 +- crates/goose/src/providers/xai.rs | 4 +- crates/goose/src/providers/xai_oauth.rs | 4 +- crates/goose/tests/acp_fixtures/mod.rs | 3 +- crates/goose/tests/agent.rs | 4 + crates/goose/tests/compaction.rs | 1 + crates/goose/tests/mcp_integration_test.rs | 1 + .../tests/session_id_propagation_test.rs | 3 +- crates/goose/tests/tetrate_streaming.rs | 4 +- 57 files changed, 449 insertions(+), 312 deletions(-) rename crates/{goose/src/providers => goose-providers/src}/api_client.rs (88%) create mode 100644 crates/goose/src/config/tls.rs diff --git a/Cargo.lock b/Cargo.lock index 33523f476..fd2078105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2232,8 +2232,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "961b955a666e25ee5a1091d219128d6e6401e3dab84efb1a2bf6b4035d797b39" dependencies = [ "crmf", - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", "x509-cert", ] @@ -2244,8 +2244,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" dependencies = [ "const-oid 0.9.6", - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", "x509-cert", ] @@ -2580,8 +2580,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36fe21b96d5b87f5de4b5b7202ec41c00110ac817ce6728fe75fb2fe5962ed92" dependencies = [ "cms", - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", "x509-cert", ] @@ -3350,16 +3350,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" -dependencies = [ - "const-oid 0.10.2", - "zeroize", -] - [[package]] name = "der-parser" version = "10.0.0" @@ -3701,12 +3691,12 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.10", + "der", "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", - "spki 0.7.3", + "spki", ] [[package]] @@ -3724,7 +3714,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8 0.10.2", + "pkcs8", "signature", ] @@ -3765,7 +3755,7 @@ dependencies = [ "group", "hkdf", "pem-rfc7468", - "pkcs8 0.10.2", + "pkcs8", "rand_core 0.6.4", "sec1", "subtle", @@ -4819,9 +4809,6 @@ dependencies = [ "opentelemetry_sdk 0.32.1", "pastey", "pctx_code_mode", - "pem", - "pkcs1", - "pkcs8 0.11.0", "process-wrap", "pulldown-cmark", "rand 0.8.6", @@ -4832,7 +4819,6 @@ dependencies = [ "rubato", "rustls", "schemars 1.2.1", - "sec1", "serde", "serde_json", "serde_urlencoded", @@ -4990,10 +4976,14 @@ dependencies = [ "env-lock", "futures", "once_cell", + "pem", + "pkcs1", + "pkcs8", "rand 0.8.6", "regex", "reqwest 0.13.4", "rmcp", + "sec1", "serde", "serde_json", "strum 0.28.0", @@ -5004,6 +4994,7 @@ dependencies = [ "tokio-stream", "tracing", "unicode-normalization", + "url", "utoipa 4.2.3", "uuid", ] @@ -8097,9 +8088,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", + "der", + "pkcs8", + "spki", ] [[package]] @@ -8108,18 +8099,8 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" -dependencies = [ - "der 0.8.0", - "spki 0.8.0", + "der", + "spki", ] [[package]] @@ -9045,10 +9026,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8 0.10.2", + "pkcs8", "rand_core 0.6.4", "signature", - "spki 0.7.3", + "spki", "subtle", "zeroize", ] @@ -9419,9 +9400,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der 0.7.10", + "der", "generic-array", - "pkcs8 0.10.2", + "pkcs8", "subtle", "zeroize", ] @@ -9835,14 +9816,14 @@ dependencies = [ "aws-lc-rs", "base64 0.22.1", "const-oid 0.9.6", - "der 0.7.10", + "der", "digest 0.10.7", "pem", "rand_core 0.9.5", "sha2 0.10.9", "signature", "sigstore-types", - "spki 0.7.3", + "spki", "thiserror 2.0.18", "tracing", "x509-cert", @@ -9908,7 +9889,7 @@ dependencies = [ "cmpv2", "cms", "const-oid 0.9.6", - "der 0.7.10", + "der", "hex", "jiff", "rand 0.9.4", @@ -10123,17 +10104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.10", -] - -[[package]] -name = "spki" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" -dependencies = [ - "base64ct", - "der 0.8.0", + "der", ] [[package]] @@ -13667,10 +13638,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", - "der 0.7.10", + "der", "sha1", "signature", - "spki 0.7.3", + "spki", "tls_codec", ] @@ -13700,7 +13671,7 @@ checksum = "f5ceece934a21607055b7ac5c25adb56a2ff559804b10705dc674d1d838c15e1" dependencies = [ "cmpv2", "cms", - "der 0.7.10", + "der", ] [[package]] diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 17eb2cedc..cbd1b7919 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -103,12 +103,14 @@ rustls-tls = [ "sigstore-verify?/rustls", "goose/rustls-tls", "goose-mcp/rustls-tls", + "goose-providers/rustls-tls", ] native-tls = [ "reqwest/native-tls", "sigstore-verify?/native-tls", "goose/native-tls", "goose-mcp/native-tls", + "goose-providers/native-tls", ] [dev-dependencies] diff --git a/crates/goose-providers/Cargo.toml b/crates/goose-providers/Cargo.toml index c054ea972..fa626714a 100644 --- a/crates/goose-providers/Cargo.toml +++ b/crates/goose-providers/Cargo.toml @@ -11,6 +11,21 @@ description.workspace = true [lints] workspace = true +[features] +default = [] +rustls-tls = [ + "reqwest/rustls", + "rmcp/reqwest", +] +native-tls = [ + "dep:pem", + "dep:pkcs1", + "dep:pkcs8", + "dep:sec1", + "reqwest/native-tls", + "rmcp/reqwest-native-tls", +] + [dependencies] anyhow = { workspace = true } async-stream = { workspace = true } @@ -32,6 +47,11 @@ async-trait = { workspace = true } strum = { workspace = true } tokio = { workspace = true } rand = { workspace = true } +url = { workspace = true } +pem = { version = "3.0.2", default-features = false, features = ["std"], optional = true } +pkcs1 = { version = "0.7.5", default-features = false, features = ["pkcs8", "std"], optional = true } +pkcs8 = { version = "0.10.2", default-features = false, features = ["alloc", "std"], optional = true } +sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8", "std"], optional = true } [dev-dependencies] test-case = { workspace = true } diff --git a/crates/goose/src/providers/api_client.rs b/crates/goose-providers/src/api_client.rs similarity index 88% rename from crates/goose/src/providers/api_client.rs rename to crates/goose-providers/src/api_client.rs index 940a0493c..2493e6ada 100644 --- a/crates/goose/src/providers/api_client.rs +++ b/crates/goose-providers/src/api_client.rs @@ -1,5 +1,3 @@ -use crate::providers::base::DEFAULT_PROVIDER_TIMEOUT_SECS; -use crate::session_context::SESSION_ID_HEADER; use anyhow::Result; use async_trait::async_trait; use reqwest::{ @@ -15,6 +13,9 @@ use std::fs::read_to_string; use std::path::PathBuf; use std::time::Duration; +const DEFAULT_PROVIDER_TIMEOUT_SECS: u64 = 600; +const SESSION_ID_HEADER: &str = "agent-session-id"; + pub struct ApiClient { client: Client, host: String, @@ -28,12 +29,7 @@ pub struct ApiClient { pub enum AuthMethod { NoAuth, BearerToken(String), - ApiKey { - header_name: String, - key: String, - }, - #[allow(dead_code)] - OAuth(OAuthConfig), + ApiKey { header_name: String, key: String }, Custom(Box), } @@ -57,48 +53,6 @@ impl TlsConfig { } } - pub fn from_config() -> Result> { - let config = crate::config::Config::global(); - let mut tls_config = TlsConfig::new(); - let mut has_tls_config = false; - - let client_cert_path = config.get_param::("GOOSE_CLIENT_CERT_PATH").ok(); - let client_key_path = config.get_param::("GOOSE_CLIENT_KEY_PATH").ok(); - - // Validate that both cert and key are provided if either is provided - match (client_cert_path, client_key_path) { - (Some(cert_path), Some(key_path)) => { - tls_config = tls_config.with_client_cert_and_key( - std::path::PathBuf::from(cert_path), - std::path::PathBuf::from(key_path), - ); - has_tls_config = true; - } - (Some(_), None) => { - return Err(anyhow::anyhow!( - "Client certificate provided (GOOSE_CLIENT_CERT_PATH) but no private key (GOOSE_CLIENT_KEY_PATH)" - )); - } - (None, Some(_)) => { - return Err(anyhow::anyhow!( - "Client private key provided (GOOSE_CLIENT_KEY_PATH) but no certificate (GOOSE_CLIENT_CERT_PATH)" - )); - } - (None, None) => {} - } - - if let Ok(ca_cert_path) = config.get_param::("GOOSE_CA_CERT_PATH") { - tls_config = tls_config.with_ca_cert(std::path::PathBuf::from(ca_cert_path)); - has_tls_config = true; - } - - if has_tls_config { - Ok(Some(tls_config)) - } else { - Ok(None) - } - } - pub fn with_client_cert_and_key(mut self, cert_path: PathBuf, key_path: PathBuf) -> Self { self.client_identity = Some(TlsCertKeyPair { cert_path, @@ -234,13 +188,6 @@ fn convert_key_to_pkcs8_pem(key_pem_str: &str) -> Result { } } -pub struct OAuthConfig { - pub host: String, - pub client_id: String, - pub redirect_url: String, - pub scopes: Vec, -} - #[async_trait] pub trait AuthProvider: Send + Sync { async fn get_auth_header(&self) -> Result<(String, String)>; @@ -261,7 +208,6 @@ impl fmt::Debug for AuthMethod { .field("header_name", header_name) .field("key", &"[hidden]") .finish(), - AuthMethod::OAuth(_) => f.debug_tuple("OAuth").field(&"[config]").finish(), AuthMethod::Custom(_) => f.debug_tuple("Custom").field(&"[provider]").finish(), } } @@ -283,19 +229,27 @@ pub struct ApiRequestBuilder<'a> { } impl ApiClient { - pub fn new(host: String, auth: AuthMethod) -> Result { - Self::with_timeout( + pub fn new_with_tls( + host: String, + auth: AuthMethod, + tls_config: Option, + ) -> Result { + Self::with_timeout_and_tls( host, auth, Duration::from_secs(DEFAULT_PROVIDER_TIMEOUT_SECS), + tls_config, ) } - pub fn with_timeout(host: String, auth: AuthMethod, timeout: Duration) -> Result { + pub fn with_timeout_and_tls( + host: String, + auth: AuthMethod, + timeout: Duration, + tls_config: Option, + ) -> Result { let mut client_builder = Client::builder().timeout(timeout); - // Configure TLS if needed - let tls_config = TlsConfig::from_config()?; if let Some(ref config) = tls_config { client_builder = Self::configure_tls(client_builder, config)?; } @@ -445,16 +399,6 @@ impl ApiClient { Ok(url) } - - async fn get_oauth_token(&self, config: &OAuthConfig) -> Result { - super::oauth::get_oauth_token_async( - &config.host, - &config.client_id, - &config.redirect_url, - &config.scopes, - ) - .await - } } impl<'a> ApiRequestBuilder<'a> { @@ -518,10 +462,6 @@ impl<'a> ApiRequestBuilder<'a> { request.header("Authorization", format!("Bearer {}", token)) } AuthMethod::ApiKey { header_name, key } => request.header(header_name.as_str(), key), - AuthMethod::OAuth(config) => { - let token = self.client.get_oauth_token(config).await?; - request.header("Authorization", format!("Bearer {}", token)) - } AuthMethod::Custom(provider) => { let (header_name, header_value) = provider.get_auth_header().await?; request.header(header_name, header_value) @@ -680,9 +620,10 @@ mod tests { ) { let runtime = tokio::runtime::Runtime::new().unwrap(); runtime.block_on(async { - let client = ApiClient::new( + let client = ApiClient::new_with_tls( "http://localhost:8080".to_string(), AuthMethod::BearerToken("test-token".to_string()), + None, ) .unwrap(); diff --git a/crates/goose-providers/src/lib.rs b/crates/goose-providers/src/lib.rs index 3ab49ef87..41c62f7b1 100644 --- a/crates/goose-providers/src/lib.rs +++ b/crates/goose-providers/src/lib.rs @@ -1,3 +1,4 @@ +pub mod api_client; pub mod base; pub mod canonical; pub mod conversation; diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index de3f874e1..dd4304798 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -41,6 +41,7 @@ rustls-tls = [ "dep:aws-lc-rs", "goose/rustls-tls", "goose-mcp/rustls-tls", + "goose-providers/rustls-tls", ] native-tls = [ "reqwest/native-tls", @@ -49,6 +50,7 @@ native-tls = [ "dep:openssl", "goose/native-tls", "goose-mcp/native-tls", + "goose-providers/native-tls", ] [dependencies] diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 062e54279..4cba9d8c3 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -52,12 +52,9 @@ rustls-tls = [ "oauth2/reqwest", "oauth2/rustls-tls", "hf-hub/rustls-tls", + "goose-providers/rustls-tls", ] native-tls = [ - "dep:pem", - "dep:pkcs1", - "dep:pkcs8", - "dep:sec1", "reqwest/native-tls", "rmcp/reqwest-native-tls", "smithy-transport-reqwest?/native-tls", @@ -65,6 +62,7 @@ native-tls = [ "jsonwebtoken/rust_crypto", "oauth2/reqwest", "oauth2/native-tls", + "goose-providers/native-tls", ] system-keyring = ["dep:keyring"] portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] @@ -196,10 +194,6 @@ pulldown-cmark = { version = "0.13", default-features = false } encoding_rs = { version = "0.8.35", default-features = false } pastey = { version = "0.2", default-features = false } shell-words = { workspace = true } -pem = { version = "3.0.2", default-features = false, features = ["std"], optional = true } -pkcs1 = { version = "0.7.5", default-features = false, features = ["pkcs8", "std"], optional = true } -pkcs8 = { version = "0.11.0", default-features = false, features = ["alloc", "std"], optional = true } -sec1 = { version = "0.7", default-features = false, features = ["der", "pkcs8", "std"], optional = true } goose-acp-macros = { path = "../goose-acp-macros", default-features = false } tower-http = { workspace = true, features = ["cors"] } http-body-util = { version = "0.1.2", default-features = false } diff --git a/crates/goose/src/config/declarative_providers.rs b/crates/goose/src/config/declarative_providers.rs index e391ecec0..d3a162a2f 100644 --- a/crates/goose/src/config/declarative_providers.rs +++ b/crates/goose/src/config/declarative_providers.rs @@ -585,10 +585,10 @@ pub fn register_declarative_provider( &config, provider_type, config.dynamic_models.unwrap_or(false), - move |model| { + move |model, tls_config| { let mut cfg = captured.clone(); resolve_config(&mut cfg)?; - HuggingFaceProvider::from_custom_config(model, cfg) + HuggingFaceProvider::from_custom_config(model, cfg, tls_config) }, move || { let mut cfg = identity_config.clone(); @@ -608,10 +608,10 @@ pub fn register_declarative_provider( &config, provider_type, config.dynamic_models.unwrap_or(false), - move |model| { + move |model, tls_config| { let mut cfg = captured.clone(); resolve_config(&mut cfg)?; - OpenAiProvider::from_custom_config(model, cfg) + OpenAiProvider::from_custom_config(model, cfg, tls_config) }, move || { let mut cfg = identity_config.clone(); @@ -628,10 +628,10 @@ pub fn register_declarative_provider( &config, provider_type, config.dynamic_models.unwrap_or(false), - move |model| { + move |model, tls_config| { let mut cfg = captured.clone(); resolve_config(&mut cfg)?; - OllamaProvider::from_custom_config(model, cfg) + OllamaProvider::from_custom_config(model, cfg, tls_config) }, move || { let mut cfg = identity_config.clone(); @@ -647,10 +647,10 @@ pub fn register_declarative_provider( &config, provider_type, config.dynamic_models.unwrap_or(false), - move |model| { + move |model, tls_config| { let mut cfg = captured.clone(); resolve_config(&mut cfg)?; - AnthropicProvider::from_custom_config(model, cfg) + AnthropicProvider::from_custom_config(model, cfg, tls_config) }, move || { let mut cfg = identity_config.clone(); diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index 78bef69f0..13abeec00 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -10,6 +10,7 @@ pub mod search_path; pub mod signup_nanogpt; pub mod signup_openrouter; pub mod signup_tetrate; +pub mod tls; pub use crate::agents::ExtensionConfig; pub use base::{merge_config_values, Config, ConfigError}; diff --git a/crates/goose/src/config/signup_nanogpt/mod.rs b/crates/goose/src/config/signup_nanogpt/mod.rs index 45699f6c4..ecd6e906a 100644 --- a/crates/goose/src/config/signup_nanogpt/mod.rs +++ b/crates/goose/src/config/signup_nanogpt/mod.rs @@ -23,8 +23,13 @@ struct PollResponse { } fn build_client() -> Result { - ApiClient::new(NANOGPT_CLI_LOGIN_HOST.to_string(), AuthMethod::NoAuth)? - .with_header("x-client", "goose") + let tls_config = crate::config::tls::provider_tls_config_from_config(Config::global())?; + ApiClient::new_with_tls( + NANOGPT_CLI_LOGIN_HOST.to_string(), + AuthMethod::NoAuth, + tls_config, + )? + .with_header("x-client", "goose") } async fn poll_for_token(client: &ApiClient, device_code: &str) -> Result { diff --git a/crates/goose/src/config/tls.rs b/crates/goose/src/config/tls.rs new file mode 100644 index 000000000..b53e76276 --- /dev/null +++ b/crates/goose/src/config/tls.rs @@ -0,0 +1,38 @@ +use crate::config::Config; +use crate::providers::api_client::TlsConfig; +use anyhow::{bail, Result}; +use std::path::PathBuf; + +pub fn provider_tls_config_from_config(config: &Config) -> Result> { + let mut tls_config = TlsConfig::new(); + let mut has_tls_config = false; + + let client_cert_path = config.get_param::("GOOSE_CLIENT_CERT_PATH").ok(); + let client_key_path = config.get_param::("GOOSE_CLIENT_KEY_PATH").ok(); + + match (client_cert_path, client_key_path) { + (Some(cert_path), Some(key_path)) => { + tls_config = tls_config + .with_client_cert_and_key(PathBuf::from(cert_path), PathBuf::from(key_path)); + has_tls_config = true; + } + (Some(_), None) => { + bail!( + "Client certificate provided (GOOSE_CLIENT_CERT_PATH) but no private key (GOOSE_CLIENT_KEY_PATH)" + ); + } + (None, Some(_)) => { + bail!( + "Client private key provided (GOOSE_CLIENT_KEY_PATH) but no certificate (GOOSE_CLIENT_CERT_PATH)" + ); + } + (None, None) => {} + } + + if let Ok(ca_cert_path) = config.get_param::("GOOSE_CA_CERT_PATH") { + tls_config = tls_config.with_ca_cert(PathBuf::from(ca_cert_path)); + has_tls_config = true; + } + + Ok(has_tls_config.then_some(tls_config)) +} diff --git a/crates/goose/src/dictation/providers.rs b/crates/goose/src/dictation/providers.rs index 2d2378970..8dd3d1ee5 100644 --- a/crates/goose/src/dictation/providers.rs +++ b/crates/goose/src/dictation/providers.rs @@ -1,3 +1,4 @@ +use crate::config::tls::provider_tls_config_from_config; use crate::config::Config; #[cfg(feature = "local-inference")] use crate::dictation::whisper::LOCAL_WHISPER_MODEL_CONFIG_KEY; @@ -251,10 +252,12 @@ fn build_api_client(provider: DictationProvider) -> Result<(ApiClient, String)> DictationProvider::Local => anyhow::bail!("Local provider should not use API client"), }; - let mut client = ApiClient::with_timeout(base_url, auth, REQUEST_TIMEOUT).map_err(|e| { - tracing::error!("Failed to create API client: {}", e); - e - })?; + let tls = provider_tls_config_from_config(config)?; + let mut client = ApiClient::with_timeout_and_tls(base_url, auth, REQUEST_TIMEOUT, tls) + .map_err(|e| { + tracing::error!("Failed to create API client: {}", e); + e + })?; if !query_params.is_empty() { client = client.with_query(query_params); } diff --git a/crates/goose/src/providers/amp_acp.rs b/crates/goose/src/providers/amp_acp.rs index efa625094..0082df2d3 100644 --- a/crates/goose/src/providers/amp_acp.rs +++ b/crates/goose/src/providers/amp_acp.rs @@ -43,14 +43,16 @@ impl ProviderDef for AmpAcpProvider { fn from_env( model: ModelConfig, extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Self::from_env_with_working_dir(model, extensions, current_working_dir()) + Self::from_env_with_working_dir(model, extensions, current_working_dir(), tls_config) } fn from_env_with_working_dir( model: ModelConfig, extensions: Vec, working_dir: PathBuf, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = Config::global(); diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index 76bf83b83..519f57105 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -63,7 +63,10 @@ pub struct AnthropicProvider { } impl AnthropicProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let model = crate::model_config::with_configured_fast_model( model, ANTHROPIC_PROVIDER_NAME, @@ -81,8 +84,8 @@ impl AnthropicProvider { key: api_key, }; - let api_client = - ApiClient::new(host, auth)?.with_header("anthropic-version", ANTHROPIC_API_VERSION)?; + let api_client = ApiClient::new_with_tls(host, auth, tls_config)? + .with_header("anthropic-version", ANTHROPIC_API_VERSION)?; Ok(Self { api_client, @@ -99,6 +102,7 @@ impl AnthropicProvider { pub fn from_custom_config( model: ModelConfig, config: DeclarativeProviderConfig, + tls_config: Option, ) -> Result { let custom_models = if !config.models.is_empty() { Some( @@ -132,7 +136,7 @@ impl AnthropicProvider { let format_options = Self::format_options_for_provider(config.preserves_thinking); - let mut api_client = ApiClient::new(config.base_url, auth)? + let mut api_client = ApiClient::new_with_tls(config.base_url, auth, tls_config)? .with_header("anthropic-version", ANTHROPIC_API_VERSION)?; if let Some(headers) = &config.headers { @@ -267,8 +271,9 @@ impl ProviderDef for AnthropicProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } @@ -379,7 +384,7 @@ mod tests { header_name: "x-api-key".to_string(), key: "test-key".to_string(), }; - let api_client = ApiClient::new(server_uri.to_string(), auth) + let api_client = ApiClient::new_with_tls(server_uri.to_string(), auth, None) .unwrap() .with_header("anthropic-version", ANTHROPIC_API_VERSION) .unwrap(); @@ -444,10 +449,13 @@ mod tests { #[test] fn from_custom_config_rejects_static_only_without_models() { let config = base_declarative_config(vec![], Some(false)); - let err = - AnthropicProvider::from_custom_config(ModelConfig::new_or_fail("claude-test"), config) - .err() - .expect("expected construction error for dynamic_models: false with empty models"); + let err = AnthropicProvider::from_custom_config( + ModelConfig::new_or_fail("claude-test"), + config, + None, + ) + .err() + .expect("expected construction error for dynamic_models: false with empty models"); let msg = err.to_string(); assert!( msg.contains("dynamic_models: false"), diff --git a/crates/goose/src/providers/avian.rs b/crates/goose/src/providers/avian.rs index 3a92a5dc5..ad179b5ed 100644 --- a/crates/goose/src/providers/avian.rs +++ b/crates/goose/src/providers/avian.rs @@ -39,6 +39,7 @@ impl ProviderDef for AvianProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = crate::config::Config::global(); @@ -47,7 +48,8 @@ impl ProviderDef for AvianProvider { .get_param("AVIAN_HOST") .unwrap_or_else(|_| AVIAN_API_HOST.to_string()); - let api_client = ApiClient::new(host, AuthMethod::BearerToken(api_key))?; + let api_client = + ApiClient::new_with_tls(host, AuthMethod::BearerToken(api_key), tls_config)?; Ok(OpenAiCompatibleProvider::new( AVIAN_PROVIDER_NAME.to_string(), diff --git a/crates/goose/src/providers/azure.rs b/crates/goose/src/providers/azure.rs index 161bb28bf..b2e096a61 100644 --- a/crates/goose/src/providers/azure.rs +++ b/crates/goose/src/providers/azure.rs @@ -74,6 +74,7 @@ impl ProviderDef for AzureProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = crate::config::Config::global(); @@ -105,7 +106,11 @@ impl ProviderDef for AzureProvider { let auth_provider = AzureAuthProvider { auth }; let host = format!("{}/openai", endpoint.trim_end_matches('/')); - let mut api_client = ApiClient::new(host, AuthMethod::Custom(Box::new(auth_provider)))?; + let mut api_client = ApiClient::new_with_tls( + host, + AuthMethod::Custom(Box::new(auth_provider)), + tls_config, + )?; if let Some(version) = api_version { api_client = api_client.with_query(vec![("api-version".to_string(), version)]); } diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 61af875a3..d64e7309e 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -1,3 +1,4 @@ +use super::api_client::TlsConfig; use anyhow::Result; use futures::future::BoxFuture; pub use goose_providers::conversation::token_usage::{ @@ -245,6 +246,7 @@ pub trait ProviderDef: Send + Sync { fn from_env( model: ModelConfig, extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> where Self: Sized; @@ -253,13 +255,14 @@ pub trait ProviderDef: Send + Sync { model: ModelConfig, extensions: Vec, _working_dir: PathBuf, + tls_config: Option, ) -> BoxFuture<'static, Result> where Self: Sized, { // ACP subprocess providers must override this so session cwd is preserved. // Non-subprocess providers can rely on the default because cwd is irrelevant. - Self::from_env(model, extensions) + Self::from_env(model, extensions, tls_config) } } diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index cfb8069e0..0193c6652 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -79,7 +79,10 @@ struct ConverseRequestParts { } impl BedrockProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); // Attempt to load config and secrets to get AWS_ prefixed keys @@ -710,8 +713,9 @@ impl ProviderDef for BedrockProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/chatgpt_codex.rs b/crates/goose/src/providers/chatgpt_codex.rs index 9b0479d7c..7a78368ac 100644 --- a/crates/goose/src/providers/chatgpt_codex.rs +++ b/crates/goose/src/providers/chatgpt_codex.rs @@ -894,7 +894,10 @@ impl ChatGptCodexProvider { Ok(()) } - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let auth_provider = Arc::new(ChatGptCodexAuthProvider::new( ChatGptCodexAuthState::instance(), )); @@ -976,8 +979,9 @@ impl ProviderDef for ChatGptCodexProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/claude_acp.rs b/crates/goose/src/providers/claude_acp.rs index 32c70d251..839e42fd5 100644 --- a/crates/goose/src/providers/claude_acp.rs +++ b/crates/goose/src/providers/claude_acp.rs @@ -41,14 +41,16 @@ impl ProviderDef for ClaudeAcpProvider { fn from_env( model: ModelConfig, extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Self::from_env_with_working_dir(model, extensions, current_working_dir()) + Self::from_env_with_working_dir(model, extensions, current_working_dir(), tls_config) } fn from_env_with_working_dir( model: ModelConfig, extensions: Vec, working_dir: PathBuf, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = Config::global(); diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 0b85ccbb8..1e51f8b7d 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -604,6 +604,7 @@ impl ProviderDef for ClaudeCodeProvider { fn from_env( model: ModelConfig, extensions: Vec, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = crate::config::Config::global(); diff --git a/crates/goose/src/providers/codex.rs b/crates/goose/src/providers/codex.rs index 12684cf36..c7f054dd4 100644 --- a/crates/goose/src/providers/codex.rs +++ b/crates/goose/src/providers/codex.rs @@ -637,6 +637,7 @@ impl ProviderDef for CodexProvider { fn from_env( model: ModelConfig, extensions: Vec, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = Config::global(); diff --git a/crates/goose/src/providers/codex_acp.rs b/crates/goose/src/providers/codex_acp.rs index f1631f1e7..83b3095fc 100644 --- a/crates/goose/src/providers/codex_acp.rs +++ b/crates/goose/src/providers/codex_acp.rs @@ -40,14 +40,16 @@ impl ProviderDef for CodexAcpProvider { fn from_env( model: ModelConfig, extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Self::from_env_with_working_dir(model, extensions, current_working_dir()) + Self::from_env_with_working_dir(model, extensions, current_working_dir(), tls_config) } fn from_env_with_working_dir( model: ModelConfig, extensions: Vec, working_dir: PathBuf, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = Config::global(); diff --git a/crates/goose/src/providers/copilot_acp.rs b/crates/goose/src/providers/copilot_acp.rs index 2d0c03b49..db41641e8 100644 --- a/crates/goose/src/providers/copilot_acp.rs +++ b/crates/goose/src/providers/copilot_acp.rs @@ -44,14 +44,16 @@ impl ProviderDef for CopilotAcpProvider { fn from_env( model: ModelConfig, extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Self::from_env_with_working_dir(model, extensions, current_working_dir()) + Self::from_env_with_working_dir(model, extensions, current_working_dir(), tls_config) } fn from_env_with_working_dir( model: ModelConfig, extensions: Vec, working_dir: PathBuf, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = Config::global(); diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index 680213498..7b8a4468f 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -37,7 +37,10 @@ pub struct CursorAgentProvider { } impl CursorAgentProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let command: String = config.get_cursor_agent_command().unwrap_or_default().into(); let resolved_command = SearchPaths::builder().with_npm().resolve(&command)?; @@ -297,8 +300,9 @@ impl ProviderDef for CursorAgentProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index 2e18dc0c1..683337d1a 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -97,7 +97,10 @@ impl DatabricksProvider { super::oauth::cleanup_oauth_cache() } - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let mut host: Result = config.get_param("DATABRICKS_HOST"); @@ -131,10 +134,11 @@ impl DatabricksProvider { token_cache: token_cache.clone(), })); - let api_client = ApiClient::with_timeout( + let api_client = ApiClient::with_timeout_and_tls( host.clone(), auth_method, Duration::from_secs(DEFAULT_PROVIDER_TIMEOUT_SECS), + tls_config.clone(), )?; let mut provider = Self { @@ -189,33 +193,6 @@ impl DatabricksProvider { ) } - pub fn from_params(host: String, api_key: String, model: ModelConfig) -> Result { - let token_cache = Arc::new(Mutex::new(Some(api_key.clone()))); - let auth = DatabricksAuth::token(api_key); - let auth_method = AuthMethod::Custom(Box::new(DatabricksAuthProvider { - auth: auth.clone(), - token_cache: token_cache.clone(), - })); - - let api_client = ApiClient::with_timeout( - host.clone(), - auth_method, - Duration::from_secs(DEFAULT_PROVIDER_TIMEOUT_SECS), - )?; - - Ok(Self { - api_client, - host, - auth, - model, - image_format: ImageFormat::OpenAi, - retry_config: RetryConfig::default(), - name: DATABRICKS_PROVIDER_NAME.to_string(), - token_cache, - instance_id: Self::resolve_instance_id(), - }) - } - fn resolve_instance_id() -> Option { let enabled = crate::config::Config::global() .get_param::("GOOSE_DATABRICKS_CLIENT_REQUEST_ID") @@ -569,8 +546,9 @@ impl ProviderDef for DatabricksProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs index b07fe66a5..fb6ee4d8b 100644 --- a/crates/goose/src/providers/databricks_v2.rs +++ b/crates/goose/src/providers/databricks_v2.rs @@ -67,7 +67,10 @@ impl DatabricksV2Provider { super::oauth::cleanup_oauth_cache() } - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let mut host: Result = config.get_param("DATABRICKS_HOST"); @@ -91,16 +94,7 @@ impl DatabricksV2Provider { DatabricksAuth::oauth(host.clone()) }; - Self::new(host, auth, model, retry_config) - } - - pub fn from_params(host: String, api_key: String, model: ModelConfig) -> Result { - Self::new( - host, - DatabricksAuth::token(api_key), - model, - RetryConfig::default(), - ) + Self::new(host, auth, model, retry_config, tls_config) } fn new( @@ -108,6 +102,7 @@ impl DatabricksV2Provider { auth: DatabricksAuth, model: ModelConfig, retry_config: RetryConfig, + tls_config: Option, ) -> Result { let token_cache = Arc::new(Mutex::new(match &auth { DatabricksAuth::Token(t) => Some(t.clone()), @@ -119,10 +114,11 @@ impl DatabricksV2Provider { token_cache: token_cache.clone(), })); - let api_client = ApiClient::with_timeout( + let api_client = ApiClient::with_timeout_and_tls( host, auth_method, Duration::from_secs(DEFAULT_PROVIDER_TIMEOUT_SECS), + tls_config, )?; Ok(Self { @@ -358,8 +354,9 @@ impl ProviderDef for DatabricksV2Provider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/gcpvertexai.rs b/crates/goose/src/providers/gcpvertexai.rs index 0a595ec39..2d3dc654a 100644 --- a/crates/goose/src/providers/gcpvertexai.rs +++ b/crates/goose/src/providers/gcpvertexai.rs @@ -165,7 +165,10 @@ impl GcpVertexAIProvider { /// /// # Arguments /// * `model` - Configuration for the model to be used - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let project_id = config.get_param("GCP_PROJECT_ID")?; let location = Self::determine_location(config)?; @@ -580,8 +583,9 @@ impl ProviderDef for GcpVertexAIProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 0a2339b77..30b51fa8a 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -47,7 +47,10 @@ pub struct GeminiCliProvider { } impl GeminiCliProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let config = Config::global(); let command: String = config.get_gemini_cli_command().unwrap_or_default().into(); let resolved_command = SearchPaths::builder().with_npm().resolve(&command)?; @@ -174,8 +177,9 @@ impl ProviderDef for GeminiCliProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/gemini_oauth.rs b/crates/goose/src/providers/gemini_oauth.rs index 00ba4651e..f7af107fc 100644 --- a/crates/goose/src/providers/gemini_oauth.rs +++ b/crates/goose/src/providers/gemini_oauth.rs @@ -837,7 +837,10 @@ pub struct GeminiOAuthProvider { } impl GeminiOAuthProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let model = crate::model_config::with_configured_fast_model( model, GEMINI_OAUTH_PROVIDER_NAME, @@ -956,8 +959,9 @@ impl ProviderDef for GeminiOAuthProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index f6d99a0ff..b8c65821e 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -202,6 +202,8 @@ pub struct GithubCopilotProvider { client_id: String, #[serde(skip)] name: String, + #[serde(skip)] + tls_config: Option, } impl GithubCopilotProvider { @@ -229,7 +231,10 @@ impl GithubCopilotProvider { }) } - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = Config::global(); let host = normalize_host( &config @@ -254,6 +259,7 @@ impl GithubCopilotProvider { urls, client_id, name: GITHUB_COPILOT_PROVIDER_NAME.to_string(), + tls_config, }) } @@ -273,7 +279,8 @@ impl GithubCopilotProvider { } let initiator = if is_user_initiated { "user" } else { "agent" }; headers.insert("X-Initiator", initiator.parse().unwrap()); - let api_client = ApiClient::new(endpoint.clone(), auth)?.with_headers(headers)?; + let api_client = ApiClient::new_with_tls(endpoint.clone(), auth, self.tls_config.clone())? + .with_headers(headers)?; api_client .response_post(session_id, path, payload) @@ -546,8 +553,9 @@ impl ProviderDef for GithubCopilotProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index e158c1355..251c3d240 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -66,7 +66,10 @@ pub struct GoogleProvider { } impl GoogleProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let model = crate::model_config::with_configured_fast_model( model, GOOGLE_PROVIDER_NAME, @@ -84,8 +87,8 @@ impl GoogleProvider { key: api_key, }; - let api_client = - ApiClient::new(host, auth)?.with_header("Content-Type", "application/json")?; + let api_client = ApiClient::new_with_tls(host, auth, tls_config)? + .with_header("Content-Type", "application/json")?; Ok(Self { api_client, @@ -136,8 +139,9 @@ impl ProviderDef for GoogleProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/huggingface.rs b/crates/goose/src/providers/huggingface.rs index 90e3974fb..c72ac3def 100644 --- a/crates/goose/src/providers/huggingface.rs +++ b/crates/goose/src/providers/huggingface.rs @@ -74,6 +74,7 @@ impl HuggingFaceProvider { pub fn from_custom_config( model: ModelConfig, config: DeclarativeProviderConfig, + tls_config: Option, ) -> Result { let custom_models = static_model_names(&config); if config.dynamic_models == Some(false) && custom_models.is_none() { @@ -91,10 +92,11 @@ impl HuggingFaceProvider { let timeout_secs = config .timeout_seconds .unwrap_or(DEFAULT_PROVIDER_TIMEOUT_SECS); - let mut api_client = ApiClient::with_timeout( + let mut api_client = ApiClient::with_timeout_and_tls( host, auth_method, std::time::Duration::from_secs(timeout_secs), + tls_config, )? .with_query(query_params); @@ -206,6 +208,7 @@ impl ProviderDef for HuggingFaceProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = Config::global(); @@ -214,7 +217,7 @@ impl ProviderDef for HuggingFaceProvider { let host: String = config .get_param("HF_HOST") .unwrap_or_else(|_| HUGGINGFACE_API_HOST.to_string()); - let api_client = ApiClient::new(host, auth_method)?; + let api_client = ApiClient::new_with_tls(host, auth_method, tls_config)?; Ok(Self { inner: OpenAiCompatibleProvider::new( @@ -442,9 +445,12 @@ mod tests { ModelInfo::new("static-b".to_string(), 128000), ]; - let provider = - HuggingFaceProvider::from_custom_config(ModelConfig::new("static-a").unwrap(), config) - .unwrap(); + let provider = HuggingFaceProvider::from_custom_config( + ModelConfig::new("static-a").unwrap(), + config, + None, + ) + .unwrap(); assert_eq!( provider.fetch_supported_models().await.unwrap(), @@ -461,6 +467,7 @@ mod tests { let error = match HuggingFaceProvider::from_custom_config( ModelConfig::new("model").unwrap(), config, + None, ) { Ok(_) => panic!("expected dynamic_models: false without static models to fail"), Err(error) => error, diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index 90afd09fe..48935e790 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -54,7 +54,10 @@ use tokio::sync::OnceCell; static REGISTRY: OnceCell> = OnceCell::const_new(); async fn init_registry() -> RwLock { - let mut registry = ProviderRegistry::new().with_providers(|registry| { + let tls_config = + crate::config::tls::provider_tls_config_from_config(crate::config::Config::global()) + .expect("failed to load provider TLS config"); + let mut registry = ProviderRegistry::new(tls_config).with_providers(|registry| { use super::inventory::registrations; registry.register_with_inventory::( diff --git a/crates/goose/src/providers/kimicode.rs b/crates/goose/src/providers/kimicode.rs index db9831c2b..92352c7f2 100644 --- a/crates/goose/src/providers/kimicode.rs +++ b/crates/goose/src/providers/kimicode.rs @@ -162,7 +162,10 @@ impl KimiCodeProvider { TokenCache::new().clear().await } - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let model = crate::model_config::with_configured_fast_model( model, KIMI_CODE_PROVIDER_NAME, @@ -372,8 +375,9 @@ impl ProviderDef for KimiCodeProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/litellm.rs b/crates/goose/src/providers/litellm.rs index c88d766f7..b0aeff714 100644 --- a/crates/goose/src/providers/litellm.rs +++ b/crates/goose/src/providers/litellm.rs @@ -37,7 +37,10 @@ pub struct LiteLLMProvider { } impl LiteLLMProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let secrets = config .get_secrets("LITELLM_API_KEY", &["LITELLM_CUSTOM_HEADERS"]) @@ -63,8 +66,12 @@ impl LiteLLMProvider { AuthMethod::BearerToken(api_key) }; - let mut api_client = - ApiClient::with_timeout(host, auth, std::time::Duration::from_secs(timeout_secs))?; + let mut api_client = ApiClient::with_timeout_and_tls( + host, + auth, + std::time::Duration::from_secs(timeout_secs), + tls_config, + )?; if let Some(headers) = custom_headers { let mut header_map = reqwest::header::HeaderMap::new(); @@ -184,8 +191,9 @@ impl ProviderDef for LiteLLMProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index b56647d19..0f821d519 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -532,6 +532,7 @@ impl ProviderDef for LocalInferenceProvider { fn from_env( model: ModelConfig, extensions: Vec, + _tls_config: Option, ) -> BoxFuture<'static, Result> where Self: Sized, diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index 031c77c26..da1eeed45 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -1,7 +1,9 @@ mod acp_tooling; pub mod amp_acp; pub mod anthropic; -pub mod api_client; +pub mod api_client { + pub use goose_providers::api_client::*; +} pub mod avian; pub mod azure; pub mod azureauth; diff --git a/crates/goose/src/providers/nanogpt.rs b/crates/goose/src/providers/nanogpt.rs index 4905c5988..eb83cb29c 100644 --- a/crates/goose/src/providers/nanogpt.rs +++ b/crates/goose/src/providers/nanogpt.rs @@ -30,16 +30,24 @@ pub struct NanoGptProvider { } impl NanoGptProvider { - fn build_client(host: &str, api_key: &str) -> Result { - ApiClient::new( + fn build_client( + host: &str, + api_key: &str, + tls_config: Option, + ) -> Result { + ApiClient::new_with_tls( host.to_string(), AuthMethod::BearerToken(api_key.to_string()), + tls_config, )? .with_header("x-client", "goose") } - async fn check_subscription(api_key: &str) -> bool { - let client = match Self::build_client(NANOGPT_SUBSCRIPTION_HOST, api_key) { + async fn check_subscription( + api_key: &str, + tls_config: Option, + ) -> bool { + let client = match Self::build_client(NANOGPT_SUBSCRIPTION_HOST, api_key, tls_config) { Ok(c) => c, Err(_) => return false, }; @@ -55,11 +63,14 @@ impl NanoGptProvider { } } - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let api_key: String = config.get_secret(NANOGPT_API_KEY)?; - let is_subscription = Self::check_subscription(&api_key).await; + let is_subscription = Self::check_subscription(&api_key, tls_config.clone()).await; let host = if is_subscription { tracing::debug!("NanoGPT subscription active, using subscription endpoint"); NANOGPT_SUBSCRIPTION_HOST.to_string() @@ -68,7 +79,7 @@ impl NanoGptProvider { NANOGPT_API_HOST.to_string() }; - let api_client = Self::build_client(&host, &api_key)?; + let api_client = Self::build_client(&host, &api_key, tls_config)?; Ok(Self { api_client, @@ -96,8 +107,9 @@ impl ProviderDef for NanoGptProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 62c3bfc55..7141cb8f8 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -130,7 +130,10 @@ pub(crate) fn ollama_host_configured(config: &crate::config::Config) -> bool { } impl OllamaProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let host: String = config .get_param("OLLAMA_HOST") @@ -158,8 +161,12 @@ impl OllamaProvider { .map_err(|_| anyhow::anyhow!("Failed to set default port"))?; } - let api_client = - ApiClient::with_timeout(base_url.to_string(), AuthMethod::NoAuth, timeout)?; + let api_client = ApiClient::with_timeout_and_tls( + base_url.to_string(), + AuthMethod::NoAuth, + timeout, + tls_config, + )?; Ok(Self { api_client, @@ -173,6 +180,7 @@ impl OllamaProvider { pub fn from_custom_config( model: ModelConfig, config: DeclarativeProviderConfig, + tls_config: Option, ) -> Result { let timeout = Duration::from_secs(config.timeout_seconds.unwrap_or(OLLAMA_TIMEOUT)); @@ -196,8 +204,12 @@ impl OllamaProvider { .map_err(|_| anyhow::anyhow!("Failed to set default port"))?; } - let mut api_client = - ApiClient::with_timeout(base_url.to_string(), AuthMethod::NoAuth, timeout)?; + let mut api_client = ApiClient::with_timeout_and_tls( + base_url.to_string(), + AuthMethod::NoAuth, + timeout, + tls_config, + )?; if let Some(headers) = &config.headers { let mut header_map = reqwest::header::HeaderMap::new(); @@ -261,8 +273,9 @@ impl ProviderDef for OllamaProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index a89179bc6..21eaa6d58 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -155,7 +155,10 @@ pub struct OpenAiProvider { } impl OpenAiProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); // Resolve host and base_path. @@ -269,10 +272,11 @@ impl OpenAiProvider { Some(key) if !key.is_empty() => AuthMethod::BearerToken(key), _ => AuthMethod::NoAuth, }; - let mut api_client = ApiClient::with_timeout( + let mut api_client = ApiClient::with_timeout_and_tls( parsed.host, auth, std::time::Duration::from_secs(timeout_secs), + tls_config, )?; if !parsed.query_params.is_empty() { @@ -404,6 +408,7 @@ impl OpenAiProvider { pub fn from_custom_config( model: ModelConfig, config: DeclarativeProviderConfig, + tls_config: Option, ) -> Result { let custom_models = if !config.models.is_empty() { Some( @@ -456,8 +461,12 @@ impl OpenAiProvider { Some(key) if !key.is_empty() => AuthMethod::BearerToken(key), _ => AuthMethod::NoAuth, }; - let mut api_client = - ApiClient::with_timeout(host, auth, std::time::Duration::from_secs(timeout_secs))?; + let mut api_client = ApiClient::with_timeout_and_tls( + host, + auth, + std::time::Duration::from_secs(timeout_secs), + tls_config, + )?; // Add custom headers if present if let Some(headers) = &config.headers { @@ -770,8 +779,9 @@ impl ProviderDef for OpenAiProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } @@ -944,7 +954,12 @@ mod tests { fn make_provider(name: &str) -> OpenAiProvider { OpenAiProvider { - api_client: ApiClient::new("http://localhost".to_string(), AuthMethod::NoAuth).unwrap(), + api_client: ApiClient::new_with_tls( + "http://localhost".to_string(), + AuthMethod::NoAuth, + None, + ) + .unwrap(), base_path: "v1/chat/completions".to_string(), organization: None, project: None, @@ -1276,7 +1291,8 @@ mod tests { dynamic_models: Option, ) -> OpenAiProvider { OpenAiProvider { - api_client: ApiClient::new(server_uri.to_string(), AuthMethod::NoAuth).unwrap(), + api_client: ApiClient::new_with_tls(server_uri.to_string(), AuthMethod::NoAuth, None) + .unwrap(), base_path: "v1/chat/completions".to_string(), organization: None, project: None, @@ -1358,7 +1374,8 @@ mod tests { config.base_url = "localhost:1234".to_string(); let provider = - OpenAiProvider::from_custom_config(ModelConfig::new_or_fail("m1"), config).unwrap(); + OpenAiProvider::from_custom_config(ModelConfig::new_or_fail("m1"), config, None) + .unwrap(); assert_eq!(provider.api_client.host(), "http://localhost:1234"); assert_eq!(provider.base_path, "v1/chat/completions"); @@ -1386,11 +1403,12 @@ mod tests { #[test] fn from_custom_config_rejects_static_only_without_models() { let config = base_declarative_config(vec![], Some(false)); - let err = - OpenAiProvider::from_custom_config(ModelConfig::new_or_fail("test-model"), config) - .expect_err( - "expected construction error for dynamic_models: false with empty models", - ); + let err = OpenAiProvider::from_custom_config( + ModelConfig::new_or_fail("test-model"), + config, + None, + ) + .expect_err("expected construction error for dynamic_models: false with empty models"); let msg = err.to_string(); assert!( msg.contains("dynamic_models: false"), diff --git a/crates/goose/src/providers/openai_compatible.rs b/crates/goose/src/providers/openai_compatible.rs index dab5b6a2f..e62ba30b0 100644 --- a/crates/goose/src/providers/openai_compatible.rs +++ b/crates/goose/src/providers/openai_compatible.rs @@ -306,9 +306,10 @@ mod tests { fn build_request_respects_non_streaming_mode() { let provider = OpenAiCompatibleProvider::new( "test".to_string(), - ApiClient::new( + ApiClient::new_with_tls( "http://localhost".to_string(), super::super::api_client::AuthMethod::NoAuth, + None, ) .unwrap(), ModelConfig::new_or_fail("test-model"), diff --git a/crates/goose/src/providers/openrouter.rs b/crates/goose/src/providers/openrouter.rs index 6b3cf5d3b..695d77a20 100644 --- a/crates/goose/src/providers/openrouter.rs +++ b/crates/goose/src/providers/openrouter.rs @@ -47,7 +47,10 @@ pub struct OpenRouterProvider { } impl OpenRouterProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let model = crate::model_config::with_configured_fast_model( model, OPENROUTER_PROVIDER_NAME, @@ -61,7 +64,7 @@ impl OpenRouterProvider { .unwrap_or_else(|_| "https://openrouter.ai".to_string()); let auth = AuthMethod::BearerToken(api_key); - let api_client = ApiClient::new(host, auth)? + let api_client = ApiClient::new_with_tls(host, auth, tls_config)? .with_header("HTTP-Referer", "https://goose-docs.ai")? .with_header("X-Title", "goose")?; @@ -183,8 +186,9 @@ impl ProviderDef for OpenRouterProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/pi_acp.rs b/crates/goose/src/providers/pi_acp.rs index 85d1afe87..dcb292db3 100644 --- a/crates/goose/src/providers/pi_acp.rs +++ b/crates/goose/src/providers/pi_acp.rs @@ -42,14 +42,16 @@ impl ProviderDef for PiAcpProvider { fn from_env( model: ModelConfig, extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Self::from_env_with_working_dir(model, extensions, current_working_dir()) + Self::from_env_with_working_dir(model, extensions, current_working_dir(), tls_config) } fn from_env_with_working_dir( model: ModelConfig, extensions: Vec, working_dir: PathBuf, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = Config::global(); diff --git a/crates/goose/src/providers/provider_registry.rs b/crates/goose/src/providers/provider_registry.rs index 974a7c5fd..dc313a188 100644 --- a/crates/goose/src/providers/provider_registry.rs +++ b/crates/goose/src/providers/provider_registry.rs @@ -1,3 +1,4 @@ +use super::api_client::TlsConfig; use super::base::{ConfigKey, ModelInfo, Provider, ProviderDef, ProviderMetadata, ProviderType}; use super::inventory::{InventoryIdentityInput, InventoryRegistration, InventoryResolvers}; use crate::config::{DeclarativeProviderConfig, ExtensionConfig}; @@ -13,6 +14,7 @@ pub type ProviderConstructor = Arc< ModelConfig, Vec, Option, + Option, ) -> BoxFuture<'static, Result>> + Send + Sync, @@ -29,6 +31,7 @@ pub struct ProviderEntry { pub(crate) cleanup: Option, provider_type: ProviderType, supports_inventory_refresh: bool, + tls_config: Option, } impl ProviderEntry { @@ -78,7 +81,7 @@ impl ProviderEntry { &self.metadata.default_model, )?; let model_config = self.normalize_model_config(model_config)?; - (self.constructor)(model_config, extensions, None).await + (self.constructor)(model_config, extensions, None, self.tls_config.clone()).await } pub async fn create( @@ -87,7 +90,7 @@ impl ProviderEntry { extensions: Vec, ) -> Result> { let model = self.normalize_model_config(model)?; - (self.constructor)(model, extensions, None).await + (self.constructor)(model, extensions, None, self.tls_config.clone()).await } pub async fn create_with_working_dir( @@ -97,19 +100,27 @@ impl ProviderEntry { working_dir: PathBuf, ) -> Result> { let model = self.normalize_model_config(model)?; - (self.constructor)(model, extensions, Some(working_dir)).await + (self.constructor)( + model, + extensions, + Some(working_dir), + self.tls_config.clone(), + ) + .await } } #[derive(Default)] pub struct ProviderRegistry { pub(crate) entries: HashMap, + tls_config: Option, } impl ProviderRegistry { - pub fn new() -> Self { + pub fn new(tls_config: Option) -> Self { Self { entries: HashMap::new(), + tls_config, } } @@ -136,13 +147,19 @@ impl ProviderRegistry { name, ProviderEntry { metadata, - constructor: Arc::new(|model, extensions, working_dir| { + constructor: Arc::new(|model, extensions, working_dir, tls_config| { Box::pin(async move { let provider = match working_dir { Some(working_dir) => { - F::from_env_with_working_dir(model, extensions, working_dir).await? + F::from_env_with_working_dir( + model, + extensions, + working_dir, + tls_config, + ) + .await? } - None => F::from_env(model, extensions).await?, + None => F::from_env(model, extensions, tls_config).await?, }; Ok(Arc::new(provider) as Arc) }) @@ -156,6 +173,7 @@ impl ProviderRegistry { ProviderType::Builtin }, supports_inventory_refresh: inventory.supports_refresh, + tls_config: self.tls_config.clone(), }, ); } @@ -169,7 +187,7 @@ impl ProviderRegistry { inventory_identity: G, ) where P: ProviderDef + 'static, - F: Fn(ModelConfig) -> Result + Send + Sync + 'static, + F: Fn(ModelConfig, Option) -> Result + Send + Sync + 'static, G: Fn() -> Result + Send + Sync + 'static, { self.register_with_name_impl::( @@ -192,7 +210,7 @@ impl ProviderRegistry { inventory_configured: H, ) where P: ProviderDef + 'static, - F: Fn(ModelConfig) -> Result + Send + Sync + 'static, + F: Fn(ModelConfig, Option) -> Result + Send + Sync + 'static, G: Fn() -> Result + Send + Sync + 'static, H: Fn() -> bool + Send + Sync + 'static, { @@ -216,7 +234,7 @@ impl ProviderRegistry { inventory_configured: Option, ) where P: ProviderDef + 'static, - F: Fn(ModelConfig) -> Result + Send + Sync + 'static, + F: Fn(ModelConfig, Option) -> Result + Send + Sync + 'static, G: Fn() -> Result + Send + Sync + 'static, { let base_metadata = P::metadata(); @@ -311,8 +329,8 @@ impl ProviderRegistry { config.name.clone(), ProviderEntry { metadata: custom_metadata, - constructor: Arc::new(move |model, _extensions, _working_dir| { - let result = constructor(model); + constructor: Arc::new(move |model, _extensions, _working_dir, tls_config| { + let result = constructor(model, tls_config); Box::pin(async move { let provider = result?; Ok(Arc::new(provider) as Arc) @@ -323,6 +341,7 @@ impl ProviderRegistry { cleanup: None, provider_type, supports_inventory_refresh, + tls_config: self.tls_config.clone(), }, ); } @@ -400,12 +419,12 @@ mod tests { #[test] fn register_with_name_can_override_inventory_configured() { - let mut registry = ProviderRegistry::new(); + let mut registry = ProviderRegistry::new(None); registry.register_with_name_and_inventory_configured::( &test_config(), ProviderType::Declarative, false, - |_| unreachable!("constructor is not used by this test"), + |_, _| unreachable!("constructor is not used by this test"), || Ok(InventoryIdentityInput::new("custom_hf", "huggingface")), || false, ); diff --git a/crates/goose/src/providers/sagemaker_tgi.rs b/crates/goose/src/providers/sagemaker_tgi.rs index 6164e4f5f..9aaa6face 100644 --- a/crates/goose/src/providers/sagemaker_tgi.rs +++ b/crates/goose/src/providers/sagemaker_tgi.rs @@ -40,7 +40,10 @@ pub struct SageMakerTgiProvider { } impl SageMakerTgiProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + _tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); // Get SageMaker endpoint name (just the name, not full URL) @@ -297,8 +300,9 @@ impl ProviderDef for SageMakerTgiProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/snowflake.rs b/crates/goose/src/providers/snowflake.rs index cb6c70616..4b7923f17 100644 --- a/crates/goose/src/providers/snowflake.rs +++ b/crates/goose/src/providers/snowflake.rs @@ -59,7 +59,10 @@ pub struct SnowflakeProvider { } impl SnowflakeProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let mut host: Result = config.get_param("SNOWFLAKE_HOST"); if host.is_err() { @@ -103,7 +106,8 @@ impl SnowflakeProvider { }; let auth = AuthMethod::BearerToken(token?); - let api_client = ApiClient::new(base_url, auth)?.with_header("User-Agent", "goose")?; + let api_client = ApiClient::new_with_tls(base_url, auth, tls_config)? + .with_header("User-Agent", "goose")?; Ok(Self { api_client, @@ -320,8 +324,9 @@ impl ProviderDef for SnowflakeProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/testprovider.rs b/crates/goose/src/providers/testprovider.rs index 84f71e93a..78ba36789 100644 --- a/crates/goose/src/providers/testprovider.rs +++ b/crates/goose/src/providers/testprovider.rs @@ -156,6 +156,7 @@ impl ProviderDef for TestProvider { fn from_env( _model: ModelConfig, _extensions: Vec, + _tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async { Err(anyhow!("TestProvider must be constructed explicitly")) }) } diff --git a/crates/goose/src/providers/tetrate.rs b/crates/goose/src/providers/tetrate.rs index 1eb4f3140..239343030 100644 --- a/crates/goose/src/providers/tetrate.rs +++ b/crates/goose/src/providers/tetrate.rs @@ -47,7 +47,10 @@ pub struct TetrateProvider { } impl TetrateProvider { - pub async fn from_env(model: ModelConfig) -> Result { + pub async fn from_env( + model: ModelConfig, + tls_config: Option, + ) -> Result { let config = crate::config::Config::global(); let api_key: String = config.get_secret("TETRATE_API_KEY")?; let host: String = config @@ -55,7 +58,7 @@ impl TetrateProvider { .unwrap_or_else(|_| "https://api.router.tetrate.ai".to_string()); let auth = AuthMethod::BearerToken(api_key); - let api_client = ApiClient::new(host, auth)? + let api_client = ApiClient::new_with_tls(host, auth, tls_config)? .with_header("HTTP-Referer", "https://goose-docs.ai")? .with_header("X-Title", "goose")?; @@ -116,8 +119,9 @@ impl ProviderDef for TetrateProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { - Box::pin(Self::from_env(model)) + Box::pin(Self::from_env(model, tls_config)) } } diff --git a/crates/goose/src/providers/xai.rs b/crates/goose/src/providers/xai.rs index e6b9d529c..e0afd1ab9 100644 --- a/crates/goose/src/providers/xai.rs +++ b/crates/goose/src/providers/xai.rs @@ -54,6 +54,7 @@ impl ProviderDef for XaiProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = crate::config::Config::global(); @@ -62,7 +63,8 @@ impl ProviderDef for XaiProvider { .get_param("XAI_HOST") .unwrap_or_else(|_| XAI_API_HOST.to_string()); - let api_client = ApiClient::new(host, AuthMethod::BearerToken(api_key))?; + let api_client = + ApiClient::new_with_tls(host, AuthMethod::BearerToken(api_key), tls_config)?; Ok(OpenAiCompatibleProvider::new( XAI_PROVIDER_NAME.to_string(), diff --git a/crates/goose/src/providers/xai_oauth.rs b/crates/goose/src/providers/xai_oauth.rs index d6f83ba15..5887d11eb 100644 --- a/crates/goose/src/providers/xai_oauth.rs +++ b/crates/goose/src/providers/xai_oauth.rs @@ -781,6 +781,7 @@ impl ProviderDef for XaiOAuthProvider { fn from_env( model: ModelConfig, _extensions: Vec, + tls_config: Option, ) -> BoxFuture<'static, Result> { Box::pin(async move { let config = crate::config::Config::global(); @@ -790,9 +791,10 @@ impl ProviderDef for XaiOAuthProvider { let auth_provider = Arc::new(XaiOAuthAuthProvider::new(XaiAuthState::instance())); let auth_for_client = Arc::clone(&auth_provider); - let api_client = ApiClient::new( + let api_client = ApiClient::new_with_tls( host, AuthMethod::Custom(Box::new(SharedAuthProvider(auth_for_client))), + tls_config, )?; let inner = OpenAiCompatibleProvider::new( diff --git a/crates/goose/tests/acp_fixtures/mod.rs b/crates/goose/tests/acp_fixtures/mod.rs index fd5677da7..d300cfc17 100644 --- a/crates/goose/tests/acp_fixtures/mod.rs +++ b/crates/goose/tests/acp_fixtures/mod.rs @@ -199,9 +199,10 @@ pub async fn spawn_acp_server_in_process( move |_provider_name, model_config, _extensions, _working_dir| { let base_url = base_url.clone(); Box::pin(async move { - let api_client = ApiClient::new( + let api_client = ApiClient::new_with_tls( base_url, ApiAuthMethod::BearerToken("test-key".to_string()), + None, ) .unwrap(); let provider: Arc = diff --git a/crates/goose/tests/agent.rs b/crates/goose/tests/agent.rs index f509f32ae..25e5c196d 100644 --- a/crates/goose/tests/agent.rs +++ b/crates/goose/tests/agent.rs @@ -381,6 +381,7 @@ mod tests { fn from_env( _model: ModelConfig, _extensions: Vec, + _tls_config: Option, ) -> futures::future::BoxFuture<'static, anyhow::Result> { Box::pin(async { Ok(Self::new()) }) } @@ -551,6 +552,7 @@ mod tests { fn from_env( _model: ModelConfig, _extensions: Vec, + _tls_config: Option, ) -> futures::future::BoxFuture<'static, anyhow::Result> { Box::pin(async { Ok(Self::new()) }) } @@ -903,6 +905,7 @@ mod tests { fn from_env( _model: ModelConfig, _extensions: Vec, + _tls_config: Option, ) -> futures::future::BoxFuture<'static, anyhow::Result> { unimplemented!() } @@ -1173,6 +1176,7 @@ mod tests { fn from_env( _model: ModelConfig, _extensions: Vec, + _tls_config: Option, ) -> futures::future::BoxFuture<'static, anyhow::Result> { Box::pin(async { Ok(Self::new()) }) } diff --git a/crates/goose/tests/compaction.rs b/crates/goose/tests/compaction.rs index 5f9eec80e..ad9dd39b1 100644 --- a/crates/goose/tests/compaction.rs +++ b/crates/goose/tests/compaction.rs @@ -199,6 +199,7 @@ impl ProviderDef for MockCompactionProvider { fn from_env( _model: ModelConfig, _extensions: Vec, + _tls_config: Option, ) -> futures::future::BoxFuture<'static, anyhow::Result> { Box::pin(async { Ok(Self::new()) }) } diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs index 859e11362..77ff78625 100644 --- a/crates/goose/tests/mcp_integration_test.rs +++ b/crates/goose/tests/mcp_integration_test.rs @@ -61,6 +61,7 @@ impl ProviderDef for MockProvider { fn from_env( model: ModelConfig, _extensions: Vec, + _tls_config: Option, ) -> futures::future::BoxFuture<'static, anyhow::Result> { Box::pin(async move { Ok(Self::new(model)) }) } diff --git a/crates/goose/tests/session_id_propagation_test.rs b/crates/goose/tests/session_id_propagation_test.rs index 74bff73e9..2d4f54e6d 100644 --- a/crates/goose/tests/session_id_propagation_test.rs +++ b/crates/goose/tests/session_id_propagation_test.rs @@ -36,9 +36,10 @@ impl HeaderCapture { } fn create_test_provider(mock_server_url: &str) -> Box { - let api_client = ApiClient::new( + let api_client = ApiClient::new_with_tls( mock_server_url.to_string(), AuthMethod::BearerToken("test-key".to_string()), + None, ) .unwrap(); let model = ModelConfig::new_or_fail("gpt-5-nano"); diff --git a/crates/goose/tests/tetrate_streaming.rs b/crates/goose/tests/tetrate_streaming.rs index dae08d988..111586a41 100644 --- a/crates/goose/tests/tetrate_streaming.rs +++ b/crates/goose/tests/tetrate_streaming.rs @@ -17,7 +17,7 @@ mod tetrate_streaming_tests { // Create a test provider with the default model let model_config = ModelConfig::new("claude-3-5-sonnet-latest")?.with_canonical_limits("tetrate"); - TetrateProvider::from_env(model_config).await + TetrateProvider::from_env(model_config, None).await } #[tokio::test] @@ -248,7 +248,7 @@ mod tetrate_streaming_tests { let model_config = ModelConfig::new("claude-3-5-sonnet-latest")?.with_canonical_limits("tetrate"); - let provider = TetrateProvider::from_env(model_config).await?; + let provider = TetrateProvider::from_env(model_config, None).await?; let messages = vec![Message::user().with_text("Hello")]; let model_config = provider.get_model_config();