Add `--insecure` flag to skip TLS verification
- commit
- 5859d66f1a95e2520c85d45e1141989db539ba61
- author
- s3rj1k · · 2026-06-15 22:33 UTC
- committer
- s3rj1k · · 2026-06-15 22:38 UTC
- parent
- 94ece7d
- signature
- SSH Unverified
SHA256:VpvOufjwUa+VgmqX6szPnLnICDNrmQEF+TBJ68rftTkThe key is unknown
6 files · +138 -0
M packages/agent/args.go +6 -0
| @@ -74,6 +74,9 @@ type Args struct { | |||
| 74 | 74 | // skill discovery, including built-ins. | |
| 75 | 75 | WithSkills bool | |
| 76 | 76 | ||
| 77 | + | // InsecureTLS skips TLS verification for custom inference endpoints. | |
| 78 | + | InsecureTLS bool | |
| 79 | + | ||
| 77 | 80 | // NoYolo turns on per-tool confirmation. Before each tool | |
| 78 | 81 | // invocation the TUI prompts the user with the tool name + args | |
| 79 | 82 | // and waits for an explicit yes/no. The user can also pick | |
| @@ -190,6 +193,8 @@ func ParseArgs(in []string) (Args, error) { | |||
| 190 | 193 | case "--with-skills", "--with-skill": | |
| 191 | 194 | // Deprecated no-op: user skills are loaded by default. | |
| 192 | 195 | a.WithSkills = true | |
| 196 | + | case "--insecure": | |
| 197 | + | a.InsecureTLS = true | |
| 193 | 198 | case "--no-yolo": | |
| 194 | 199 | a.NoYolo = true | |
| 195 | 200 | case "--reasoning": | |
| @@ -373,6 +378,7 @@ func PrintHelp(version string) { | |||
| 373 | 378 | row{"--model ID", "model id (see --list-models)"}, | |
| 374 | 379 | row{"--api-key KEY", "api key for this run (env / auth.json fallback)"}, | |
| 375 | 380 | row{"--base-url URL", "override provider api base url"}, | |
| 381 | + | row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"}, | |
| 376 | 382 | row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"}, | |
| 377 | 383 | row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"}, | |
| 378 | 384 | ) | |
| @@ -74,6 +74,9 @@ type Args struct { | |||||
| 74 | // skill discovery, including built-ins. | 74 | // skill discovery, including built-ins. | ||
| 75 | WithSkills bool | 75 | WithSkills bool | ||
| 76 | 76 | ||||
| 77 | + | // InsecureTLS skips TLS verification for custom inference endpoints. | |||
| 78 | + | InsecureTLS bool | |||
| 79 | + | ||||
| 77 | // NoYolo turns on per-tool confirmation. Before each tool | 80 | // NoYolo turns on per-tool confirmation. Before each tool | ||
| 78 | // invocation the TUI prompts the user with the tool name + args | 81 | // invocation the TUI prompts the user with the tool name + args | ||
| 79 | // and waits for an explicit yes/no. The user can also pick | 82 | // and waits for an explicit yes/no. The user can also pick | ||
| @@ -190,6 +193,8 @@ func ParseArgs(in []string) (Args, error) { | |||||
| 190 | case "--with-skills", "--with-skill": | 193 | case "--with-skills", "--with-skill": | ||
| 191 | // Deprecated no-op: user skills are loaded by default. | 194 | // Deprecated no-op: user skills are loaded by default. | ||
| 192 | a.WithSkills = true | 195 | a.WithSkills = true | ||
| 196 | + | case "--insecure": | |||
| 197 | + | a.InsecureTLS = true | |||
| 193 | case "--no-yolo": | 198 | case "--no-yolo": | ||
| 194 | a.NoYolo = true | 199 | a.NoYolo = true | ||
| 195 | case "--reasoning": | 200 | case "--reasoning": | ||
| @@ -373,6 +378,7 @@ func PrintHelp(version string) { | |||||
| 373 | row{"--model ID", "model id (see --list-models)"}, | 378 | row{"--model ID", "model id (see --list-models)"}, | ||
| 374 | row{"--api-key KEY", "api key for this run (env / auth.json fallback)"}, | 379 | row{"--api-key KEY", "api key for this run (env / auth.json fallback)"}, | ||
| 375 | row{"--base-url URL", "override provider api base url"}, | 380 | row{"--base-url URL", "override provider api base url"}, | ||
| 381 | + | row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"}, | |||
| 376 | row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"}, | 382 | row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"}, | ||
| 377 | row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"}, | 383 | row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"}, | ||
| 378 | ) | 384 | ) | ||
M packages/agent/build.go +5 -0
| @@ -420,6 +420,11 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 420 | 420 | args.BaseURL = "http://localhost:11434" | |
| 421 | 421 | } | |
| 422 | 422 | ||
| 423 | + | provider.InsecureSkipVerify = (args.InsecureTLS || cfg.Insecure) && args.BaseURL != "" | |
| 424 | + | if provider.InsecureSkipVerify { | |
| 425 | + | provider.ApplyInsecureTLS() | |
| 426 | + | } | |
| 427 | + | ||
| 423 | 428 | // If the model has a base URL, credentials are optional (local | |
| 424 | 429 | // models like ollama don't need real API keys). | |
| 425 | 430 | if resolvedModel.BaseURL != "" && credErr != nil { | |
| @@ -420,6 +420,11 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 420 | args.BaseURL = "http://localhost:11434" | 420 | args.BaseURL = "http://localhost:11434" | ||
| 421 | } | 421 | } | ||
| 422 | 422 | ||||
| 423 | + | provider.InsecureSkipVerify = (args.InsecureTLS || cfg.Insecure) && args.BaseURL != "" | |||
| 424 | + | if provider.InsecureSkipVerify { | |||
| 425 | + | provider.ApplyInsecureTLS() | |||
| 426 | + | } | |||
| 427 | + | ||||
| 423 | // If the model has a base URL, credentials are optional (local | 428 | // If the model has a base URL, credentials are optional (local | ||
| 424 | // models like ollama don't need real API keys). | 429 | // models like ollama don't need real API keys). | ||
| 425 | if resolvedModel.BaseURL != "" && credErr != nil { | 430 | if resolvedModel.BaseURL != "" && credErr != nil { | ||
M packages/agent/build_test.go +59 -0
| @@ -211,3 +211,62 @@ func TestCanonicalProviderAliasesAreKnown(t *testing.T) { | |||
| 211 | 211 | } | |
| 212 | 212 | } | |
| 213 | 213 | } | |
| 214 | + | ||
| 215 | + | func TestResolveInsecureOnlyWithCustomBaseURL(t *testing.T) { | |
| 216 | + | orig := provider.InsecureSkipVerify | |
| 217 | + | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | |
| 218 | + | ||
| 219 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 220 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |
| 221 | + | ||
| 222 | + | // no --base-url: must stay false even with --insecure. | |
| 223 | + | provider.InsecureSkipVerify = false | |
| 224 | + | _, err := Resolve(Args{Provider: "openai", InsecureTLS: true}, false) | |
| 225 | + | if err != nil { | |
| 226 | + | t.Fatalf("Resolve: %v", err) | |
| 227 | + | } | |
| 228 | + | if provider.InsecureSkipVerify { | |
| 229 | + | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | // --base-url + --insecure: must be true. | |
| 233 | + | provider.InsecureSkipVerify = false | |
| 234 | + | _, err = Resolve(Args{Provider: "openai", InsecureTLS: true, BaseURL: "https://my-llm.internal/v1"}, false) | |
| 235 | + | if err != nil { | |
| 236 | + | t.Fatalf("Resolve: %v", err) | |
| 237 | + | } | |
| 238 | + | if !provider.InsecureSkipVerify { | |
| 239 | + | t.Fatal("InsecureSkipVerify must be set with --insecure and --base-url") | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | func TestResolveInsecureFromConfig(t *testing.T) { | |
| 244 | + | orig := provider.InsecureSkipVerify | |
| 245 | + | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | |
| 246 | + | ||
| 247 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 248 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |
| 249 | + | if err := SaveConfig(Config{Provider: "openai", Insecure: true}); err != nil { | |
| 250 | + | t.Fatal(err) | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // no --base-url: must stay false. | |
| 254 | + | provider.InsecureSkipVerify = false | |
| 255 | + | _, err := Resolve(Args{Provider: "openai"}, false) | |
| 256 | + | if err != nil { | |
| 257 | + | t.Fatalf("Resolve: %v", err) | |
| 258 | + | } | |
| 259 | + | if provider.InsecureSkipVerify { | |
| 260 | + | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | // --base-url: must be true. | |
| 264 | + | provider.InsecureSkipVerify = false | |
| 265 | + | _, err = Resolve(Args{Provider: "openai", BaseURL: "https://my-llm.internal/v1"}, false) | |
| 266 | + | if err != nil { | |
| 267 | + | t.Fatalf("Resolve: %v", err) | |
| 268 | + | } | |
| 269 | + | if !provider.InsecureSkipVerify { | |
| 270 | + | t.Fatal("InsecureSkipVerify must be set when config insecure=true and --base-url is provided") | |
| 271 | + | } | |
| 272 | + | } | |
| @@ -211,3 +211,62 @@ func TestCanonicalProviderAliasesAreKnown(t *testing.T) { | |||||
| 211 | } | 211 | } | ||
| 212 | } | 212 | } | ||
| 213 | } | 213 | } | ||
| 214 | + | ||||
| 215 | + | func TestResolveInsecureOnlyWithCustomBaseURL(t *testing.T) { | |||
| 216 | + | orig := provider.InsecureSkipVerify | |||
| 217 | + | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | |||
| 218 | + | ||||
| 219 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 220 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |||
| 221 | + | ||||
| 222 | + | // no --base-url: must stay false even with --insecure. | |||
| 223 | + | provider.InsecureSkipVerify = false | |||
| 224 | + | _, err := Resolve(Args{Provider: "openai", InsecureTLS: true}, false) | |||
| 225 | + | if err != nil { | |||
| 226 | + | t.Fatalf("Resolve: %v", err) | |||
| 227 | + | } | |||
| 228 | + | if provider.InsecureSkipVerify { | |||
| 229 | + | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | // --base-url + --insecure: must be true. | |||
| 233 | + | provider.InsecureSkipVerify = false | |||
| 234 | + | _, err = Resolve(Args{Provider: "openai", InsecureTLS: true, BaseURL: "https://my-llm.internal/v1"}, false) | |||
| 235 | + | if err != nil { | |||
| 236 | + | t.Fatalf("Resolve: %v", err) | |||
| 237 | + | } | |||
| 238 | + | if !provider.InsecureSkipVerify { | |||
| 239 | + | t.Fatal("InsecureSkipVerify must be set with --insecure and --base-url") | |||
| 240 | + | } | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | func TestResolveInsecureFromConfig(t *testing.T) { | |||
| 244 | + | orig := provider.InsecureSkipVerify | |||
| 245 | + | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | |||
| 246 | + | ||||
| 247 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 248 | + | t.Setenv("OPENAI_API_KEY", "test-key") | |||
| 249 | + | if err := SaveConfig(Config{Provider: "openai", Insecure: true}); err != nil { | |||
| 250 | + | t.Fatal(err) | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | // no --base-url: must stay false. | |||
| 254 | + | provider.InsecureSkipVerify = false | |||
| 255 | + | _, err := Resolve(Args{Provider: "openai"}, false) | |||
| 256 | + | if err != nil { | |||
| 257 | + | t.Fatalf("Resolve: %v", err) | |||
| 258 | + | } | |||
| 259 | + | if provider.InsecureSkipVerify { | |||
| 260 | + | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | // --base-url: must be true. | |||
| 264 | + | provider.InsecureSkipVerify = false | |||
| 265 | + | _, err = Resolve(Args{Provider: "openai", BaseURL: "https://my-llm.internal/v1"}, false) | |||
| 266 | + | if err != nil { | |||
| 267 | + | t.Fatalf("Resolve: %v", err) | |||
| 268 | + | } | |||
| 269 | + | if !provider.InsecureSkipVerify { | |||
| 270 | + | t.Fatal("InsecureSkipVerify must be set when config insecure=true and --base-url is provided") | |||
| 271 | + | } | |||
| 272 | + | } | |||
M packages/agent/config.go +3 -0
| @@ -45,6 +45,9 @@ type Config struct { | |||
| 45 | 45 | // which is on; false shows ignored entries. Toggle from /settings. | |
| 46 | 46 | RespectGitignore *bool `json:"respect_gitignore,omitempty"` | |
| 47 | 47 | ||
| 48 | + | // Insecure skips TLS verification for custom inference endpoints. | |
| 49 | + | Insecure bool `json:"insecure,omitempty"` | |
| 50 | + | ||
| 48 | 51 | // LastChangelogShown is the version whose release-notes | |
| 49 | 52 | // dialog the user has already seen. When the running binary's | |
| 50 | 53 | // version differs, the next interactive run shows the | |
| @@ -45,6 +45,9 @@ type Config struct { | |||||
| 45 | // which is on; false shows ignored entries. Toggle from /settings. | 45 | // which is on; false shows ignored entries. Toggle from /settings. | ||
| 46 | RespectGitignore *bool `json:"respect_gitignore,omitempty"` | 46 | RespectGitignore *bool `json:"respect_gitignore,omitempty"` | ||
| 47 | 47 | ||||
| 48 | + | // Insecure skips TLS verification for custom inference endpoints. | |||
| 49 | + | Insecure bool `json:"insecure,omitempty"` | |||
| 50 | + | ||||
| 48 | // LastChangelogShown is the version whose release-notes | 51 | // LastChangelogShown is the version whose release-notes | ||
| 49 | // dialog the user has already seen. When the running binary's | 52 | // dialog the user has already seen. When the running binary's | ||
| 50 | // version differs, the next interactive run shows the | 53 | // version differs, the next interactive run shows the | ||
A packages/provider/httpclient.go +16 -0
| @@ -0,0 +1,16 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "crypto/tls" | |
| 5 | + | "net/http" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // InsecureSkipVerify disables TLS cert verification for inference connections. | |
| 9 | + | var InsecureSkipVerify bool | |
| 10 | + | ||
| 11 | + | // ApplyInsecureTLS replaces http.DefaultTransport to skip TLS cert verification. | |
| 12 | + | func ApplyInsecureTLS() { | |
| 13 | + | http.DefaultTransport = &http.Transport{ | |
| 14 | + | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec | |
| 15 | + | } | |
| 16 | + | } | |
| @@ -0,0 +1,16 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "crypto/tls" | |||
| 5 | + | "net/http" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // InsecureSkipVerify disables TLS cert verification for inference connections. | |||
| 9 | + | var InsecureSkipVerify bool | |||
| 10 | + | ||||
| 11 | + | // ApplyInsecureTLS replaces http.DefaultTransport to skip TLS cert verification. | |||
| 12 | + | func ApplyInsecureTLS() { | |||
| 13 | + | http.DefaultTransport = &http.Transport{ | |||
| 14 | + | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec | |||
| 15 | + | } | |||
| 16 | + | } | |||
A packages/provider/httpclient_test.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "net/http" | |
| 5 | + | "net/http/httptest" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestApplyInsecureTLSSetsDefaultTransport(t *testing.T) { | |
| 10 | + | orig := http.DefaultTransport | |
| 11 | + | t.Cleanup(func() { http.DefaultTransport = orig }) | |
| 12 | + | ||
| 13 | + | ApplyInsecureTLS() | |
| 14 | + | ||
| 15 | + | tr, ok := http.DefaultTransport.(*http.Transport) | |
| 16 | + | if !ok { | |
| 17 | + | t.Fatalf("expected *http.Transport, got %T", http.DefaultTransport) | |
| 18 | + | } | |
| 19 | + | if tr.TLSClientConfig == nil || !tr.TLSClientConfig.InsecureSkipVerify { | |
| 20 | + | t.Fatal("expected InsecureSkipVerify=true in TLS config") | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func TestInsecureClientReachesTLSServer(t *testing.T) { | |
| 25 | + | srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 26 | + | w.WriteHeader(http.StatusOK) | |
| 27 | + | })) | |
| 28 | + | defer srv.Close() | |
| 29 | + | ||
| 30 | + | orig := http.DefaultTransport | |
| 31 | + | t.Cleanup(func() { http.DefaultTransport = orig }) | |
| 32 | + | ||
| 33 | + | client := &http.Client{} | |
| 34 | + | ||
| 35 | + | if _, err := client.Get(srv.URL); err == nil { | |
| 36 | + | t.Fatal("expected TLS error with default transport, got nil") | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | ApplyInsecureTLS() | |
| 40 | + | ||
| 41 | + | resp, err := client.Get(srv.URL) | |
| 42 | + | if err != nil { | |
| 43 | + | t.Fatalf("request failed after ApplyInsecureTLS: %v", err) | |
| 44 | + | } | |
| 45 | + | resp.Body.Close() | |
| 46 | + | if resp.StatusCode != http.StatusOK { | |
| 47 | + | t.Fatalf("status=%d", resp.StatusCode) | |
| 48 | + | } | |
| 49 | + | } | |
| @@ -0,0 +1,49 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "net/http" | |||
| 5 | + | "net/http/httptest" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestApplyInsecureTLSSetsDefaultTransport(t *testing.T) { | |||
| 10 | + | orig := http.DefaultTransport | |||
| 11 | + | t.Cleanup(func() { http.DefaultTransport = orig }) | |||
| 12 | + | ||||
| 13 | + | ApplyInsecureTLS() | |||
| 14 | + | ||||
| 15 | + | tr, ok := http.DefaultTransport.(*http.Transport) | |||
| 16 | + | if !ok { | |||
| 17 | + | t.Fatalf("expected *http.Transport, got %T", http.DefaultTransport) | |||
| 18 | + | } | |||
| 19 | + | if tr.TLSClientConfig == nil || !tr.TLSClientConfig.InsecureSkipVerify { | |||
| 20 | + | t.Fatal("expected InsecureSkipVerify=true in TLS config") | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func TestInsecureClientReachesTLSServer(t *testing.T) { | |||
| 25 | + | srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 26 | + | w.WriteHeader(http.StatusOK) | |||
| 27 | + | })) | |||
| 28 | + | defer srv.Close() | |||
| 29 | + | ||||
| 30 | + | orig := http.DefaultTransport | |||
| 31 | + | t.Cleanup(func() { http.DefaultTransport = orig }) | |||
| 32 | + | ||||
| 33 | + | client := &http.Client{} | |||
| 34 | + | ||||
| 35 | + | if _, err := client.Get(srv.URL); err == nil { | |||
| 36 | + | t.Fatal("expected TLS error with default transport, got nil") | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | ApplyInsecureTLS() | |||
| 40 | + | ||||
| 41 | + | resp, err := client.Get(srv.URL) | |||
| 42 | + | if err != nil { | |||
| 43 | + | t.Fatalf("request failed after ApplyInsecureTLS: %v", err) | |||
| 44 | + | } | |||
| 45 | + | resp.Body.Close() | |||
| 46 | + | if resp.StatusCode != http.StatusOK { | |||
| 47 | + | t.Fatalf("status=%d", resp.StatusCode) | |||
| 48 | + | } | |||
| 49 | + | } | |||