detached 5859d66

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 UnverifiedSHA256:VpvOufjwUa+VgmqX6szPnLnICDNrmQEF+TBJ68rftTk The key is unknown
6 files · +138 -0
M packages/agent/args.go +6 -0
@@ -74,6 +74,9 @@ type Args struct {
7474 // skill discovery, including built-ins.
7575 WithSkills bool
7676
77+ // InsecureTLS skips TLS verification for custom inference endpoints.
78+ InsecureTLS bool
79+
7780 // NoYolo turns on per-tool confirmation. Before each tool
7881 // invocation the TUI prompts the user with the tool name + args
7982 // and waits for an explicit yes/no. The user can also pick
@@ -190,6 +193,8 @@ func ParseArgs(in []string) (Args, error) {
190193 case "--with-skills", "--with-skill":
191194 // Deprecated no-op: user skills are loaded by default.
192195 a.WithSkills = true
196+ case "--insecure":
197+ a.InsecureTLS = true
193198 case "--no-yolo":
194199 a.NoYolo = true
195200 case "--reasoning":
@@ -373,6 +378,7 @@ func PrintHelp(version string) {
373378 row{"--model ID", "model id (see --list-models)"},
374379 row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
375380 row{"--base-url URL", "override provider api base url"},
381+ row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"},
376382 row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"},
377383 row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"},
378384 )
@@ -74,6 +74,9 @@ type Args struct {
74 // skill discovery, including built-ins.74 // skill discovery, including built-ins.
75 WithSkills bool75 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 tool80 // NoYolo turns on per-tool confirmation. Before each tool
78 // invocation the TUI prompts the user with the tool name + args81 // invocation the TUI prompts the user with the tool name + args
79 // and waits for an explicit yes/no. The user can also pick82 // 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 = true195 a.WithSkills = true
196+ case "--insecure":
197+ a.InsecureTLS = true
193 case "--no-yolo":198 case "--no-yolo":
194 a.NoYolo = true199 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) {
420420 args.BaseURL = "http://localhost:11434"
421421 }
422422
423+ provider.InsecureSkipVerify = (args.InsecureTLS || cfg.Insecure) && args.BaseURL != ""
424+ if provider.InsecureSkipVerify {
425+ provider.ApplyInsecureTLS()
426+ }
427+
423428 // If the model has a base URL, credentials are optional (local
424429 // models like ollama don't need real API keys).
425430 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 (local428 // 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) {
211211 }
212212 }
213213 }
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 {
4545 // which is on; false shows ignored entries. Toggle from /settings.
4646 RespectGitignore *bool `json:"respect_gitignore,omitempty"`
4747
48+ // Insecure skips TLS verification for custom inference endpoints.
49+ Insecure bool `json:"insecure,omitempty"`
50+
4851 // LastChangelogShown is the version whose release-notes
4952 // dialog the user has already seen. When the running binary's
5053 // 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-notes51 // LastChangelogShown is the version whose release-notes
49 // dialog the user has already seen. When the running binary's52 // dialog the user has already seen. When the running binary's
50 // version differs, the next interactive run shows the53 // 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close