From 3d1ed400d625d0dcdb638e9e295f2db409901bb7 Mon Sep 17 00:00:00 2001 From: johnnyfish Date: Sat, 4 Jul 2026 14:51:24 -0700 Subject: [PATCH] fix: refresh stale gateway proxy_url in Codex config on each run --- cmd/onecli/run.go | 67 +++++++++++++++++++---- cmd/onecli/run_test.go | 117 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 9 deletions(-) diff --git a/cmd/onecli/run.go b/cmd/onecli/run.go index 65b8760..0bb328f 100644 --- a/cmd/onecli/run.go +++ b/cmd/onecli/run.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "runtime" "slices" "strings" @@ -261,6 +262,7 @@ var caTrustKeys = []string{ "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", + "AWS_CA_BUNDLE", "CURL_CA_BUNDLE", "GIT_SSL_CAINFO", "DENO_CERT", @@ -557,8 +559,9 @@ func maybeCreateCodexAuthStub(out *output.Writer, client *api.Client) { } // maybeInjectNativeProxyConfig writes proxy_url into a TOML config file for -// agents that have their own managed proxy (e.g. Codex). Also sets the -// agent-specific CA certificate env var. +// agents that have their own managed proxy (e.g. Codex), refreshing a stale +// gateway-owned value from a previous run. Also sets the agent-specific CA +// certificate env var. func maybeInjectNativeProxyConfig(out *output.Writer, agentName, configRelDir string, env []string, caPath string) { home, err := os.UserHomeDir() if err != nil { @@ -572,17 +575,23 @@ func maybeInjectNativeProxyConfig(out *output.Writer, agentName, configRelDir st configPath := filepath.Join(home, configRelDir, "config.toml") data, _ := os.ReadFile(configPath) - content := string(data) - // Inject [network] section with proxy_url if not already present. - if !strings.Contains(content, "proxy_url") { - section := "\n[network]\nproxy_url = \"" + proxyURL + "\"\n" - content += section - if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { + updated, status := upsertGatewayProxyURL(string(data), proxyURL) + switch status { + case proxyURLCurrent: + // Already pointing at this gateway URL; nothing to write. + case proxyURLForeign: + out.Stderr(fmt.Sprintf("onecli: warning: %s has a custom proxy_url in config.toml; leaving it unchanged, so native %s traffic will bypass the gateway.", agentName, agentName)) + default: + if err := os.WriteFile(configPath, []byte(updated), 0o600); err != nil { out.Stderr(fmt.Sprintf("onecli: warning: could not write proxy config for %s: %v", agentName, err)) return } - out.Stderr(fmt.Sprintf("onecli: configured native proxy for %s.", agentName)) + if status == proxyURLAdded { + out.Stderr(fmt.Sprintf("onecli: configured native proxy for %s.", agentName)) + } else { + out.Stderr(fmt.Sprintf("onecli: updated native proxy for %s.", agentName)) + } } // Set CODEX_CA_CERTIFICATE if we have a CA path — Codex reads this @@ -592,6 +601,46 @@ func maybeInjectNativeProxyConfig(out *output.Writer, agentName, configRelDir st } } +type proxyURLStatus int + +const ( + proxyURLAdded proxyURLStatus = iota + proxyURLRefreshed + proxyURLCurrent + proxyURLForeign +) + +var proxyURLLineRE = regexp.MustCompile(`^([ \t]*proxy_url[ \t]*=[ \t]*)"([^"]*)"[ \t]*$`) + +// upsertGatewayProxyURL ensures content carries proxy_url = "", +// appending a [network] section when the file has none. Gateway proxy URLs +// embed a per-run aoc_ session token, so a previously injected value goes +// stale on every new run; only values carrying that marker are rewritten +// (in place, preserving the rest of the file). A user-managed proxy_url — +// or one on a line we cannot parse, e.g. with a trailing comment — is left +// untouched. +func upsertGatewayProxyURL(content, proxyURL string) (string, proxyURLStatus) { + if !strings.Contains(content, "proxy_url") { + return content + "\n[network]\nproxy_url = \"" + proxyURL + "\"\n", proxyURLAdded + } + lines := strings.Split(content, "\n") + for i, line := range lines { + m := proxyURLLineRE.FindStringSubmatch(line) + if m == nil { + continue + } + if m[2] == proxyURL { + return content, proxyURLCurrent + } + if !strings.Contains(m[2], "aoc_") { + return content, proxyURLForeign + } + lines[i] = m[1] + `"` + proxyURL + `"` + return strings.Join(lines, "\n"), proxyURLRefreshed + } + return content, proxyURLForeign +} + // maybeInstallGatewayPlugin installs the Hermes transform_tool_result recovery // plugin and enables it in ~/.hermes/config.yaml. The plugin runs in the agent // process and appends gateway recovery guidance to any tool result that looks diff --git a/cmd/onecli/run_test.go b/cmd/onecli/run_test.go index c7886c8..df49b11 100644 --- a/cmd/onecli/run_test.go +++ b/cmd/onecli/run_test.go @@ -808,6 +808,123 @@ func TestMaybeInstallGatewayHook_CodexHooksFile(t *testing.T) { } } +func TestUpsertGatewayProxyURL(t *testing.T) { + gw := "http://aoc_new:x@127.0.0.1:10255" + tests := []struct { + name string + content string + want string + status proxyURLStatus + }{ + { + name: "empty file gets network section", + content: "", + want: "\n[network]\nproxy_url = \"" + gw + "\"\n", + status: proxyURLAdded, + }, + { + name: "existing config gets section appended", + content: "model = \"o4\"\n", + want: "model = \"o4\"\n\n[network]\nproxy_url = \"" + gw + "\"\n", + status: proxyURLAdded, + }, + { + name: "stale gateway url refreshed in place", + content: "# codex config\nmodel = \"o4\"\n\n[network]\nproxy_url = \"http://aoc_old:x@127.0.0.1:10255\"\nkeep = true\n", + want: "# codex config\nmodel = \"o4\"\n\n[network]\nproxy_url = \"" + gw + "\"\nkeep = true\n", + status: proxyURLRefreshed, + }, + { + name: "current gateway url untouched", + content: "[network]\nproxy_url = \"" + gw + "\"\n", + want: "[network]\nproxy_url = \"" + gw + "\"\n", + status: proxyURLCurrent, + }, + { + name: "user-managed proxy untouched", + content: "[network]\nproxy_url = \"http://corp-proxy:8080\"\n", + want: "[network]\nproxy_url = \"http://corp-proxy:8080\"\n", + status: proxyURLForeign, + }, + { + name: "unparseable proxy_url line untouched", + content: "[network]\nproxy_url = \"http://aoc_old:x@127.0.0.1:10255\" # pinned\n", + want: "[network]\nproxy_url = \"http://aoc_old:x@127.0.0.1:10255\" # pinned\n", + status: proxyURLForeign, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, status := upsertGatewayProxyURL(tt.content, gw) + if got != tt.want { + t.Errorf("content = %q, want %q", got, tt.want) + } + if status != tt.status { + t.Errorf("status = %d, want %d", status, tt.status) + } + }) + } +} + +func TestMaybeInjectNativeProxyConfig_RefreshesStaleToken(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test overrides HOME, which UserHomeDir ignores on windows") + } + home := t.TempDir() + t.Setenv("HOME", home) + + configPath := filepath.Join(home, ".codex", "config.toml") + if err := os.MkdirAll(filepath.Dir(configPath), 0o750); err != nil { + t.Fatal(err) + } + stale := "model = \"o4\"\n\n[network]\nproxy_url = \"http://aoc_old:x@127.0.0.1:10255\"\n" + if err := os.WriteFile(configPath, []byte(stale), 0o600); err != nil { + t.Fatal(err) + } + + var stderr bytes.Buffer + out := output.NewWithWriters(io.Discard, &stderr) + env := []string{"HTTPS_PROXY=http://aoc_new:x@127.0.0.1:10255"} + maybeInjectNativeProxyConfig(out, "Codex", ".codex", env, "") + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + content := string(data) + if !strings.Contains(content, "aoc_new") { + t.Errorf("stale token was not refreshed: %s", content) + } + if strings.Contains(content, "aoc_old") { + t.Errorf("stale token still present: %s", content) + } + if !strings.Contains(content, "model = \"o4\"") { + t.Errorf("unrelated config was dropped: %s", content) + } + if !strings.Contains(stderr.String(), "updated native proxy for Codex") { + t.Errorf("stderr = %q, want update notice", stderr.String()) + } + + // A user-managed proxy_url must survive and produce a warning. + custom := "[network]\nproxy_url = \"http://corp-proxy:8080\"\n" + if err := os.WriteFile(configPath, []byte(custom), 0o600); err != nil { + t.Fatal(err) + } + stderr.Reset() + maybeInjectNativeProxyConfig(out, "Codex", ".codex", env, "") + data, err = os.ReadFile(configPath) + if err != nil { + t.Fatal(err) + } + if string(data) != custom { + t.Errorf("user-managed config was modified: %s", data) + } + if !strings.Contains(stderr.String(), "custom proxy_url") { + t.Errorf("stderr = %q, want custom proxy warning", stderr.String()) + } +} + func userPromptEntries(t *testing.T, path string) []any { t.Helper() m := readSettingsMap(t, path)