Add global HTTP proxy configuration
- commit
- 43baf04e7cf498cd1f0f9559faab7ee1bd358ccf
- author
- patriceckhart · · 2026-08-06 06:01 UTC
- parent
- c420b4e
- signature
- unsigned
5 files · +125 -0
M README.md +12 -0
| @@ -150,6 +150,18 @@ $ZOT_HOME/ | |||
| 150 | 150 | ||
| 151 | 151 | Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and guidelines for every run. `--system-prompt` still wins per-invocation. Delete the file to revert to the default. | |
| 152 | 152 | ||
| 153 | + | ### HTTP proxy | |
| 154 | + | ||
| 155 | + | To route zot-managed HTTP and HTTPS requests through one proxy, add `http_proxy` to `$ZOT_HOME/config.json`: | |
| 156 | + | ||
| 157 | + | ```json | |
| 158 | + | { | |
| 159 | + | "http_proxy": "http://127.0.0.1:7890" | |
| 160 | + | } | |
| 161 | + | ``` | |
| 162 | + | ||
| 163 | + | The setting is applied at startup to both HTTP and HTTPS traffic. Existing `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, and `https_proxy` environment variables take precedence for their corresponding protocol. `NO_PROXY` and `no_proxy` continue to control bypasses. Restart zot after changing the config file. If the URL contains proxy credentials, prefer protected environment variables because `config.json` is not a credential store. | |
| 164 | + | ||
| 153 | 165 | ## Persistent instructions (AGENTS.md) | |
| 154 | 166 | ||
| 155 | 167 | Use `AGENTS.md` to give zot standing instructions that layer **on top of** the default system prompt, without replacing it. This is the friendliest way to shape behavior (for example, taming local models that jump straight to code edits) because it adds guidance rather than taking over the base identity the way `SYSTEM.md` does. | |
| @@ -150,6 +150,18 @@ $ZOT_HOME/ | |||||
| 150 | 150 | ||||
| 151 | Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and guidelines for every run. `--system-prompt` still wins per-invocation. Delete the file to revert to the default. | 151 | Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and guidelines for every run. `--system-prompt` still wins per-invocation. Delete the file to revert to the default. | ||
| 152 | 152 | ||||
| 153 | + | ### HTTP proxy | |||
| 154 | + | ||||
| 155 | + | To route zot-managed HTTP and HTTPS requests through one proxy, add `http_proxy` to `$ZOT_HOME/config.json`: | |||
| 156 | + | ||||
| 157 | + | ```json | |||
| 158 | + | { | |||
| 159 | + | "http_proxy": "http://127.0.0.1:7890" | |||
| 160 | + | } | |||
| 161 | + | ``` | |||
| 162 | + | ||||
| 163 | + | The setting is applied at startup to both HTTP and HTTPS traffic. Existing `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, and `https_proxy` environment variables take precedence for their corresponding protocol. `NO_PROXY` and `no_proxy` continue to control bypasses. Restart zot after changing the config file. If the URL contains proxy credentials, prefer protected environment variables because `config.json` is not a credential store. | |||
| 164 | + | ||||
| 153 | ## Persistent instructions (AGENTS.md) | 165 | ## Persistent instructions (AGENTS.md) | ||
| 154 | 166 | ||||
| 155 | Use `AGENTS.md` to give zot standing instructions that layer **on top of** the default system prompt, without replacing it. This is the friendliest way to shape behavior (for example, taming local models that jump straight to code edits) because it adds guidance rather than taking over the base identity the way `SYSTEM.md` does. | 167 | Use `AGENTS.md` to give zot standing instructions that layer **on top of** the default system prompt, without replacing it. This is the friendliest way to shape behavior (for example, taming local models that jump straight to code edits) because it adds guidance rather than taking over the base identity the way `SYSTEM.md` does. | ||
M docs/providers.md +14 -0
| @@ -4,6 +4,20 @@ zot ships with built-in providers and a model catalog. You can select models | |||
| 4 | 4 | with `/model`, list them with `zot --list-models`, and add private models in | |
| 5 | 5 | `$ZOT_HOME/models.json`. | |
| 6 | 6 | ||
| 7 | + | ## HTTP proxy | |
| 8 | + | ||
| 9 | + | Set one global proxy for zot-managed HTTP and HTTPS traffic with the `http_proxy` key in `$ZOT_HOME/config.json`: | |
| 10 | + | ||
| 11 | + | ```json | |
| 12 | + | { | |
| 13 | + | "http_proxy": "http://127.0.0.1:7890" | |
| 14 | + | } | |
| 15 | + | ``` | |
| 16 | + | ||
| 17 | + | zot applies this setting at startup. Existing `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, and `https_proxy` environment variables take precedence for their corresponding protocol. Standard `NO_PROXY` and `no_proxy` bypass lists remain effective. Restart zot after changing the setting. | |
| 18 | + | ||
| 19 | + | `config.json` is not a credential store. If the proxy URL contains a username or password, prefer setting the proxy environment variables in a protected shell or service configuration rather than saving those credentials in `config.json`. | |
| 20 | + | ||
| 7 | 21 | ## Login methods | |
| 8 | 22 | ||
| 9 | 23 | Use `/login` in interactive mode. Type in either provider picker to filter the list by provider ID or display name. | |
| @@ -4,6 +4,20 @@ zot ships with built-in providers and a model catalog. You can select models | |||||
| 4 | with `/model`, list them with `zot --list-models`, and add private models in | 4 | with `/model`, list them with `zot --list-models`, and add private models in | ||
| 5 | `$ZOT_HOME/models.json`. | 5 | `$ZOT_HOME/models.json`. | ||
| 6 | 6 | ||||
| 7 | + | ## HTTP proxy | |||
| 8 | + | ||||
| 9 | + | Set one global proxy for zot-managed HTTP and HTTPS traffic with the `http_proxy` key in `$ZOT_HOME/config.json`: | |||
| 10 | + | ||||
| 11 | + | ```json | |||
| 12 | + | { | |||
| 13 | + | "http_proxy": "http://127.0.0.1:7890" | |||
| 14 | + | } | |||
| 15 | + | ``` | |||
| 16 | + | ||||
| 17 | + | zot applies this setting at startup. Existing `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, and `https_proxy` environment variables take precedence for their corresponding protocol. Standard `NO_PROXY` and `no_proxy` bypass lists remain effective. Restart zot after changing the setting. | |||
| 18 | + | ||||
| 19 | + | `config.json` is not a credential store. If the proxy URL contains a username or password, prefer setting the proxy environment variables in a protected shell or service configuration rather than saving those credentials in `config.json`. | |||
| 20 | + | ||||
| 7 | ## Login methods | 21 | ## Login methods | ||
| 8 | 22 | ||||
| 9 | Use `/login` in interactive mode. Type in either provider picker to filter the list by provider ID or display name. | 23 | Use `/login` in interactive mode. Type in either provider picker to filter the list by provider ID or display name. | ||
M packages/agent/cli.go +4 -0
| @@ -193,6 +193,10 @@ func fanoutAgentEvent(mgr *extensions.Manager, ev core.AgentEvent) { | |||
| 193 | 193 | ||
| 194 | 194 | // Run is the top-level entrypoint for the zot binary. | |
| 195 | 195 | func Run(rawArgs []string, version string) error { | |
| 196 | + | // Apply network configuration before any subcommand can make an HTTP | |
| 197 | + | // request. Standard proxy environment variables retain precedence. | |
| 198 | + | applyConfiguredHTTPProxy() | |
| 199 | + | ||
| 196 | 200 | // Subcommand router: `zot bot ...` is handled separately so the | |
| 197 | 201 | // generic flag parser doesn't reject "bot" as a positional arg. | |
| 198 | 202 | if handled, err := runBotCommand(rawArgs, version); handled { | |
| @@ -193,6 +193,10 @@ func fanoutAgentEvent(mgr *extensions.Manager, ev core.AgentEvent) { | |||||
| 193 | 193 | ||||
| 194 | // Run is the top-level entrypoint for the zot binary. | 194 | // Run is the top-level entrypoint for the zot binary. | ||
| 195 | func Run(rawArgs []string, version string) error { | 195 | func Run(rawArgs []string, version string) error { | ||
| 196 | + | // Apply network configuration before any subcommand can make an HTTP | |||
| 197 | + | // request. Standard proxy environment variables retain precedence. | |||
| 198 | + | applyConfiguredHTTPProxy() | |||
| 199 | + | ||||
| 196 | // Subcommand router: `zot bot ...` is handled separately so the | 200 | // Subcommand router: `zot bot ...` is handled separately so the | ||
| 197 | // generic flag parser doesn't reject "bot" as a positional arg. | 201 | // generic flag parser doesn't reject "bot" as a positional arg. | ||
| 198 | if handled, err := runBotCommand(rawArgs, version); handled { | 202 | if handled, err := runBotCommand(rawArgs, version); handled { | ||
M packages/agent/config.go +24 -0
| @@ -108,6 +108,10 @@ type Config struct { | |||
| 108 | 108 | // Insecure skips TLS verification for custom inference endpoints. | |
| 109 | 109 | Insecure bool `json:"insecure,omitempty"` | |
| 110 | 110 | ||
| 111 | + | // HTTPProxy is a global proxy URL used for HTTP and HTTPS requests when | |
| 112 | + | // the corresponding standard proxy environment variable is not already set. | |
| 113 | + | HTTPProxy string `json:"http_proxy,omitempty"` | |
| 114 | + | ||
| 111 | 115 | // LastChangelogShown is the version whose release-notes | |
| 112 | 116 | // dialog the user has already seen. When the running binary's | |
| 113 | 117 | // version differs, the next interactive run shows the | |
| @@ -224,6 +228,26 @@ func SaveConfig(c Config) error { | |||
| 224 | 228 | return os.WriteFile(ConfigPath(), b, 0o644) | |
| 225 | 229 | } | |
| 226 | 230 | ||
| 231 | + | // applyConfiguredHTTPProxy makes the persisted proxy available to Go's | |
| 232 | + | // standard HTTP transport and to child processes. Explicit environment | |
| 233 | + | // variables take precedence over config.json. | |
| 234 | + | func applyConfiguredHTTPProxy() { | |
| 235 | + | cfg, err := LoadConfig() | |
| 236 | + | if err != nil { | |
| 237 | + | return | |
| 238 | + | } | |
| 239 | + | proxy := strings.TrimSpace(cfg.HTTPProxy) | |
| 240 | + | if proxy == "" { | |
| 241 | + | return | |
| 242 | + | } | |
| 243 | + | if os.Getenv("HTTP_PROXY") == "" && os.Getenv("http_proxy") == "" { | |
| 244 | + | _ = os.Setenv("HTTP_PROXY", proxy) | |
| 245 | + | } | |
| 246 | + | if os.Getenv("HTTPS_PROXY") == "" && os.Getenv("https_proxy") == "" { | |
| 247 | + | _ = os.Setenv("HTTPS_PROXY", proxy) | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 227 | 251 | // AuthStoreFor returns the auth.Store backed by AuthPath(). | |
| 228 | 252 | func AuthStoreFor() *auth.Store { return auth.NewStore(AuthPath()) } | |
| 229 | 253 | ||
| @@ -108,6 +108,10 @@ type Config struct { | |||||
| 108 | // Insecure skips TLS verification for custom inference endpoints. | 108 | // Insecure skips TLS verification for custom inference endpoints. | ||
| 109 | Insecure bool `json:"insecure,omitempty"` | 109 | Insecure bool `json:"insecure,omitempty"` | ||
| 110 | 110 | ||||
| 111 | + | // HTTPProxy is a global proxy URL used for HTTP and HTTPS requests when | |||
| 112 | + | // the corresponding standard proxy environment variable is not already set. | |||
| 113 | + | HTTPProxy string `json:"http_proxy,omitempty"` | |||
| 114 | + | ||||
| 111 | // LastChangelogShown is the version whose release-notes | 115 | // LastChangelogShown is the version whose release-notes | ||
| 112 | // dialog the user has already seen. When the running binary's | 116 | // dialog the user has already seen. When the running binary's | ||
| 113 | // version differs, the next interactive run shows the | 117 | // version differs, the next interactive run shows the | ||
| @@ -224,6 +228,26 @@ func SaveConfig(c Config) error { | |||||
| 224 | return os.WriteFile(ConfigPath(), b, 0o644) | 228 | return os.WriteFile(ConfigPath(), b, 0o644) | ||
| 225 | } | 229 | } | ||
| 226 | 230 | ||||
| 231 | + | // applyConfiguredHTTPProxy makes the persisted proxy available to Go's | |||
| 232 | + | // standard HTTP transport and to child processes. Explicit environment | |||
| 233 | + | // variables take precedence over config.json. | |||
| 234 | + | func applyConfiguredHTTPProxy() { | |||
| 235 | + | cfg, err := LoadConfig() | |||
| 236 | + | if err != nil { | |||
| 237 | + | return | |||
| 238 | + | } | |||
| 239 | + | proxy := strings.TrimSpace(cfg.HTTPProxy) | |||
| 240 | + | if proxy == "" { | |||
| 241 | + | return | |||
| 242 | + | } | |||
| 243 | + | if os.Getenv("HTTP_PROXY") == "" && os.Getenv("http_proxy") == "" { | |||
| 244 | + | _ = os.Setenv("HTTP_PROXY", proxy) | |||
| 245 | + | } | |||
| 246 | + | if os.Getenv("HTTPS_PROXY") == "" && os.Getenv("https_proxy") == "" { | |||
| 247 | + | _ = os.Setenv("HTTPS_PROXY", proxy) | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | ||||
| 227 | // AuthStoreFor returns the auth.Store backed by AuthPath(). | 251 | // AuthStoreFor returns the auth.Store backed by AuthPath(). | ||
| 228 | func AuthStoreFor() *auth.Store { return auth.NewStore(AuthPath()) } | 252 | func AuthStoreFor() *auth.Store { return auth.NewStore(AuthPath()) } | ||
| 229 | 253 | ||||
A packages/agent/config_proxy_test.go +71 -0
| @@ -0,0 +1,71 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestApplyConfiguredHTTPProxy(t *testing.T) { | |
| 9 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 10 | + | for _, key := range []string{"HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"} { | |
| 11 | + | t.Setenv(key, "") | |
| 12 | + | } | |
| 13 | + | if err := SaveConfig(Config{HTTPProxy: " http://proxy.example:8080 "}); err != nil { | |
| 14 | + | t.Fatal(err) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | applyConfiguredHTTPProxy() | |
| 18 | + | ||
| 19 | + | if got := getenvForTest("HTTP_PROXY"); got != "http://proxy.example:8080" { | |
| 20 | + | t.Fatalf("HTTP_PROXY = %q", got) | |
| 21 | + | } | |
| 22 | + | if got := getenvForTest("HTTPS_PROXY"); got != "http://proxy.example:8080" { | |
| 23 | + | t.Fatalf("HTTPS_PROXY = %q", got) | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | func TestApplyConfiguredHTTPProxyPreservesEnvironment(t *testing.T) { | |
| 28 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 29 | + | t.Setenv("HTTP_PROXY", "http://http-env.example:8080") | |
| 30 | + | t.Setenv("http_proxy", "") | |
| 31 | + | t.Setenv("HTTPS_PROXY", "") | |
| 32 | + | t.Setenv("https_proxy", "http://https-env.example:8080") | |
| 33 | + | if err := SaveConfig(Config{HTTPProxy: "http://config.example:8080"}); err != nil { | |
| 34 | + | t.Fatal(err) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | applyConfiguredHTTPProxy() | |
| 38 | + | ||
| 39 | + | if got := getenvForTest("HTTP_PROXY"); got != "http://http-env.example:8080" { | |
| 40 | + | t.Fatalf("HTTP_PROXY = %q", got) | |
| 41 | + | } | |
| 42 | + | if got := getenvForTest("HTTPS_PROXY"); got != "" { | |
| 43 | + | t.Fatalf("HTTPS_PROXY = %q, want empty because lowercase environment setting takes precedence", got) | |
| 44 | + | } | |
| 45 | + | if got := getenvForTest("https_proxy"); got != "http://https-env.example:8080" { | |
| 46 | + | t.Fatalf("https_proxy = %q", got) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func TestApplyConfiguredHTTPProxyIgnoresEmptySetting(t *testing.T) { | |
| 51 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |
| 52 | + | for _, key := range []string{"HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"} { | |
| 53 | + | t.Setenv(key, "") | |
| 54 | + | } | |
| 55 | + | if err := SaveConfig(Config{HTTPProxy: " "}); err != nil { | |
| 56 | + | t.Fatal(err) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | applyConfiguredHTTPProxy() | |
| 60 | + | ||
| 61 | + | if got := getenvForTest("HTTP_PROXY"); got != "" { | |
| 62 | + | t.Fatalf("HTTP_PROXY = %q", got) | |
| 63 | + | } | |
| 64 | + | if got := getenvForTest("HTTPS_PROXY"); got != "" { | |
| 65 | + | t.Fatalf("HTTPS_PROXY = %q", got) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func getenvForTest(key string) string { | |
| 70 | + | return os.Getenv(key) | |
| 71 | + | } | |
| @@ -0,0 +1,71 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestApplyConfiguredHTTPProxy(t *testing.T) { | |||
| 9 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 10 | + | for _, key := range []string{"HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"} { | |||
| 11 | + | t.Setenv(key, "") | |||
| 12 | + | } | |||
| 13 | + | if err := SaveConfig(Config{HTTPProxy: " http://proxy.example:8080 "}); err != nil { | |||
| 14 | + | t.Fatal(err) | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | applyConfiguredHTTPProxy() | |||
| 18 | + | ||||
| 19 | + | if got := getenvForTest("HTTP_PROXY"); got != "http://proxy.example:8080" { | |||
| 20 | + | t.Fatalf("HTTP_PROXY = %q", got) | |||
| 21 | + | } | |||
| 22 | + | if got := getenvForTest("HTTPS_PROXY"); got != "http://proxy.example:8080" { | |||
| 23 | + | t.Fatalf("HTTPS_PROXY = %q", got) | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | func TestApplyConfiguredHTTPProxyPreservesEnvironment(t *testing.T) { | |||
| 28 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 29 | + | t.Setenv("HTTP_PROXY", "http://http-env.example:8080") | |||
| 30 | + | t.Setenv("http_proxy", "") | |||
| 31 | + | t.Setenv("HTTPS_PROXY", "") | |||
| 32 | + | t.Setenv("https_proxy", "http://https-env.example:8080") | |||
| 33 | + | if err := SaveConfig(Config{HTTPProxy: "http://config.example:8080"}); err != nil { | |||
| 34 | + | t.Fatal(err) | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | applyConfiguredHTTPProxy() | |||
| 38 | + | ||||
| 39 | + | if got := getenvForTest("HTTP_PROXY"); got != "http://http-env.example:8080" { | |||
| 40 | + | t.Fatalf("HTTP_PROXY = %q", got) | |||
| 41 | + | } | |||
| 42 | + | if got := getenvForTest("HTTPS_PROXY"); got != "" { | |||
| 43 | + | t.Fatalf("HTTPS_PROXY = %q, want empty because lowercase environment setting takes precedence", got) | |||
| 44 | + | } | |||
| 45 | + | if got := getenvForTest("https_proxy"); got != "http://https-env.example:8080" { | |||
| 46 | + | t.Fatalf("https_proxy = %q", got) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func TestApplyConfiguredHTTPProxyIgnoresEmptySetting(t *testing.T) { | |||
| 51 | + | t.Setenv("ZOT_HOME", t.TempDir()) | |||
| 52 | + | for _, key := range []string{"HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"} { | |||
| 53 | + | t.Setenv(key, "") | |||
| 54 | + | } | |||
| 55 | + | if err := SaveConfig(Config{HTTPProxy: " "}); err != nil { | |||
| 56 | + | t.Fatal(err) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | applyConfiguredHTTPProxy() | |||
| 60 | + | ||||
| 61 | + | if got := getenvForTest("HTTP_PROXY"); got != "" { | |||
| 62 | + | t.Fatalf("HTTP_PROXY = %q", got) | |||
| 63 | + | } | |||
| 64 | + | if got := getenvForTest("HTTPS_PROXY"); got != "" { | |||
| 65 | + | t.Fatalf("HTTPS_PROXY = %q", got) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | func getenvForTest(key string) string { | |||
| 70 | + | return os.Getenv(key) | |||
| 71 | + | } | |||