Scope --insecure TLS to explicit base URL, drop global transport override
Builds on s3rj1k's --insecure flag (#35) but limits insecure TLS to the resolved inference client for an explicit --base-url, instead of mutating http.DefaultTransport process-wide. Built-in providers, auth, and model discovery keep normal certificate verification. Documents the flag in the CLI reference. Co-authored-by: s3rj1k <evasive.gyron@gmail.com>
- commit
- ab7fb37046c36ec7f804520e1cbbda674a1f9969
- author
- patriceckhart · · 2026-06-16 05:41 UTC
- parent
- e0ca3fd
- signature
- unsigned
5 files · +149 -89
M README.md +1 -0
| @@ -141,6 +141,7 @@ zot --help | |||
| 141 | 141 | | `--model <id>` | Pick the model (see `--list-models`). | | |
| 142 | 142 | | `--api-key <key>` | Override the API key. | | |
| 143 | 143 | | `--base-url <url>` | Override the provider base URL (tests, self-hosted). | | |
| 144 | + | | `--insecure` | Skip TLS certificate verification, only for the explicit `--base-url` endpoint (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. | | |
| 144 | 145 | | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). | | |
| 145 | 146 | | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). | | |
| 146 | 147 | | `--reasoning off\|minimum\|low\|medium\|high\|maximum` | Set thinking level on supported models (default: off). | | |
| @@ -141,6 +141,7 @@ zot --help | |||||
| 141 | | `--model <id>` | Pick the model (see `--list-models`). | | 141 | | `--model <id>` | Pick the model (see `--list-models`). | | ||
| 142 | | `--api-key <key>` | Override the API key. | | 142 | | `--api-key <key>` | Override the API key. | | ||
| 143 | | `--base-url <url>` | Override the provider base URL (tests, self-hosted). | | 143 | | `--base-url <url>` | Override the provider base URL (tests, self-hosted). | | ||
| 144 | + | | `--insecure` | Skip TLS certificate verification, only for the explicit `--base-url` endpoint (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. | | |||
| 144 | | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). | | 145 | | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). | | ||
| 145 | | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). | | 146 | | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). | | ||
| 146 | | `--reasoning off\|minimum\|low\|medium\|high\|maximum` | Set thinking level on supported models (default: off). | | 147 | | `--reasoning off\|minimum\|low\|medium\|high\|maximum` | Set thinking level on supported models (default: off). | | ||
M packages/agent/build.go +54 -42
| @@ -23,6 +23,7 @@ type Resolved struct { | |||
| 23 | 23 | AuthMethod string // "apikey" | "oauth" | "" (no credential yet) | |
| 24 | 24 | AccountID string // ChatGPT account id (for openai oauth), "" otherwise | |
| 25 | 25 | BaseURL string | |
| 26 | + | InsecureTLS bool | |
| 26 | 27 | CWD string | |
| 27 | 28 | Reasoning string | |
| 28 | 29 | Temperature *float32 | |
| @@ -409,6 +410,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 409 | 410 | model = fm.ID | |
| 410 | 411 | } | |
| 411 | 412 | ||
| 413 | + | explicitBaseURL := args.BaseURL != "" | |
| 414 | + | ||
| 412 | 415 | // If the model defines a base URL (e.g. local ollama) and the | |
| 413 | 416 | // user didn't pass --base-url, use the model's URL. For ollama, | |
| 414 | 417 | // keep http://localhost:11434 as a fallback only after the model | |
| @@ -420,10 +423,10 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 420 | 423 | args.BaseURL = "http://localhost:11434" | |
| 421 | 424 | } | |
| 422 | 425 | ||
| 423 | - | provider.InsecureSkipVerify = (args.InsecureTLS || cfg.Insecure) && args.BaseURL != "" | |
| 424 | - | if provider.InsecureSkipVerify { | |
| 425 | - | provider.ApplyInsecureTLS() | |
| 426 | - | } | |
| 426 | + | // Insecure TLS is intentionally scoped to explicit custom endpoints. | |
| 427 | + | // Built-in provider base URLs, auth calls, and model discovery keep normal | |
| 428 | + | // certificate verification even when --insecure is present. | |
| 429 | + | insecureTLS := (args.InsecureTLS || cfg.Insecure) && explicitBaseURL | |
| 427 | 430 | ||
| 428 | 431 | // If the model has a base URL, credentials are optional (local | |
| 429 | 432 | // models like ollama don't need real API keys). | |
| @@ -512,6 +515,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 512 | 515 | AuthMethod: method, | |
| 513 | 516 | AccountID: accountID, | |
| 514 | 517 | BaseURL: args.BaseURL, | |
| 518 | + | InsecureTLS: insecureTLS, | |
| 515 | 519 | CWD: args.CWD, | |
| 516 | 520 | Reasoning: reasoning, | |
| 517 | 521 | Temperature: temperature, | |
| @@ -639,90 +643,98 @@ func (r Resolved) NewClient() provider.Client { | |||
| 639 | 643 | if !r.HasCredential() { | |
| 640 | 644 | panic("NewClient called without credential; check HasCredential first") | |
| 641 | 645 | } | |
| 646 | + | wrap := r.withHTTPClient | |
| 642 | 647 | switch r.Provider { | |
| 643 | 648 | case "ollama": | |
| 644 | - | return provider.NewOpenAI(r.Credential, r.BaseURL) | |
| 649 | + | return wrap(provider.NewOpenAI(r.Credential, r.BaseURL)) | |
| 645 | 650 | case "kimi": | |
| 646 | 651 | // kimi-coding speaks anthropic-messages on api.kimi.com/coding. | |
| 647 | 652 | // Subscription OAuth (refreshed) wraps the same Anthropic-shaped client. | |
| 648 | - | inner := provider.NewKimiCodingWithHeaders(r.Credential, r.BaseURL, kimiCodeHeaders()) | |
| 653 | + | inner := wrap(provider.NewKimiCodingWithHeaders(r.Credential, r.BaseURL, kimiCodeHeaders())) | |
| 649 | 654 | if r.AuthMethod == "oauth" { | |
| 650 | 655 | return r.wrapWithRefresh(inner) | |
| 651 | 656 | } | |
| 652 | 657 | return inner | |
| 653 | 658 | case "moonshotai": | |
| 654 | - | return provider.NewMoonshot(r.Credential, r.BaseURL) | |
| 659 | + | return wrap(provider.NewMoonshot(r.Credential, r.BaseURL)) | |
| 655 | 660 | case "moonshotai-cn": | |
| 656 | - | return provider.NewMoonshotCN(r.Credential, r.BaseURL) | |
| 661 | + | return wrap(provider.NewMoonshotCN(r.Credential, r.BaseURL)) | |
| 657 | 662 | case "deepseek": | |
| 658 | - | return provider.NewDeepSeek(r.Credential, r.BaseURL) | |
| 663 | + | return wrap(provider.NewDeepSeek(r.Credential, r.BaseURL)) | |
| 659 | 664 | case "openai": | |
| 660 | - | return provider.NewOpenAI(r.Credential, r.BaseURL) | |
| 665 | + | return wrap(provider.NewOpenAI(r.Credential, r.BaseURL)) | |
| 661 | 666 | case "openai-codex": | |
| 662 | - | inner := provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL) | |
| 667 | + | inner := wrap(provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)) | |
| 663 | 668 | return r.wrapWithRefresh(inner) | |
| 664 | 669 | case "openai-responses": | |
| 665 | 670 | // Public OpenAI Responses API (api.openai.com/v1/responses) via | |
| 666 | 671 | // API key. Separate provider from `openai` (Chat Completions) and | |
| 667 | 672 | // from `openai-codex` (ChatGPT subscription OAuth). | |
| 668 | - | return provider.NewOpenAIResponses(r.Credential, r.BaseURL) | |
| 673 | + | return wrap(provider.NewOpenAIResponses(r.Credential, r.BaseURL)) | |
| 669 | 674 | case "google": | |
| 670 | - | return provider.NewGemini(r.Credential, r.BaseURL) | |
| 675 | + | return wrap(provider.NewGemini(r.Credential, r.BaseURL)) | |
| 671 | 676 | case "cerebras": | |
| 672 | - | return provider.NewCerebras(r.Credential, r.BaseURL) | |
| 677 | + | return wrap(provider.NewCerebras(r.Credential, r.BaseURL)) | |
| 673 | 678 | case "groq": | |
| 674 | - | return provider.NewGroq(r.Credential, r.BaseURL) | |
| 679 | + | return wrap(provider.NewGroq(r.Credential, r.BaseURL)) | |
| 675 | 680 | case "xai": | |
| 676 | - | return provider.NewXAI(r.Credential, r.BaseURL) | |
| 681 | + | return wrap(provider.NewXAI(r.Credential, r.BaseURL)) | |
| 677 | 682 | case "together": | |
| 678 | - | return provider.NewTogether(r.Credential, r.BaseURL) | |
| 683 | + | return wrap(provider.NewTogether(r.Credential, r.BaseURL)) | |
| 679 | 684 | case "huggingface": | |
| 680 | - | return provider.NewHuggingFace(r.Credential, r.BaseURL) | |
| 685 | + | return wrap(provider.NewHuggingFace(r.Credential, r.BaseURL)) | |
| 681 | 686 | case "openrouter": | |
| 682 | - | return provider.NewOpenRouter(r.Credential, r.BaseURL) | |
| 687 | + | return wrap(provider.NewOpenRouter(r.Credential, r.BaseURL)) | |
| 683 | 688 | case "zai": | |
| 684 | - | return provider.NewZAI(r.Credential, r.BaseURL) | |
| 689 | + | return wrap(provider.NewZAI(r.Credential, r.BaseURL)) | |
| 685 | 690 | case "xiaomi": | |
| 686 | - | return provider.NewXiaomi(r.Credential, r.BaseURL) | |
| 691 | + | return wrap(provider.NewXiaomi(r.Credential, r.BaseURL)) | |
| 687 | 692 | case "xiaomi-token-plan-ams": | |
| 688 | - | return provider.NewXiaomiTokenPlan("ams", r.Credential, r.BaseURL) | |
| 693 | + | return wrap(provider.NewXiaomiTokenPlan("ams", r.Credential, r.BaseURL)) | |
| 689 | 694 | case "xiaomi-token-plan-cn": | |
| 690 | - | return provider.NewXiaomiTokenPlan("cn", r.Credential, r.BaseURL) | |
| 695 | + | return wrap(provider.NewXiaomiTokenPlan("cn", r.Credential, r.BaseURL)) | |
| 691 | 696 | case "xiaomi-token-plan-sgp": | |
| 692 | - | return provider.NewXiaomiTokenPlan("sgp", r.Credential, r.BaseURL) | |
| 697 | + | return wrap(provider.NewXiaomiTokenPlan("sgp", r.Credential, r.BaseURL)) | |
| 693 | 698 | case "opencode": | |
| 694 | - | return provider.NewOpenCode(r.Credential, r.BaseURL) | |
| 699 | + | return wrap(provider.NewOpenCode(r.Credential, r.BaseURL)) | |
| 695 | 700 | case "opencode-go": | |
| 696 | - | return provider.NewOpenCodeGo(r.Credential, r.BaseURL) | |
| 701 | + | return wrap(provider.NewOpenCodeGo(r.Credential, r.BaseURL)) | |
| 697 | 702 | case "minimax": | |
| 698 | - | return provider.NewMinimaxAnthropic(r.Credential, r.BaseURL) | |
| 703 | + | return wrap(provider.NewMinimaxAnthropic(r.Credential, r.BaseURL)) | |
| 699 | 704 | case "minimax-cn": | |
| 700 | - | return provider.NewMinimaxCNAnthropic(r.Credential, r.BaseURL) | |
| 705 | + | return wrap(provider.NewMinimaxCNAnthropic(r.Credential, r.BaseURL)) | |
| 701 | 706 | case "fireworks": | |
| 702 | - | return provider.NewFireworksAnthropic(r.Credential, r.BaseURL) | |
| 707 | + | return wrap(provider.NewFireworksAnthropic(r.Credential, r.BaseURL)) | |
| 703 | 708 | case "vercel-ai-gateway": | |
| 704 | - | return provider.NewVercelGatewayAnthropic(r.Credential, r.BaseURL) | |
| 709 | + | return wrap(provider.NewVercelGatewayAnthropic(r.Credential, r.BaseURL)) | |
| 705 | 710 | case "mistral": | |
| 706 | - | return provider.NewMistral(r.Credential, r.BaseURL) | |
| 711 | + | return wrap(provider.NewMistral(r.Credential, r.BaseURL)) | |
| 707 | 712 | case "amazon-bedrock": | |
| 708 | - | return provider.NewBedrock(r.Credential, r.BaseURL) | |
| 713 | + | return wrap(provider.NewBedrock(r.Credential, r.BaseURL)) | |
| 709 | 714 | case "google-vertex": | |
| 710 | - | return provider.NewGoogleVertex(r.Credential, r.BaseURL) | |
| 715 | + | return wrap(provider.NewGoogleVertex(r.Credential, r.BaseURL)) | |
| 711 | 716 | case "azure-openai-responses": | |
| 712 | - | return provider.NewAzureOpenAIResponses(r.Credential, r.BaseURL) | |
| 717 | + | return wrap(provider.NewAzureOpenAIResponses(r.Credential, r.BaseURL)) | |
| 713 | 718 | case "github-copilot": | |
| 714 | - | return provider.NewGithubCopilot(r.Credential, r.BaseURL) | |
| 719 | + | return wrap(provider.NewGithubCopilot(r.Credential, r.BaseURL)) | |
| 715 | 720 | case "cloudflare-workers-ai": | |
| 716 | - | return provider.NewCloudflareWorkersAI(r.Credential, r.BaseURL) | |
| 721 | + | return wrap(provider.NewCloudflareWorkersAI(r.Credential, r.BaseURL)) | |
| 717 | 722 | case "cloudflare-ai-gateway": | |
| 718 | - | return provider.NewCloudflareAIGateway(r.Credential, r.BaseURL) | |
| 723 | + | return wrap(provider.NewCloudflareAIGateway(r.Credential, r.BaseURL)) | |
| 719 | 724 | default: | |
| 720 | 725 | if r.AuthMethod == "oauth" { | |
| 721 | - | inner := provider.NewAnthropicOAuth(r.Credential, r.BaseURL) | |
| 726 | + | inner := wrap(provider.NewAnthropicOAuth(r.Credential, r.BaseURL)) | |
| 722 | 727 | return r.wrapWithRefresh(inner) | |
| 723 | 728 | } | |
| 724 | - | return provider.NewAnthropic(r.Credential, r.BaseURL) | |
| 729 | + | return wrap(provider.NewAnthropic(r.Credential, r.BaseURL)) | |
| 730 | + | } | |
| 731 | + | } | |
| 732 | + | ||
| 733 | + | func (r Resolved) withHTTPClient(c provider.Client) provider.Client { | |
| 734 | + | if !r.InsecureTLS { | |
| 735 | + | return c | |
| 725 | 736 | } | |
| 737 | + | return provider.WithHTTPClient(c, provider.NewHTTPClient(true)) | |
| 726 | 738 | } | |
| 727 | 739 | ||
| 728 | 740 | // wrapWithRefresh wraps an OAuth client so the access token is | |
| @@ -748,12 +760,12 @@ func (r Resolved) wrapWithRefresh(inner provider.Client) provider.Client { | |||
| 748 | 760 | factory := func(token string) provider.Client { | |
| 749 | 761 | switch provName { | |
| 750 | 762 | case "openai-codex": | |
| 751 | - | return provider.NewOpenAICodex(token, accountID, baseURL) | |
| 763 | + | return r.withHTTPClient(provider.NewOpenAICodex(token, accountID, baseURL)) | |
| 752 | 764 | case "kimi": | |
| 753 | 765 | // anthropic-messages on api.kimi.com/coding. | |
| 754 | - | return provider.NewKimiCodingWithHeaders(token, baseURL, kimiCodeHeaders()) | |
| 766 | + | return r.withHTTPClient(provider.NewKimiCodingWithHeaders(token, baseURL, kimiCodeHeaders())) | |
| 755 | 767 | default: | |
| 756 | - | return provider.NewAnthropicOAuth(token, baseURL) | |
| 768 | + | return r.withHTTPClient(provider.NewAnthropicOAuth(token, baseURL)) | |
| 757 | 769 | } | |
| 758 | 770 | } | |
| 759 | 771 | ||
| @@ -23,6 +23,7 @@ type Resolved struct { | |||||
| 23 | AuthMethod string // "apikey" | "oauth" | "" (no credential yet) | 23 | AuthMethod string // "apikey" | "oauth" | "" (no credential yet) | ||
| 24 | AccountID string // ChatGPT account id (for openai oauth), "" otherwise | 24 | AccountID string // ChatGPT account id (for openai oauth), "" otherwise | ||
| 25 | BaseURL string | 25 | BaseURL string | ||
| 26 | + | InsecureTLS bool | |||
| 26 | CWD string | 27 | CWD string | ||
| 27 | Reasoning string | 28 | Reasoning string | ||
| 28 | Temperature *float32 | 29 | Temperature *float32 | ||
| @@ -409,6 +410,8 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 409 | model = fm.ID | 410 | model = fm.ID | ||
| 410 | } | 411 | } | ||
| 411 | 412 | ||||
| 413 | + | explicitBaseURL := args.BaseURL != "" | |||
| 414 | + | ||||
| 412 | // If the model defines a base URL (e.g. local ollama) and the | 415 | // If the model defines a base URL (e.g. local ollama) and the | ||
| 413 | // user didn't pass --base-url, use the model's URL. For ollama, | 416 | // user didn't pass --base-url, use the model's URL. For ollama, | ||
| 414 | // keep http://localhost:11434 as a fallback only after the model | 417 | // keep http://localhost:11434 as a fallback only after the model | ||
| @@ -420,10 +423,10 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 420 | args.BaseURL = "http://localhost:11434" | 423 | args.BaseURL = "http://localhost:11434" | ||
| 421 | } | 424 | } | ||
| 422 | 425 | ||||
| 423 | - | provider.InsecureSkipVerify = (args.InsecureTLS || cfg.Insecure) && args.BaseURL != "" | 426 | + | // Insecure TLS is intentionally scoped to explicit custom endpoints. |
| 424 | - | if provider.InsecureSkipVerify { | 427 | + | // Built-in provider base URLs, auth calls, and model discovery keep normal |
| 425 | - | provider.ApplyInsecureTLS() | 428 | + | // certificate verification even when --insecure is present. |
| 426 | - | } | 429 | + | insecureTLS := (args.InsecureTLS || cfg.Insecure) && explicitBaseURL |
| 427 | 430 | ||||
| 428 | // If the model has a base URL, credentials are optional (local | 431 | // If the model has a base URL, credentials are optional (local | ||
| 429 | // models like ollama don't need real API keys). | 432 | // models like ollama don't need real API keys). | ||
| @@ -512,6 +515,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 512 | AuthMethod: method, | 515 | AuthMethod: method, | ||
| 513 | AccountID: accountID, | 516 | AccountID: accountID, | ||
| 514 | BaseURL: args.BaseURL, | 517 | BaseURL: args.BaseURL, | ||
| 518 | + | InsecureTLS: insecureTLS, | |||
| 515 | CWD: args.CWD, | 519 | CWD: args.CWD, | ||
| 516 | Reasoning: reasoning, | 520 | Reasoning: reasoning, | ||
| 517 | Temperature: temperature, | 521 | Temperature: temperature, | ||
| @@ -639,90 +643,98 @@ func (r Resolved) NewClient() provider.Client { | |||||
| 639 | if !r.HasCredential() { | 643 | if !r.HasCredential() { | ||
| 640 | panic("NewClient called without credential; check HasCredential first") | 644 | panic("NewClient called without credential; check HasCredential first") | ||
| 641 | } | 645 | } | ||
| 646 | + | wrap := r.withHTTPClient | |||
| 642 | switch r.Provider { | 647 | switch r.Provider { | ||
| 643 | case "ollama": | 648 | case "ollama": | ||
| 644 | - | return provider.NewOpenAI(r.Credential, r.BaseURL) | 649 | + | return wrap(provider.NewOpenAI(r.Credential, r.BaseURL)) |
| 645 | case "kimi": | 650 | case "kimi": | ||
| 646 | // kimi-coding speaks anthropic-messages on api.kimi.com/coding. | 651 | // kimi-coding speaks anthropic-messages on api.kimi.com/coding. | ||
| 647 | // Subscription OAuth (refreshed) wraps the same Anthropic-shaped client. | 652 | // Subscription OAuth (refreshed) wraps the same Anthropic-shaped client. | ||
| 648 | - | inner := provider.NewKimiCodingWithHeaders(r.Credential, r.BaseURL, kimiCodeHeaders()) | 653 | + | inner := wrap(provider.NewKimiCodingWithHeaders(r.Credential, r.BaseURL, kimiCodeHeaders())) |
| 649 | if r.AuthMethod == "oauth" { | 654 | if r.AuthMethod == "oauth" { | ||
| 650 | return r.wrapWithRefresh(inner) | 655 | return r.wrapWithRefresh(inner) | ||
| 651 | } | 656 | } | ||
| 652 | return inner | 657 | return inner | ||
| 653 | case "moonshotai": | 658 | case "moonshotai": | ||
| 654 | - | return provider.NewMoonshot(r.Credential, r.BaseURL) | 659 | + | return wrap(provider.NewMoonshot(r.Credential, r.BaseURL)) |
| 655 | case "moonshotai-cn": | 660 | case "moonshotai-cn": | ||
| 656 | - | return provider.NewMoonshotCN(r.Credential, r.BaseURL) | 661 | + | return wrap(provider.NewMoonshotCN(r.Credential, r.BaseURL)) |
| 657 | case "deepseek": | 662 | case "deepseek": | ||
| 658 | - | return provider.NewDeepSeek(r.Credential, r.BaseURL) | 663 | + | return wrap(provider.NewDeepSeek(r.Credential, r.BaseURL)) |
| 659 | case "openai": | 664 | case "openai": | ||
| 660 | - | return provider.NewOpenAI(r.Credential, r.BaseURL) | 665 | + | return wrap(provider.NewOpenAI(r.Credential, r.BaseURL)) |
| 661 | case "openai-codex": | 666 | case "openai-codex": | ||
| 662 | - | inner := provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL) | 667 | + | inner := wrap(provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)) |
| 663 | return r.wrapWithRefresh(inner) | 668 | return r.wrapWithRefresh(inner) | ||
| 664 | case "openai-responses": | 669 | case "openai-responses": | ||
| 665 | // Public OpenAI Responses API (api.openai.com/v1/responses) via | 670 | // Public OpenAI Responses API (api.openai.com/v1/responses) via | ||
| 666 | // API key. Separate provider from `openai` (Chat Completions) and | 671 | // API key. Separate provider from `openai` (Chat Completions) and | ||
| 667 | // from `openai-codex` (ChatGPT subscription OAuth). | 672 | // from `openai-codex` (ChatGPT subscription OAuth). | ||
| 668 | - | return provider.NewOpenAIResponses(r.Credential, r.BaseURL) | 673 | + | return wrap(provider.NewOpenAIResponses(r.Credential, r.BaseURL)) |
| 669 | case "google": | 674 | case "google": | ||
| 670 | - | return provider.NewGemini(r.Credential, r.BaseURL) | 675 | + | return wrap(provider.NewGemini(r.Credential, r.BaseURL)) |
| 671 | case "cerebras": | 676 | case "cerebras": | ||
| 672 | - | return provider.NewCerebras(r.Credential, r.BaseURL) | 677 | + | return wrap(provider.NewCerebras(r.Credential, r.BaseURL)) |
| 673 | case "groq": | 678 | case "groq": | ||
| 674 | - | return provider.NewGroq(r.Credential, r.BaseURL) | 679 | + | return wrap(provider.NewGroq(r.Credential, r.BaseURL)) |
| 675 | case "xai": | 680 | case "xai": | ||
| 676 | - | return provider.NewXAI(r.Credential, r.BaseURL) | 681 | + | return wrap(provider.NewXAI(r.Credential, r.BaseURL)) |
| 677 | case "together": | 682 | case "together": | ||
| 678 | - | return provider.NewTogether(r.Credential, r.BaseURL) | 683 | + | return wrap(provider.NewTogether(r.Credential, r.BaseURL)) |
| 679 | case "huggingface": | 684 | case "huggingface": | ||
| 680 | - | return provider.NewHuggingFace(r.Credential, r.BaseURL) | 685 | + | return wrap(provider.NewHuggingFace(r.Credential, r.BaseURL)) |
| 681 | case "openrouter": | 686 | case "openrouter": | ||
| 682 | - | return provider.NewOpenRouter(r.Credential, r.BaseURL) | 687 | + | return wrap(provider.NewOpenRouter(r.Credential, r.BaseURL)) |
| 683 | case "zai": | 688 | case "zai": | ||
| 684 | - | return provider.NewZAI(r.Credential, r.BaseURL) | 689 | + | return wrap(provider.NewZAI(r.Credential, r.BaseURL)) |
| 685 | case "xiaomi": | 690 | case "xiaomi": | ||
| 686 | - | return provider.NewXiaomi(r.Credential, r.BaseURL) | 691 | + | return wrap(provider.NewXiaomi(r.Credential, r.BaseURL)) |
| 687 | case "xiaomi-token-plan-ams": | 692 | case "xiaomi-token-plan-ams": | ||
| 688 | - | return provider.NewXiaomiTokenPlan("ams", r.Credential, r.BaseURL) | 693 | + | return wrap(provider.NewXiaomiTokenPlan("ams", r.Credential, r.BaseURL)) |
| 689 | case "xiaomi-token-plan-cn": | 694 | case "xiaomi-token-plan-cn": | ||
| 690 | - | return provider.NewXiaomiTokenPlan("cn", r.Credential, r.BaseURL) | 695 | + | return wrap(provider.NewXiaomiTokenPlan("cn", r.Credential, r.BaseURL)) |
| 691 | case "xiaomi-token-plan-sgp": | 696 | case "xiaomi-token-plan-sgp": | ||
| 692 | - | return provider.NewXiaomiTokenPlan("sgp", r.Credential, r.BaseURL) | 697 | + | return wrap(provider.NewXiaomiTokenPlan("sgp", r.Credential, r.BaseURL)) |
| 693 | case "opencode": | 698 | case "opencode": | ||
| 694 | - | return provider.NewOpenCode(r.Credential, r.BaseURL) | 699 | + | return wrap(provider.NewOpenCode(r.Credential, r.BaseURL)) |
| 695 | case "opencode-go": | 700 | case "opencode-go": | ||
| 696 | - | return provider.NewOpenCodeGo(r.Credential, r.BaseURL) | 701 | + | return wrap(provider.NewOpenCodeGo(r.Credential, r.BaseURL)) |
| 697 | case "minimax": | 702 | case "minimax": | ||
| 698 | - | return provider.NewMinimaxAnthropic(r.Credential, r.BaseURL) | 703 | + | return wrap(provider.NewMinimaxAnthropic(r.Credential, r.BaseURL)) |
| 699 | case "minimax-cn": | 704 | case "minimax-cn": | ||
| 700 | - | return provider.NewMinimaxCNAnthropic(r.Credential, r.BaseURL) | 705 | + | return wrap(provider.NewMinimaxCNAnthropic(r.Credential, r.BaseURL)) |
| 701 | case "fireworks": | 706 | case "fireworks": | ||
| 702 | - | return provider.NewFireworksAnthropic(r.Credential, r.BaseURL) | 707 | + | return wrap(provider.NewFireworksAnthropic(r.Credential, r.BaseURL)) |
| 703 | case "vercel-ai-gateway": | 708 | case "vercel-ai-gateway": | ||
| 704 | - | return provider.NewVercelGatewayAnthropic(r.Credential, r.BaseURL) | 709 | + | return wrap(provider.NewVercelGatewayAnthropic(r.Credential, r.BaseURL)) |
| 705 | case "mistral": | 710 | case "mistral": | ||
| 706 | - | return provider.NewMistral(r.Credential, r.BaseURL) | 711 | + | return wrap(provider.NewMistral(r.Credential, r.BaseURL)) |
| 707 | case "amazon-bedrock": | 712 | case "amazon-bedrock": | ||
| 708 | - | return provider.NewBedrock(r.Credential, r.BaseURL) | 713 | + | return wrap(provider.NewBedrock(r.Credential, r.BaseURL)) |
| 709 | case "google-vertex": | 714 | case "google-vertex": | ||
| 710 | - | return provider.NewGoogleVertex(r.Credential, r.BaseURL) | 715 | + | return wrap(provider.NewGoogleVertex(r.Credential, r.BaseURL)) |
| 711 | case "azure-openai-responses": | 716 | case "azure-openai-responses": | ||
| 712 | - | return provider.NewAzureOpenAIResponses(r.Credential, r.BaseURL) | 717 | + | return wrap(provider.NewAzureOpenAIResponses(r.Credential, r.BaseURL)) |
| 713 | case "github-copilot": | 718 | case "github-copilot": | ||
| 714 | - | return provider.NewGithubCopilot(r.Credential, r.BaseURL) | 719 | + | return wrap(provider.NewGithubCopilot(r.Credential, r.BaseURL)) |
| 715 | case "cloudflare-workers-ai": | 720 | case "cloudflare-workers-ai": | ||
| 716 | - | return provider.NewCloudflareWorkersAI(r.Credential, r.BaseURL) | 721 | + | return wrap(provider.NewCloudflareWorkersAI(r.Credential, r.BaseURL)) |
| 717 | case "cloudflare-ai-gateway": | 722 | case "cloudflare-ai-gateway": | ||
| 718 | - | return provider.NewCloudflareAIGateway(r.Credential, r.BaseURL) | 723 | + | return wrap(provider.NewCloudflareAIGateway(r.Credential, r.BaseURL)) |
| 719 | default: | 724 | default: | ||
| 720 | if r.AuthMethod == "oauth" { | 725 | if r.AuthMethod == "oauth" { | ||
| 721 | - | inner := provider.NewAnthropicOAuth(r.Credential, r.BaseURL) | 726 | + | inner := wrap(provider.NewAnthropicOAuth(r.Credential, r.BaseURL)) |
| 722 | return r.wrapWithRefresh(inner) | 727 | return r.wrapWithRefresh(inner) | ||
| 723 | } | 728 | } | ||
| 724 | - | return provider.NewAnthropic(r.Credential, r.BaseURL) | 729 | + | return wrap(provider.NewAnthropic(r.Credential, r.BaseURL)) |
| 730 | + | } | |||
| 731 | + | } | |||
| 732 | + | ||||
| 733 | + | func (r Resolved) withHTTPClient(c provider.Client) provider.Client { | |||
| 734 | + | if !r.InsecureTLS { | |||
| 735 | + | return c | |||
| 725 | } | 736 | } | ||
| 737 | + | return provider.WithHTTPClient(c, provider.NewHTTPClient(true)) | |||
| 726 | } | 738 | } | ||
| 727 | 739 | ||||
| 728 | // wrapWithRefresh wraps an OAuth client so the access token is | 740 | // wrapWithRefresh wraps an OAuth client so the access token is | ||
| @@ -748,12 +760,12 @@ func (r Resolved) wrapWithRefresh(inner provider.Client) provider.Client { | |||||
| 748 | factory := func(token string) provider.Client { | 760 | factory := func(token string) provider.Client { | ||
| 749 | switch provName { | 761 | switch provName { | ||
| 750 | case "openai-codex": | 762 | case "openai-codex": | ||
| 751 | - | return provider.NewOpenAICodex(token, accountID, baseURL) | 763 | + | return r.withHTTPClient(provider.NewOpenAICodex(token, accountID, baseURL)) |
| 752 | case "kimi": | 764 | case "kimi": | ||
| 753 | // anthropic-messages on api.kimi.com/coding. | 765 | // anthropic-messages on api.kimi.com/coding. | ||
| 754 | - | return provider.NewKimiCodingWithHeaders(token, baseURL, kimiCodeHeaders()) | 766 | + | return r.withHTTPClient(provider.NewKimiCodingWithHeaders(token, baseURL, kimiCodeHeaders())) |
| 755 | default: | 767 | default: | ||
| 756 | - | return provider.NewAnthropicOAuth(token, baseURL) | 768 | + | return r.withHTTPClient(provider.NewAnthropicOAuth(token, baseURL)) |
| 757 | } | 769 | } | ||
| 758 | } | 770 | } | ||
| 759 | 771 | ||||
M packages/agent/build_test.go +34 -26
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package agent | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "net/http" | |
| 4 | 5 | "os" | |
| 5 | 6 | "path/filepath" | |
| 6 | 7 | "strings" | |
| @@ -212,37 +213,35 @@ func TestCanonicalProviderAliasesAreKnown(t *testing.T) { | |||
| 212 | 213 | } | |
| 213 | 214 | } | |
| 214 | 215 | ||
| 215 | - | func TestResolveInsecureOnlyWithCustomBaseURL(t *testing.T) { | |
| 216 | - | orig := provider.InsecureSkipVerify | |
| 217 | - | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | |
| 216 | + | func TestResolveInsecureOnlyWithExplicitBaseURL(t *testing.T) { | |
| 217 | + | orig := http.DefaultTransport | |
| 218 | + | t.Cleanup(func() { http.DefaultTransport = orig }) | |
| 218 | 219 | ||
| 219 | 220 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 220 | 221 | t.Setenv("OPENAI_API_KEY", "test-key") | |
| 221 | 222 | ||
| 222 | - | // no --base-url: must stay false even with --insecure. | |
| 223 | - | provider.InsecureSkipVerify = false | |
| 224 | - | _, err := Resolve(Args{Provider: "openai", InsecureTLS: true}, false) | |
| 223 | + | resolved, err := Resolve(Args{Provider: "moonshotai", InsecureTLS: true}, false) | |
| 225 | 224 | if err != nil { | |
| 226 | 225 | t.Fatalf("Resolve: %v", err) | |
| 227 | 226 | } | |
| 228 | - | if provider.InsecureSkipVerify { | |
| 229 | - | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | |
| 227 | + | if resolved.InsecureTLS { | |
| 228 | + | t.Fatal("InsecureTLS must not be set for built-in provider base URLs") | |
| 230 | 229 | } | |
| 230 | + | assertDefaultTransportStillSecure(t) | |
| 231 | 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) | |
| 232 | + | resolved, err = Resolve(Args{Provider: "openai", InsecureTLS: true, BaseURL: "https://my-llm.internal/v1"}, false) | |
| 235 | 233 | if err != nil { | |
| 236 | 234 | t.Fatalf("Resolve: %v", err) | |
| 237 | 235 | } | |
| 238 | - | if !provider.InsecureSkipVerify { | |
| 239 | - | t.Fatal("InsecureSkipVerify must be set with --insecure and --base-url") | |
| 236 | + | if !resolved.InsecureTLS { | |
| 237 | + | t.Fatal("InsecureTLS must be set with --insecure and explicit --base-url") | |
| 240 | 238 | } | |
| 239 | + | assertDefaultTransportStillSecure(t) | |
| 241 | 240 | } | |
| 242 | 241 | ||
| 243 | - | func TestResolveInsecureFromConfig(t *testing.T) { | |
| 244 | - | orig := provider.InsecureSkipVerify | |
| 245 | - | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | |
| 242 | + | func TestResolveInsecureFromConfigRequiresExplicitBaseURL(t *testing.T) { | |
| 243 | + | orig := http.DefaultTransport | |
| 244 | + | t.Cleanup(func() { http.DefaultTransport = orig }) | |
| 246 | 245 | ||
| 247 | 246 | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 248 | 247 | t.Setenv("OPENAI_API_KEY", "test-key") | |
| @@ -250,23 +249,32 @@ func TestResolveInsecureFromConfig(t *testing.T) { | |||
| 250 | 249 | t.Fatal(err) | |
| 251 | 250 | } | |
| 252 | 251 | ||
| 253 | - | // no --base-url: must stay false. | |
| 254 | - | provider.InsecureSkipVerify = false | |
| 255 | - | _, err := Resolve(Args{Provider: "openai"}, false) | |
| 252 | + | resolved, err := Resolve(Args{Provider: "openai"}, false) | |
| 256 | 253 | if err != nil { | |
| 257 | 254 | t.Fatalf("Resolve: %v", err) | |
| 258 | 255 | } | |
| 259 | - | if provider.InsecureSkipVerify { | |
| 260 | - | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | |
| 256 | + | if resolved.InsecureTLS { | |
| 257 | + | t.Fatal("InsecureTLS must not be set without a custom base URL") | |
| 261 | 258 | } | |
| 259 | + | assertDefaultTransportStillSecure(t) | |
| 262 | 260 | ||
| 263 | - | // --base-url: must be true. | |
| 264 | - | provider.InsecureSkipVerify = false | |
| 265 | - | _, err = Resolve(Args{Provider: "openai", BaseURL: "https://my-llm.internal/v1"}, false) | |
| 261 | + | resolved, err = Resolve(Args{Provider: "openai", BaseURL: "https://my-llm.internal/v1"}, false) | |
| 266 | 262 | if err != nil { | |
| 267 | 263 | t.Fatalf("Resolve: %v", err) | |
| 268 | 264 | } | |
| 269 | - | if !provider.InsecureSkipVerify { | |
| 270 | - | t.Fatal("InsecureSkipVerify must be set when config insecure=true and --base-url is provided") | |
| 265 | + | if !resolved.InsecureTLS { | |
| 266 | + | t.Fatal("InsecureTLS must be set when config insecure=true and --base-url is provided") | |
| 267 | + | } | |
| 268 | + | assertDefaultTransportStillSecure(t) | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | func assertDefaultTransportStillSecure(t *testing.T) { | |
| 272 | + | t.Helper() | |
| 273 | + | tr, ok := http.DefaultTransport.(*http.Transport) | |
| 274 | + | if !ok { | |
| 275 | + | return | |
| 276 | + | } | |
| 277 | + | if tr.TLSClientConfig != nil && tr.TLSClientConfig.InsecureSkipVerify { | |
| 278 | + | t.Fatal("http.DefaultTransport must not be made insecure") | |
| 271 | 279 | } | |
| 272 | 280 | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package agent | 1 | package agent | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "net/http" | |||
| 4 | "os" | 5 | "os" | ||
| 5 | "path/filepath" | 6 | "path/filepath" | ||
| 6 | "strings" | 7 | "strings" | ||
| @@ -212,37 +213,35 @@ func TestCanonicalProviderAliasesAreKnown(t *testing.T) { | |||||
| 212 | } | 213 | } | ||
| 213 | } | 214 | } | ||
| 214 | 215 | ||||
| 215 | - | func TestResolveInsecureOnlyWithCustomBaseURL(t *testing.T) { | 216 | + | func TestResolveInsecureOnlyWithExplicitBaseURL(t *testing.T) { |
| 216 | - | orig := provider.InsecureSkipVerify | 217 | + | orig := http.DefaultTransport |
| 217 | - | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | 218 | + | t.Cleanup(func() { http.DefaultTransport = orig }) |
| 218 | 219 | ||||
| 219 | t.Setenv("ZOT_HOME", t.TempDir()) | 220 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 220 | t.Setenv("OPENAI_API_KEY", "test-key") | 221 | t.Setenv("OPENAI_API_KEY", "test-key") | ||
| 221 | 222 | ||||
| 222 | - | // no --base-url: must stay false even with --insecure. | 223 | + | resolved, err := Resolve(Args{Provider: "moonshotai", InsecureTLS: true}, false) |
| 223 | - | provider.InsecureSkipVerify = false | |||
| 224 | - | _, err := Resolve(Args{Provider: "openai", InsecureTLS: true}, false) | |||
| 225 | if err != nil { | 224 | if err != nil { | ||
| 226 | t.Fatalf("Resolve: %v", err) | 225 | t.Fatalf("Resolve: %v", err) | ||
| 227 | } | 226 | } | ||
| 228 | - | if provider.InsecureSkipVerify { | 227 | + | if resolved.InsecureTLS { |
| 229 | - | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | 228 | + | t.Fatal("InsecureTLS must not be set for built-in provider base URLs") |
| 230 | } | 229 | } | ||
| 230 | + | assertDefaultTransportStillSecure(t) | |||
| 231 | 231 | ||||
| 232 | - | // --base-url + --insecure: must be true. | 232 | + | resolved, err = Resolve(Args{Provider: "openai", InsecureTLS: true, BaseURL: "https://my-llm.internal/v1"}, false) |
| 233 | - | provider.InsecureSkipVerify = false | |||
| 234 | - | _, err = Resolve(Args{Provider: "openai", InsecureTLS: true, BaseURL: "https://my-llm.internal/v1"}, false) | |||
| 235 | if err != nil { | 233 | if err != nil { | ||
| 236 | t.Fatalf("Resolve: %v", err) | 234 | t.Fatalf("Resolve: %v", err) | ||
| 237 | } | 235 | } | ||
| 238 | - | if !provider.InsecureSkipVerify { | 236 | + | if !resolved.InsecureTLS { |
| 239 | - | t.Fatal("InsecureSkipVerify must be set with --insecure and --base-url") | 237 | + | t.Fatal("InsecureTLS must be set with --insecure and explicit --base-url") |
| 240 | } | 238 | } | ||
| 239 | + | assertDefaultTransportStillSecure(t) | |||
| 241 | } | 240 | } | ||
| 242 | 241 | ||||
| 243 | - | func TestResolveInsecureFromConfig(t *testing.T) { | 242 | + | func TestResolveInsecureFromConfigRequiresExplicitBaseURL(t *testing.T) { |
| 244 | - | orig := provider.InsecureSkipVerify | 243 | + | orig := http.DefaultTransport |
| 245 | - | t.Cleanup(func() { provider.InsecureSkipVerify = orig }) | 244 | + | t.Cleanup(func() { http.DefaultTransport = orig }) |
| 246 | 245 | ||||
| 247 | t.Setenv("ZOT_HOME", t.TempDir()) | 246 | t.Setenv("ZOT_HOME", t.TempDir()) | ||
| 248 | t.Setenv("OPENAI_API_KEY", "test-key") | 247 | t.Setenv("OPENAI_API_KEY", "test-key") | ||
| @@ -250,23 +249,32 @@ func TestResolveInsecureFromConfig(t *testing.T) { | |||||
| 250 | t.Fatal(err) | 249 | t.Fatal(err) | ||
| 251 | } | 250 | } | ||
| 252 | 251 | ||||
| 253 | - | // no --base-url: must stay false. | 252 | + | resolved, err := Resolve(Args{Provider: "openai"}, false) |
| 254 | - | provider.InsecureSkipVerify = false | |||
| 255 | - | _, err := Resolve(Args{Provider: "openai"}, false) | |||
| 256 | if err != nil { | 253 | if err != nil { | ||
| 257 | t.Fatalf("Resolve: %v", err) | 254 | t.Fatalf("Resolve: %v", err) | ||
| 258 | } | 255 | } | ||
| 259 | - | if provider.InsecureSkipVerify { | 256 | + | if resolved.InsecureTLS { |
| 260 | - | t.Fatal("InsecureSkipVerify must not be set without a custom base URL") | 257 | + | t.Fatal("InsecureTLS must not be set without a custom base URL") |
| 261 | } | 258 | } | ||
| 259 | + | assertDefaultTransportStillSecure(t) | |||
| 262 | 260 | ||||
| 263 | - | // --base-url: must be true. | 261 | + | resolved, err = Resolve(Args{Provider: "openai", BaseURL: "https://my-llm.internal/v1"}, false) |
| 264 | - | provider.InsecureSkipVerify = false | |||
| 265 | - | _, err = Resolve(Args{Provider: "openai", BaseURL: "https://my-llm.internal/v1"}, false) | |||
| 266 | if err != nil { | 262 | if err != nil { | ||
| 267 | t.Fatalf("Resolve: %v", err) | 263 | t.Fatalf("Resolve: %v", err) | ||
| 268 | } | 264 | } | ||
| 269 | - | if !provider.InsecureSkipVerify { | 265 | + | if !resolved.InsecureTLS { |
| 270 | - | t.Fatal("InsecureSkipVerify must be set when config insecure=true and --base-url is provided") | 266 | + | t.Fatal("InsecureTLS must be set when config insecure=true and --base-url is provided") |
| 267 | + | } | |||
| 268 | + | assertDefaultTransportStillSecure(t) | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | func assertDefaultTransportStillSecure(t *testing.T) { | |||
| 272 | + | t.Helper() | |||
| 273 | + | tr, ok := http.DefaultTransport.(*http.Transport) | |||
| 274 | + | if !ok { | |||
| 275 | + | return | |||
| 276 | + | } | |||
| 277 | + | if tr.TLSClientConfig != nil && tr.TLSClientConfig.InsecureSkipVerify { | |||
| 278 | + | t.Fatal("http.DefaultTransport must not be made insecure") | |||
| 271 | } | 279 | } | ||
| 272 | } | 280 | } | ||
M packages/provider/httpclient.go +40 -6
| @@ -5,12 +5,46 @@ import ( | |||
| 5 | 5 | "net/http" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| 8 | - | // InsecureSkipVerify disables TLS cert verification for inference connections. | |
| 9 | - | var InsecureSkipVerify bool | |
| 8 | + | // NewHTTPClient returns a provider HTTP client. When insecureTLS is true, | |
| 9 | + | // only this client skips TLS certificate verification. The process-wide | |
| 10 | + | // default transport is left untouched so auth, discovery, and other providers | |
| 11 | + | // keep normal certificate validation. | |
| 12 | + | func NewHTTPClient(insecureTLS bool) *http.Client { | |
| 13 | + | if !insecureTLS { | |
| 14 | + | return &http.Client{Timeout: 0} | |
| 15 | + | } | |
| 16 | + | tr, ok := http.DefaultTransport.(*http.Transport) | |
| 17 | + | if ok { | |
| 18 | + | tr = tr.Clone() | |
| 19 | + | } else { | |
| 20 | + | tr = &http.Transport{} | |
| 21 | + | } | |
| 22 | + | if tr.TLSClientConfig != nil { | |
| 23 | + | tr.TLSClientConfig = tr.TLSClientConfig.Clone() | |
| 24 | + | } else { | |
| 25 | + | tr.TLSClientConfig = &tls.Config{} | |
| 26 | + | } | |
| 27 | + | tr.TLSClientConfig.InsecureSkipVerify = true //nolint:gosec | |
| 28 | + | return &http.Client{Timeout: 0, Transport: tr} | |
| 29 | + | } | |
| 10 | 30 | ||
| 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 | |
| 31 | + | // WithHTTPClient scopes an HTTP client to a concrete provider client. | |
| 32 | + | // Unsupported clients are returned unchanged. | |
| 33 | + | func WithHTTPClient(c Client, httpClient *http.Client) Client { | |
| 34 | + | if httpClient == nil { | |
| 35 | + | return c | |
| 36 | + | } | |
| 37 | + | switch v := c.(type) { | |
| 38 | + | case *openaiClient: | |
| 39 | + | v.http = httpClient | |
| 40 | + | case *anthropicClient: | |
| 41 | + | v.http = httpClient | |
| 42 | + | case *geminiClient: | |
| 43 | + | v.http = httpClient | |
| 44 | + | case *bedrockClient: | |
| 45 | + | v.http = httpClient | |
| 46 | + | case *renamedClient: | |
| 47 | + | v.inner = WithHTTPClient(v.inner, httpClient) | |
| 15 | 48 | } | |
| 49 | + | return c | |
| 16 | 50 | } | |
| @@ -5,12 +5,46 @@ import ( | |||||
| 5 | "net/http" | 5 | "net/http" | ||
| 6 | ) | 6 | ) | ||
| 7 | 7 | ||||
| 8 | - | // InsecureSkipVerify disables TLS cert verification for inference connections. | 8 | + | // NewHTTPClient returns a provider HTTP client. When insecureTLS is true, |
| 9 | - | var InsecureSkipVerify bool | 9 | + | // only this client skips TLS certificate verification. The process-wide |
| 10 | + | // default transport is left untouched so auth, discovery, and other providers | |||
| 11 | + | // keep normal certificate validation. | |||
| 12 | + | func NewHTTPClient(insecureTLS bool) *http.Client { | |||
| 13 | + | if !insecureTLS { | |||
| 14 | + | return &http.Client{Timeout: 0} | |||
| 15 | + | } | |||
| 16 | + | tr, ok := http.DefaultTransport.(*http.Transport) | |||
| 17 | + | if ok { | |||
| 18 | + | tr = tr.Clone() | |||
| 19 | + | } else { | |||
| 20 | + | tr = &http.Transport{} | |||
| 21 | + | } | |||
| 22 | + | if tr.TLSClientConfig != nil { | |||
| 23 | + | tr.TLSClientConfig = tr.TLSClientConfig.Clone() | |||
| 24 | + | } else { | |||
| 25 | + | tr.TLSClientConfig = &tls.Config{} | |||
| 26 | + | } | |||
| 27 | + | tr.TLSClientConfig.InsecureSkipVerify = true //nolint:gosec | |||
| 28 | + | return &http.Client{Timeout: 0, Transport: tr} | |||
| 29 | + | } | |||
| 10 | 30 | ||||
| 11 | - | // ApplyInsecureTLS replaces http.DefaultTransport to skip TLS cert verification. | 31 | + | // WithHTTPClient scopes an HTTP client to a concrete provider client. |
| 12 | - | func ApplyInsecureTLS() { | 32 | + | // Unsupported clients are returned unchanged. |
| 13 | - | http.DefaultTransport = &http.Transport{ | 33 | + | func WithHTTPClient(c Client, httpClient *http.Client) Client { |
| 14 | - | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec | 34 | + | if httpClient == nil { |
| 35 | + | return c | |||
| 36 | + | } | |||
| 37 | + | switch v := c.(type) { | |||
| 38 | + | case *openaiClient: | |||
| 39 | + | v.http = httpClient | |||
| 40 | + | case *anthropicClient: | |||
| 41 | + | v.http = httpClient | |||
| 42 | + | case *geminiClient: | |||
| 43 | + | v.http = httpClient | |||
| 44 | + | case *bedrockClient: | |||
| 45 | + | v.http = httpClient | |||
| 46 | + | case *renamedClient: | |||
| 47 | + | v.inner = WithHTTPClient(v.inner, httpClient) | |||
| 15 | } | 48 | } | ||
| 49 | + | return c | |||
| 16 | } | 50 | } | ||
M packages/provider/httpclient_test.go +20 -15
| @@ -6,22 +6,24 @@ import ( | |||
| 6 | 6 | "testing" | |
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | - | func TestApplyInsecureTLSSetsDefaultTransport(t *testing.T) { | |
| 9 | + | func TestNewHTTPClientDoesNotChangeDefaultTransport(t *testing.T) { | |
| 10 | 10 | orig := http.DefaultTransport | |
| 11 | 11 | t.Cleanup(func() { http.DefaultTransport = orig }) | |
| 12 | 12 | ||
| 13 | - | ApplyInsecureTLS() | |
| 14 | - | ||
| 15 | - | tr, ok := http.DefaultTransport.(*http.Transport) | |
| 13 | + | client := NewHTTPClient(true) | |
| 14 | + | if http.DefaultTransport != orig { | |
| 15 | + | t.Fatal("NewHTTPClient must not mutate http.DefaultTransport") | |
| 16 | + | } | |
| 17 | + | tr, ok := client.Transport.(*http.Transport) | |
| 16 | 18 | if !ok { | |
| 17 | - | t.Fatalf("expected *http.Transport, got %T", http.DefaultTransport) | |
| 19 | + | t.Fatalf("expected *http.Transport, got %T", client.Transport) | |
| 18 | 20 | } | |
| 19 | 21 | if tr.TLSClientConfig == nil || !tr.TLSClientConfig.InsecureSkipVerify { | |
| 20 | - | t.Fatal("expected InsecureSkipVerify=true in TLS config") | |
| 22 | + | t.Fatal("expected scoped InsecureSkipVerify=true in TLS config") | |
| 21 | 23 | } | |
| 22 | 24 | } | |
| 23 | 25 | ||
| 24 | - | func TestInsecureClientReachesTLSServer(t *testing.T) { | |
| 26 | + | func TestScopedInsecureClientReachesTLSServer(t *testing.T) { | |
| 25 | 27 | srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 26 | 28 | w.WriteHeader(http.StatusOK) | |
| 27 | 29 | })) | |
| @@ -30,20 +32,23 @@ func TestInsecureClientReachesTLSServer(t *testing.T) { | |||
| 30 | 32 | orig := http.DefaultTransport | |
| 31 | 33 | t.Cleanup(func() { http.DefaultTransport = orig }) | |
| 32 | 34 | ||
| 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") | |
| 35 | + | secureClient := NewHTTPClient(false) | |
| 36 | + | if _, err := secureClient.Get(srv.URL); err == nil { | |
| 37 | + | t.Fatal("expected TLS error with secure client, got nil") | |
| 37 | 38 | } | |
| 38 | 39 | ||
| 39 | - | ApplyInsecureTLS() | |
| 40 | - | ||
| 41 | - | resp, err := client.Get(srv.URL) | |
| 40 | + | insecureClient := NewHTTPClient(true) | |
| 41 | + | resp, err := insecureClient.Get(srv.URL) | |
| 42 | 42 | if err != nil { | |
| 43 | - | t.Fatalf("request failed after ApplyInsecureTLS: %v", err) | |
| 43 | + | t.Fatalf("request failed with scoped insecure client: %v", err) | |
| 44 | 44 | } | |
| 45 | 45 | resp.Body.Close() | |
| 46 | 46 | if resp.StatusCode != http.StatusOK { | |
| 47 | 47 | t.Fatalf("status=%d", resp.StatusCode) | |
| 48 | 48 | } | |
| 49 | + | ||
| 50 | + | defaultClient := &http.Client{} | |
| 51 | + | if _, err := defaultClient.Get(srv.URL); err == nil { | |
| 52 | + | t.Fatal("default client must still reject self-signed TLS") | |
| 53 | + | } | |
| 49 | 54 | } | |
| @@ -6,22 +6,24 @@ import ( | |||||
| 6 | "testing" | 6 | "testing" | ||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | - | func TestApplyInsecureTLSSetsDefaultTransport(t *testing.T) { | 9 | + | func TestNewHTTPClientDoesNotChangeDefaultTransport(t *testing.T) { |
| 10 | orig := http.DefaultTransport | 10 | orig := http.DefaultTransport | ||
| 11 | t.Cleanup(func() { http.DefaultTransport = orig }) | 11 | t.Cleanup(func() { http.DefaultTransport = orig }) | ||
| 12 | 12 | ||||
| 13 | - | ApplyInsecureTLS() | 13 | + | client := NewHTTPClient(true) |
| 14 | - | 14 | + | if http.DefaultTransport != orig { | |
| 15 | - | tr, ok := http.DefaultTransport.(*http.Transport) | 15 | + | t.Fatal("NewHTTPClient must not mutate http.DefaultTransport") |
| 16 | + | } | |||
| 17 | + | tr, ok := client.Transport.(*http.Transport) | |||
| 16 | if !ok { | 18 | if !ok { | ||
| 17 | - | t.Fatalf("expected *http.Transport, got %T", http.DefaultTransport) | 19 | + | t.Fatalf("expected *http.Transport, got %T", client.Transport) |
| 18 | } | 20 | } | ||
| 19 | if tr.TLSClientConfig == nil || !tr.TLSClientConfig.InsecureSkipVerify { | 21 | if tr.TLSClientConfig == nil || !tr.TLSClientConfig.InsecureSkipVerify { | ||
| 20 | - | t.Fatal("expected InsecureSkipVerify=true in TLS config") | 22 | + | t.Fatal("expected scoped InsecureSkipVerify=true in TLS config") |
| 21 | } | 23 | } | ||
| 22 | } | 24 | } | ||
| 23 | 25 | ||||
| 24 | - | func TestInsecureClientReachesTLSServer(t *testing.T) { | 26 | + | func TestScopedInsecureClientReachesTLSServer(t *testing.T) { |
| 25 | srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | 27 | srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | ||
| 26 | w.WriteHeader(http.StatusOK) | 28 | w.WriteHeader(http.StatusOK) | ||
| 27 | })) | 29 | })) | ||
| @@ -30,20 +32,23 @@ func TestInsecureClientReachesTLSServer(t *testing.T) { | |||||
| 30 | orig := http.DefaultTransport | 32 | orig := http.DefaultTransport | ||
| 31 | t.Cleanup(func() { http.DefaultTransport = orig }) | 33 | t.Cleanup(func() { http.DefaultTransport = orig }) | ||
| 32 | 34 | ||||
| 33 | - | client := &http.Client{} | 35 | + | secureClient := NewHTTPClient(false) |
| 34 | - | 36 | + | if _, err := secureClient.Get(srv.URL); err == nil { | |
| 35 | - | if _, err := client.Get(srv.URL); err == nil { | 37 | + | t.Fatal("expected TLS error with secure client, got nil") |
| 36 | - | t.Fatal("expected TLS error with default transport, got nil") | |||
| 37 | } | 38 | } | ||
| 38 | 39 | ||||
| 39 | - | ApplyInsecureTLS() | 40 | + | insecureClient := NewHTTPClient(true) |
| 40 | - | 41 | + | resp, err := insecureClient.Get(srv.URL) | |
| 41 | - | resp, err := client.Get(srv.URL) | |||
| 42 | if err != nil { | 42 | if err != nil { | ||
| 43 | - | t.Fatalf("request failed after ApplyInsecureTLS: %v", err) | 43 | + | t.Fatalf("request failed with scoped insecure client: %v", err) |
| 44 | } | 44 | } | ||
| 45 | resp.Body.Close() | 45 | resp.Body.Close() | ||
| 46 | if resp.StatusCode != http.StatusOK { | 46 | if resp.StatusCode != http.StatusOK { | ||
| 47 | t.Fatalf("status=%d", resp.StatusCode) | 47 | t.Fatalf("status=%d", resp.StatusCode) | ||
| 48 | } | 48 | } | ||
| 49 | + | ||||
| 50 | + | defaultClient := &http.Client{} | |||
| 51 | + | if _, err := defaultClient.Get(srv.URL); err == nil { | |||
| 52 | + | t.Fatal("default client must still reject self-signed TLS") | |||
| 53 | + | } | |||
| 49 | } | 54 | } | ||