diff --git a/packages/agent/args.go b/packages/agent/args.go index 6ec93b9..c201b69 100644 --- a/packages/agent/args.go +++ b/packages/agent/args.go @@ -74,6 +74,9 @@ type Args struct { // skill discovery, including built-ins. WithSkills bool + // InsecureTLS skips TLS verification for custom inference endpoints. + InsecureTLS bool + // NoYolo turns on per-tool confirmation. Before each tool // invocation the TUI prompts the user with the tool name + args // and waits for an explicit yes/no. The user can also pick @@ -190,6 +193,8 @@ func ParseArgs(in []string) (Args, error) { case "--with-skills", "--with-skill": // Deprecated no-op: user skills are loaded by default. a.WithSkills = true + case "--insecure": + a.InsecureTLS = true case "--no-yolo": a.NoYolo = true case "--reasoning": @@ -373,6 +378,7 @@ func PrintHelp(version string) { row{"--model ID", "model id (see --list-models)"}, row{"--api-key KEY", "api key for this run (env / auth.json fallback)"}, row{"--base-url URL", "override provider api base url"}, + row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"}, row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"}, row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"}, ) diff --git a/packages/agent/build.go b/packages/agent/build.go index 7b54b42..23d8301 100644 --- a/packages/agent/build.go +++ b/packages/agent/build.go @@ -420,6 +420,11 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { args.BaseURL = "http://localhost:11434" } + provider.InsecureSkipVerify = (args.InsecureTLS || cfg.Insecure) && args.BaseURL != "" + if provider.InsecureSkipVerify { + provider.ApplyInsecureTLS() + } + // If the model has a base URL, credentials are optional (local // models like ollama don't need real API keys). if resolvedModel.BaseURL != "" && credErr != nil { diff --git a/packages/agent/build_test.go b/packages/agent/build_test.go index 2b06a7a..1ce8224 100644 --- a/packages/agent/build_test.go +++ b/packages/agent/build_test.go @@ -211,3 +211,62 @@ func TestCanonicalProviderAliasesAreKnown(t *testing.T) { } } } + +func TestResolveInsecureOnlyWithCustomBaseURL(t *testing.T) { + orig := provider.InsecureSkipVerify + t.Cleanup(func() { provider.InsecureSkipVerify = orig }) + + t.Setenv("ZOT_HOME", t.TempDir()) + t.Setenv("OPENAI_API_KEY", "test-key") + + // no --base-url: must stay false even with --insecure. + provider.InsecureSkipVerify = false + _, err := Resolve(Args{Provider: "openai", InsecureTLS: true}, false) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if provider.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify must not be set without a custom base URL") + } + + // --base-url + --insecure: must be true. + provider.InsecureSkipVerify = false + _, err = Resolve(Args{Provider: "openai", InsecureTLS: true, BaseURL: "https://my-llm.internal/v1"}, false) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if !provider.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify must be set with --insecure and --base-url") + } +} + +func TestResolveInsecureFromConfig(t *testing.T) { + orig := provider.InsecureSkipVerify + t.Cleanup(func() { provider.InsecureSkipVerify = orig }) + + t.Setenv("ZOT_HOME", t.TempDir()) + t.Setenv("OPENAI_API_KEY", "test-key") + if err := SaveConfig(Config{Provider: "openai", Insecure: true}); err != nil { + t.Fatal(err) + } + + // no --base-url: must stay false. + provider.InsecureSkipVerify = false + _, err := Resolve(Args{Provider: "openai"}, false) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if provider.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify must not be set without a custom base URL") + } + + // --base-url: must be true. + provider.InsecureSkipVerify = false + _, err = Resolve(Args{Provider: "openai", BaseURL: "https://my-llm.internal/v1"}, false) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if !provider.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify must be set when config insecure=true and --base-url is provided") + } +} diff --git a/packages/agent/config.go b/packages/agent/config.go index ed6ac9f..a29d9a1 100644 --- a/packages/agent/config.go +++ b/packages/agent/config.go @@ -45,6 +45,9 @@ type Config struct { // which is on; false shows ignored entries. Toggle from /settings. RespectGitignore *bool `json:"respect_gitignore,omitempty"` + // Insecure skips TLS verification for custom inference endpoints. + Insecure bool `json:"insecure,omitempty"` + // LastChangelogShown is the version whose release-notes // dialog the user has already seen. When the running binary's // version differs, the next interactive run shows the diff --git a/packages/provider/httpclient.go b/packages/provider/httpclient.go new file mode 100644 index 0000000..2343fa4 --- /dev/null +++ b/packages/provider/httpclient.go @@ -0,0 +1,16 @@ +package provider + +import ( + "crypto/tls" + "net/http" +) + +// InsecureSkipVerify disables TLS cert verification for inference connections. +var InsecureSkipVerify bool + +// ApplyInsecureTLS replaces http.DefaultTransport to skip TLS cert verification. +func ApplyInsecureTLS() { + http.DefaultTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + } +} diff --git a/packages/provider/httpclient_test.go b/packages/provider/httpclient_test.go new file mode 100644 index 0000000..6635960 --- /dev/null +++ b/packages/provider/httpclient_test.go @@ -0,0 +1,49 @@ +package provider + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestApplyInsecureTLSSetsDefaultTransport(t *testing.T) { + orig := http.DefaultTransport + t.Cleanup(func() { http.DefaultTransport = orig }) + + ApplyInsecureTLS() + + tr, ok := http.DefaultTransport.(*http.Transport) + if !ok { + t.Fatalf("expected *http.Transport, got %T", http.DefaultTransport) + } + if tr.TLSClientConfig == nil || !tr.TLSClientConfig.InsecureSkipVerify { + t.Fatal("expected InsecureSkipVerify=true in TLS config") + } +} + +func TestInsecureClientReachesTLSServer(t *testing.T) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + orig := http.DefaultTransport + t.Cleanup(func() { http.DefaultTransport = orig }) + + client := &http.Client{} + + if _, err := client.Get(srv.URL); err == nil { + t.Fatal("expected TLS error with default transport, got nil") + } + + ApplyInsecureTLS() + + resp, err := client.Get(srv.URL) + if err != nil { + t.Fatalf("request failed after ApplyInsecureTLS: %v", err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status=%d", resp.StatusCode) + } +}