Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions cmd/onecli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 = "<proxyURL>",
// 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
Expand Down
117 changes: 117 additions & 0 deletions cmd/onecli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading