Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,9 @@ fn run() -> Result<()> {
Ok(())
}
Some(Commands::Metrics(args)) => run_metrics_command(args),
Some(Commands::Update) => update::run_update(),
Some(Commands::Update) => {
update::run_update(store.config.resolve_insecure_skip_tls_verify())
}
None => {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
let mut forwarded = Vec::new();
Expand Down
25 changes: 13 additions & 12 deletions crates/cli/src/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const LEGACY_UPDATE_VERSION_ENV: &str = "DEEPSEEK_VERSION";
const UPDATE_USER_AGENT: &str = "deepseek-tui-updater";

/// Run the self-update workflow.
pub fn run_update() -> Result<()> {
pub fn run_update(skip_verify: bool) -> Result<()> {
let current_exe =
std::env::current_exe().context("failed to determine current executable path")?;
let targets = update_targets_for_exe(&current_exe);
Expand All @@ -30,7 +30,7 @@ pub fn run_update() -> Result<()> {
println!("Current binary: {}", current_exe.display());

// Step 1: Fetch latest release metadata
let release = fetch_latest_release().with_context(update_network_fallback_hint)?;
let release = fetch_latest_release(skip_verify).with_context(update_network_fallback_hint)?;
let latest_tag = &release.tag_name;
println!("Latest release: {latest_tag}");

Expand All @@ -39,7 +39,7 @@ pub fn run_update() -> Result<()> {
Some(checksum_asset) => {
println!("Downloading {}...", checksum_asset.name);
let checksum_bytes =
download_url(&checksum_asset.browser_download_url).with_context(|| {
download_url(&checksum_asset.browser_download_url, skip_verify).with_context(|| {
format!(
"failed to download {}\n{}",
checksum_asset.name,
Expand Down Expand Up @@ -74,7 +74,7 @@ pub fn run_update() -> Result<()> {
})?;

println!("Downloading {}...", asset.name);
let bytes = download_url(&asset.browser_download_url).with_context(|| {
let bytes = download_url(&asset.browser_download_url, skip_verify).with_context(|| {
format!(
"failed to download {}\n{}",
asset.name,
Expand Down Expand Up @@ -288,15 +288,16 @@ struct Asset {
browser_download_url: String,
}

fn update_http_client() -> Result<reqwest::blocking::Client> {
fn update_http_client(skip_verify: bool) -> Result<reqwest::blocking::Client> {
reqwest::blocking::Client::builder()
.danger_accept_invalid_certs(skip_verify)
.user_agent(UPDATE_USER_AGENT)
.build()
.context("failed to build update HTTP client")
}

/// Fetch the latest release metadata from GitHub.
fn fetch_latest_release() -> Result<Release> {
fn fetch_latest_release(skip_verify: bool) -> Result<Release> {
if let Some(base_url) = release_base_url_from_env() {
let version = update_version_from_env().unwrap_or_else(|| env!("CARGO_PKG_VERSION").into());
return Ok(release_from_mirror_base_url(
Expand All @@ -306,7 +307,7 @@ fn fetch_latest_release() -> Result<Release> {
std::env::consts::ARCH,
));
}
fetch_latest_release_from_url(LATEST_RELEASE_URL)
fetch_latest_release_from_url(LATEST_RELEASE_URL, skip_verify)
}

fn release_base_url_from_env() -> Option<String> {
Expand Down Expand Up @@ -365,8 +366,8 @@ fn update_network_fallback_hint() -> String {
)
}

fn fetch_latest_release_from_url(url: &str) -> Result<Release> {
let client = update_http_client()?;
fn fetch_latest_release_from_url(url: &str, skip_verify: bool) -> Result<Release> {
let client = update_http_client(skip_verify)?;
let response = client
.get(url)
.header(reqwest::header::ACCEPT, "application/vnd.github+json")
Expand All @@ -389,8 +390,8 @@ fn fetch_latest_release_from_url(url: &str) -> Result<Release> {
}

/// Download a URL to bytes.
fn download_url(url: &str) -> Result<Vec<u8>> {
let client = update_http_client()?;
fn download_url(url: &str, skip_verify: bool) -> Result<Vec<u8>> {
let client = update_http_client(skip_verify)?;
let response = client
.get(url)
.send()
Expand Down Expand Up @@ -909,7 +910,7 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *deepseek-wind
fn download_url_reads_binary_body_with_updater_user_agent() {
let (url, request_rx, handle) =
serve_http_once("200 OK", "application/octet-stream", b"\0binary bytes");
let bytes = download_url(&url).expect("binary download should succeed");
let bytes = download_url(&url, false).expect("binary download should succeed");

assert_eq!(bytes, b"\0binary bytes");

Expand Down
38 changes: 37 additions & 1 deletion crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ pub struct ConfigToml {
/// applies the defaults documented in [`LspConfigToml`].
#[serde(default)]
pub lsp: Option<LspConfigToml>,
/// Skip TLS certificate verification for all outbound HTTPS requests.
/// Defaults to `false` (verify certificates). Set to `true` only when
/// connecting to servers with self-signed or untrusted certificates.
#[serde(default)]
pub insecure_skip_tls_verify: Option<bool>,
#[serde(flatten)]
pub extras: BTreeMap<String, toml::Value>,
}
Expand Down Expand Up @@ -943,6 +948,17 @@ impl ConfigToml {
out
}

/// Resolve `insecure_skip_tls_verify` considering the environment variable
/// override first, then the config file value, defaulting to `false`.
#[must_use]
pub fn resolve_insecure_skip_tls_verify(&self) -> bool {
std::env::var("DEEPSEEK_INSECURE_SKIP_TLS_VERIFY")
.ok()
.and_then(|v| parse_bool(&v).ok())
.or(self.insecure_skip_tls_verify)
.unwrap_or(false)
}

/// Resolve runtime options without touching platform credential stores.
///
/// This method keeps library callers prompt-free: CLI flag → config file
Expand Down Expand Up @@ -1463,7 +1479,8 @@ pub fn default_config_path() -> Result<PathBuf> {
Ok(home.join(".deepseek").join(CONFIG_FILE_NAME))
}

fn parse_bool(raw: &str) -> Result<bool> {
/// Parse common boolean string representations.
pub fn parse_bool(raw: &str) -> Result<bool> {
match raw.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" | "enabled" => Ok(true),
"0" | "false" | "no" | "off" | "disabled" => Ok(false),
Expand Down Expand Up @@ -1577,6 +1594,7 @@ struct EnvRuntimeOverrides {
sglang_base_url: Option<String>,
vllm_base_url: Option<String>,
ollama_base_url: Option<String>,
insecure_skip_tls_verify: Option<bool>,
}

impl EnvRuntimeOverrides {
Expand Down Expand Up @@ -1643,6 +1661,9 @@ impl EnvRuntimeOverrides {
ollama_base_url: std::env::var("OLLAMA_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
insecure_skip_tls_verify: std::env::var("DEEPSEEK_INSECURE_SKIP_TLS_VERIFY")
.ok()
.and_then(|v| parse_bool(&v).ok()),
}
}

Expand Down Expand Up @@ -1731,6 +1752,7 @@ mod tests {
vllm_base_url: Option<OsString>,
ollama_api_key: Option<OsString>,
ollama_base_url: Option<OsString>,
deepseek_insecure_skip_tls_verify: Option<OsString>,
}

impl EnvGuard {
Expand Down Expand Up @@ -1766,6 +1788,7 @@ mod tests {
vllm_base_url: env::var_os("VLLM_BASE_URL"),
ollama_api_key: env::var_os("OLLAMA_API_KEY"),
ollama_base_url: env::var_os("OLLAMA_BASE_URL"),
deepseek_insecure_skip_tls_verify: env::var_os("DEEPSEEK_INSECURE_SKIP_TLS_VERIFY"),
};
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
Expand Down Expand Up @@ -1799,6 +1822,7 @@ mod tests {
env::remove_var("VLLM_BASE_URL");
env::remove_var("OLLAMA_API_KEY");
env::remove_var("OLLAMA_BASE_URL");
env::remove_var("DEEPSEEK_INSECURE_SKIP_TLS_VERIFY");
}
guard
}
Expand Down Expand Up @@ -1846,6 +1870,7 @@ mod tests {
Self::restore_var("VLLM_BASE_URL", self.vllm_base_url.take());
Self::restore_var("OLLAMA_API_KEY", self.ollama_api_key.take());
Self::restore_var("OLLAMA_BASE_URL", self.ollama_base_url.take());
Self::restore_var("DEEPSEEK_INSECURE_SKIP_TLS_VERIFY", self.deepseek_insecure_skip_tls_verify.take());
}
}
}
Expand Down Expand Up @@ -2800,4 +2825,15 @@ mod tests {
assert_eq!(resolved.api_key.as_deref(), Some("cli-key"));
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Cli));
}

#[test]
fn insecure_skip_tls_verify_toml_deserializes() {
let config: ConfigToml = toml::from_str(
r#"
insecure_skip_tls_verify = true
"#,
)
.expect("config toml");
assert_eq!(config.insecure_skip_tls_verify, Some(true));
}
}
7 changes: 5 additions & 2 deletions crates/hooks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ pub struct WebhookHookSink {
}

impl WebhookHookSink {
pub fn new(url: String) -> Self {
pub fn new(url: String, skip_verify: bool) -> Self {
Self {
url,
client: reqwest::Client::new(),
client: reqwest::Client::builder()
.danger_accept_invalid_certs(skip_verify)
.build()
.unwrap_or_else(|_| reqwest::Client::new()),
}
}
}
Expand Down
25 changes: 24 additions & 1 deletion crates/tui/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,8 @@ impl DeepSeekClient {
retry.enabled, retry.max_retries, retry.initial_delay, retry.max_delay
));

let http_client = Self::build_http_client(&api_key, &http_headers)?;
let skip_verify = config.insecure_skip_tls_verify.unwrap_or(false);
let http_client = Self::build_http_client(&api_key, &http_headers, skip_verify)?;

Ok(Self {
http_client,
Expand All @@ -509,9 +510,11 @@ impl DeepSeekClient {
fn build_http_client(
api_key: &str,
extra_headers: &HashMap<String, String>,
skip_verify: bool,
) -> Result<reqwest::Client> {
let headers = build_default_headers(api_key, extra_headers)?;
let mut builder = reqwest::Client::builder()
.danger_accept_invalid_certs(skip_verify)
.default_headers(headers)
.user_agent(concat!(
"Mozilla/5.0 (compatible; deepseek-tui/",
Expand Down Expand Up @@ -2925,4 +2928,24 @@ mod tests {
);
}
}

#[test]
fn build_http_client_succeeds_with_skip_verify_true() {
let client = DeepSeekClient::build_http_client(
"test-key",
&HashMap::new(),
true,
);
assert!(client.is_ok());
}

#[test]
fn build_http_client_succeeds_with_skip_verify_false() {
let client = DeepSeekClient::build_http_client(
"test-key",
&HashMap::new(),
false,
);
assert!(client.is_ok());
}
}
20 changes: 11 additions & 9 deletions crates/tui/src/commands/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ fn install_skill(app: &mut App, spec: &str) -> CommandResult {
Err(err) => return CommandResult::error(format!("Invalid install source: {err}")),
};
let skills_dir = app.skills_dir.clone();
let (network, max_size, registry_url) = installer_settings(app);
let (network, max_size, registry_url, skip_verify) = installer_settings(app);

let outcome = run_async(async move {
install::install_with_registry(
Expand All @@ -279,6 +279,7 @@ fn install_skill(app: &mut App, spec: &str) -> CommandResult {
&network,
false,
&registry_url,
skip_verify,
)
.await
});
Expand Down Expand Up @@ -309,10 +310,10 @@ fn update_skill(app: &mut App, name: &str) -> CommandResult {
return CommandResult::error("Usage: /skill update <name>");
}
let skills_dir = app.skills_dir.clone();
let (network, max_size, registry_url) = installer_settings(app);
let (network, max_size, registry_url, skip_verify) = installer_settings(app);
let owned_name = name.to_string();
let outcome = run_async(async move {
install::update_with_registry(&owned_name, &skills_dir, max_size, &network, &registry_url)
install::update_with_registry(&owned_name, &skills_dir, max_size, &network, &registry_url, skip_verify)
.await
});

Expand Down Expand Up @@ -368,8 +369,8 @@ fn trust_skill(app: &mut App, name: &str) -> CommandResult {

/// List skills available in the configured curated registry.
pub fn list_remote_skills(app: &mut App) -> CommandResult {
let (network, _max_size, registry_url) = installer_settings(app);
let registry = run_async(async move { install::fetch_registry(&network, &registry_url).await });
let (network, _max_size, registry_url, skip_verify) = installer_settings(app);
let registry = run_async(async move { install::fetch_registry(&network, &registry_url, skip_verify).await });
match registry {
Ok(RegistryFetchResult::Loaded(doc)) => {
if doc.skills.is_empty() {
Expand Down Expand Up @@ -406,11 +407,11 @@ pub fn list_remote_skills(app: &mut App) -> CommandResult {
/// For each skill the sync checks the cached ETag / SHA-256 before
/// downloading so unchanged skills are skipped in O(1) network round-trips.
fn sync_skills(app: &mut App) -> CommandResult {
let (network, max_size, registry_url) = installer_settings(app);
let (network, max_size, registry_url, skip_verify) = installer_settings(app);
let cache_dir = install::default_cache_skills_dir();

let result = run_async(async move {
install::sync_registry(&network, &registry_url, &cache_dir, max_size).await
install::sync_registry(&network, &registry_url, &cache_dir, max_size, skip_verify).await
});

match result {
Expand Down Expand Up @@ -473,7 +474,7 @@ fn sync_skills(app: &mut App) -> CommandResult {
/// round-trip the install/update operation will incur next. If the config
/// fails to parse, we fall back to defaults so the user still gets a
/// network-gated install rather than a silent crash.
fn installer_settings(_app: &App) -> (NetworkPolicy, u64, String) {
fn installer_settings(_app: &App) -> (NetworkPolicy, u64, String, bool) {
let cfg = crate::config::Config::load(None, None).unwrap_or_default();
let network = cfg
.network
Expand All @@ -487,7 +488,8 @@ fn installer_settings(_app: &App) -> (NetworkPolicy, u64, String) {
let registry_url = skills_cfg
.and_then(|s| s.registry_url.clone())
.unwrap_or_else(|| DEFAULT_REGISTRY_URL.to_string());
(network, max_size, registry_url)
let skip_verify = cfg.insecure_skip_tls_verify.unwrap_or(false);
(network, max_size, registry_url, skip_verify)
}

fn run_async<F, T>(future: F) -> T
Expand Down
Loading