diff --git a/README.md b/README.md index 334ae74f..65094773 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,8 @@ SGLANG_BASE_URL="http://localhost:30000/v1" codewhale --provider sglang --model # Self-hosted vLLM VLLM_BASE_URL="http://localhost:8000/v1" codewhale --provider vllm --model deepseek-v4-flash +# Trusted LAN vLLM over HTTP +DEEPSEEK_ALLOW_INSECURE_HTTP=1 VLLM_BASE_URL="http://192.168.0.110:8000/v1" codewhale --provider vllm --model deepseek-v4-flash # Self-hosted Ollama ollama pull codewhale-coder:1.3b diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 025a8344..b89825ac 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -2862,6 +2862,10 @@ mod tests { #[test] fn base_url_security_rejects_insecure_non_local_http() { + let _lock = ALLOW_INSECURE_HTTP_ENV_LOCK.lock().unwrap(); + let _guard = AllowInsecureHttpEnvGuard::capture(); + unsafe { std::env::remove_var(ALLOW_INSECURE_HTTP_ENV) }; + let err = validate_base_url_security("http://api.deepseek.com") .expect_err("non-local insecure HTTP should be rejected"); assert!(err.to_string().contains("Refusing insecure base URL")); @@ -2869,10 +2873,46 @@ mod tests { #[test] fn base_url_security_allows_localhost_http() { + let _lock = ALLOW_INSECURE_HTTP_ENV_LOCK.lock().unwrap(); + let _guard = AllowInsecureHttpEnvGuard::capture(); + unsafe { std::env::remove_var(ALLOW_INSECURE_HTTP_ENV) }; + assert!(validate_base_url_security("http://localhost:8080").is_ok()); assert!(validate_base_url_security("http://127.0.0.1:8080").is_ok()); } + #[test] + fn base_url_security_allows_non_local_http_with_explicit_opt_in() { + let _lock = ALLOW_INSECURE_HTTP_ENV_LOCK.lock().unwrap(); + let _guard = AllowInsecureHttpEnvGuard::capture(); + unsafe { std::env::set_var(ALLOW_INSECURE_HTTP_ENV, "1") }; + + assert!(validate_base_url_security("http://192.168.0.110:8000/v1").is_ok()); + } + + /// Serialize tests that mutate `DEEPSEEK_ALLOW_INSECURE_HTTP`; env vars are + /// process-global and would otherwise leak across security checks. + static ALLOW_INSECURE_HTTP_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + struct AllowInsecureHttpEnvGuard { + prior: Option, + } + impl AllowInsecureHttpEnvGuard { + fn capture() -> Self { + Self { + prior: std::env::var_os(ALLOW_INSECURE_HTTP_ENV), + } + } + } + impl Drop for AllowInsecureHttpEnvGuard { + fn drop(&mut self) { + match &self.prior { + Some(v) => unsafe { std::env::set_var(ALLOW_INSECURE_HTTP_ENV, v) }, + None => unsafe { std::env::remove_var(ALLOW_INSECURE_HTTP_ENV) }, + } + } + } + #[test] fn connection_health_degrades_and_recovers() { let now = Instant::now(); diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 63fd1e80..46fb0023 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -6445,6 +6445,35 @@ model = "qwen2.5-coder:7b" Ok(()) } + #[test] + fn vllm_env_resolves_reported_lan_http_endpoint_and_model() -> Result<()> { + let _lock = lock_test_env(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let temp_root = env::temp_dir().join(format!( + "codewhale-tui-vllm-lan-http-test-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root)?; + let _guard = EnvGuard::new(&temp_root); + + // Safety: test-only environment mutation guarded by a global mutex. + unsafe { + env::set_var("DEEPSEEK_PROVIDER", "vllm"); + env::set_var("VLLM_BASE_URL", "http://192.168.0.110:8000/v1"); + env::set_var("DEEPSEEK_MODEL", "deepseek-v4-flash"); + } + + let config = Config::load(None, None)?; + assert_eq!(config.api_provider(), ApiProvider::Vllm); + assert_eq!(config.deepseek_base_url(), "http://192.168.0.110:8000/v1"); + assert_eq!(config.default_model(), "deepseek-v4-flash"); + Ok(()) + } + #[test] fn ollama_env_overrides_base_url_and_model() -> Result<()> { let _lock = lock_test_env();