diff --git a/config.example.yaml b/config.example.yaml index 3a44af693..027b7acb5 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -220,6 +220,8 @@ codex: # Some superstitious users believe request tracking identifiers can be used # as evidence for TOS enforcement bans; this option only satisfies those odd concerns. identity-confuse: false + # Disable forcing the official Codex User-Agent and Originator headers on HTTP requests. + disable-codex-cloaking: false # When true, optimize Codex Desktop and codex-tui requests for multi-agent v2. # This refreshes Codex spawn_agent model details, removes message parameter encryption, # normalizes encrypted agent_message content for Codex, and converts agent_message input diff --git a/internal/config/codex_websocket_header_defaults_test.go b/internal/config/codex_websocket_header_defaults_test.go index 6eb0e65ad..86bf610e2 100644 --- a/internal/config/codex_websocket_header_defaults_test.go +++ b/internal/config/codex_websocket_header_defaults_test.go @@ -29,6 +29,9 @@ codex-header-defaults: if got := cfg.CodexHeaderDefaults.BetaFeatures; got != "feature-a,feature-b" { t.Fatalf("BetaFeatures = %q, want %q", got, "feature-a,feature-b") } + if cfg.Codex.DisableCodexCloaking { + t.Fatal("DisableCodexCloaking = true, want default false") + } } func TestLoadConfigOptional_CodexIdentityConfuse(t *testing.T) { @@ -37,6 +40,7 @@ func TestLoadConfigOptional_CodexIdentityConfuse(t *testing.T) { configYAML := []byte(` codex: identity-confuse: true + disable-codex-cloaking: true optimize-multi-agent-v2: true `) if err := os.WriteFile(configPath, configYAML, 0o600); err != nil { @@ -51,6 +55,9 @@ codex: if !cfg.Codex.IdentityConfuse { t.Fatalf("IdentityConfuse = false, want true") } + if !cfg.Codex.DisableCodexCloaking { + t.Fatal("DisableCodexCloaking = false, want true") + } if !cfg.Codex.OptimizeMultiAgentV2 { t.Fatalf("OptimizeMultiAgentV2 = false, want true") } diff --git a/internal/config/config_types.go b/internal/config/config_types.go index cb4e63ede..a75a4c70e 100644 --- a/internal/config/config_types.go +++ b/internal/config/config_types.go @@ -127,6 +127,8 @@ type XAIConfig struct { // CodexConfig configures provider-wide Codex request behavior. type CodexConfig struct { IdentityConfuse bool `yaml:"identity-confuse" json:"identity-confuse"` + // DisableCodexCloaking disables forcing the official Codex identity headers on HTTP requests. + DisableCodexCloaking bool `yaml:"disable-codex-cloaking" json:"disable-codex-cloaking"` // OptimizeMultiAgentV2 optimizes official Codex multi-agent requests. OptimizeMultiAgentV2 bool `yaml:"optimize-multi-agent-v2" json:"optimize-multi-agent-v2"` // LiveMediaRelay terminates and relays Codex Live WebRTC media in this process. diff --git a/internal/runtime/executor/codex_executor_request.go b/internal/runtime/executor/codex_executor_request.go index 7a5f86348..8a76620c1 100644 --- a/internal/runtime/executor/codex_executor_request.go +++ b/internal/runtime/executor/codex_executor_request.go @@ -23,7 +23,7 @@ import ( ) const ( - codexUserAgent = "codex-tui/0.135.0 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10 (codex-tui; 0.135.0)" + codexUserAgent = "codex-tui/0.146.0 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10 (codex-tui; 0.146.0)" codexOriginator = "codex-tui" codexDefaultImageToolModel = "gpt-image-2" codexResponsesLiteHeader = "X-OpenAI-Internal-Codex-Responses-Lite" @@ -352,6 +352,10 @@ func applyCodexHeadersFromSources(r *http.Request, auth *cliproxyauth.Auth, toke attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(r, attrs) + if cfg != nil && !cfg.Codex.DisableCodexCloaking { + r.Header.Set("User-Agent", codexUserAgent) + r.Header.Set("Originator", codexOriginator) + } } func normalizeCodexInstructions(body []byte) []byte { diff --git a/internal/runtime/executor/codex_openai_images_test.go b/internal/runtime/executor/codex_openai_images_test.go index 6bc5b6389..bd1818d42 100644 --- a/internal/runtime/executor/codex_openai_images_test.go +++ b/internal/runtime/executor/codex_openai_images_test.go @@ -105,8 +105,8 @@ func TestCodexExecutorDirectOpenAIImageGenerationUsesImagesEndpoint(t *testing.T if gotClientRequestID != "client-request-1" { t.Fatalf("X-Client-Request-Id = %q, want %q", gotClientRequestID, "client-request-1") } - if gotOriginator != "Codex Desktop" { - t.Fatalf("Originator = %q, want %q", gotOriginator, "Codex Desktop") + if gotOriginator != codexOriginator { + t.Fatalf("Originator = %q, want %q", gotOriginator, codexOriginator) } if got := gjson.GetBytes(gotBody, "model").String(); got != "gpt-image-1.5" { t.Fatalf("model = %q, want gpt-image-1.5; body=%s", got, string(gotBody)) diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index 17bbdcbea..3d6b2ade2 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -1510,6 +1510,7 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) { t.Fatalf("NewRequest() error = %v", err) } cfg := &config.Config{ + Codex: config.CodexConfig{DisableCodexCloaking: true}, CodexHeaderDefaults: config.CodexHeaderDefaults{ UserAgent: "config-ua", BetaFeatures: "config-beta", @@ -1533,6 +1534,41 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) { } } +func TestApplyCodexHeadersDefaultsToCodexCloaking(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil) + if err != nil { + t.Fatalf("NewRequest() error = %v", err) + } + req.Header.Set("User-Agent", "existing-ua") + req.Header.Set("Originator", "existing-origin") + cfg := &config.Config{ + CodexHeaderDefaults: config.CodexHeaderDefaults{ + UserAgent: "config-ua", + }, + } + auth := &cliproxyauth.Auth{ + Provider: "codex", + Attributes: map[string]string{ + "api_key": "api-key", + "header:User-Agent": "custom-ua", + "header:Originator": "custom-origin", + }, + } + ginHeaders := http.Header{ + "User-Agent": []string{"client-ua"}, + "Originator": []string{"client-origin"}, + } + + applyCodexHeadersFromSources(req, auth, "api-key", false, cfg, ginHeaders) + + if got := req.Header.Get("User-Agent"); got != codexUserAgent { + t.Fatalf("User-Agent = %q, want %q", got, codexUserAgent) + } + if got := req.Header.Get("Originator"); got != codexOriginator { + t.Fatalf("Originator = %q, want %q", got, codexOriginator) + } +} + func TestApplyModelHeaderOverridesFromModelConfig(t *testing.T) { const wantUA = "codex-tui/0.144.0 (Mac OS 26.5.1; arm64) iTerm.app/3.6.11 (codex-tui; 0.144.0)" req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil) @@ -1614,7 +1650,8 @@ func TestApplyCodexHeadersPassesThroughClientIdentityHeaders(t *testing.T) { "X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d", })) - applyCodexHeaders(req, auth, "oauth-token", true, nil) + cfg := &config.Config{Codex: config.CodexConfig{DisableCodexCloaking: true}} + applyCodexHeaders(req, auth, "oauth-token", true, cfg) if got := req.Header.Get("Originator"); got != "Codex Desktop" { t.Fatalf("Originator = %s, want %s", got, "Codex Desktop") diff --git a/internal/watcher/diff/config_diff.go b/internal/watcher/diff/config_diff.go index e89eeed91..cc3a0507d 100644 --- a/internal/watcher/diff/config_diff.go +++ b/internal/watcher/diff/config_diff.go @@ -108,6 +108,9 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { if oldCfg.Codex.IdentityConfuse != newCfg.Codex.IdentityConfuse { changes = append(changes, fmt.Sprintf("codex.identity-confuse: %t -> %t", oldCfg.Codex.IdentityConfuse, newCfg.Codex.IdentityConfuse)) } + if oldCfg.Codex.DisableCodexCloaking != newCfg.Codex.DisableCodexCloaking { + changes = append(changes, fmt.Sprintf("codex.disable-codex-cloaking: %t -> %t", oldCfg.Codex.DisableCodexCloaking, newCfg.Codex.DisableCodexCloaking)) + } if oldCfg.Codex.OptimizeMultiAgentV2 != newCfg.Codex.OptimizeMultiAgentV2 { changes = append(changes, fmt.Sprintf("codex.optimize-multi-agent-v2: %t -> %t", oldCfg.Codex.OptimizeMultiAgentV2, newCfg.Codex.OptimizeMultiAgentV2)) } diff --git a/internal/watcher/diff/config_diff_test.go b/internal/watcher/diff/config_diff_test.go index 4a365b2a8..2fe865408 100644 --- a/internal/watcher/diff/config_diff_test.go +++ b/internal/watcher/diff/config_diff_test.go @@ -39,6 +39,7 @@ func TestBuildConfigChangeDetails(t *testing.T) { newCfg := &config.Config{ Port: 9090, AuthDir: "/tmp/auth-new", + Codex: config.CodexConfig{DisableCodexCloaking: true}, GeminiKey: []config.GeminiKey{ {APIKey: "old", BaseURL: "http://old", ExcludedModels: []string{"old-model", "extra"}}, }, @@ -78,6 +79,7 @@ func TestBuildConfigChangeDetails(t *testing.T) { expectContains(t, details, "remote-management.allow-remote: false -> true") expectContains(t, details, "remote-management.disable-auto-update-panel: false -> true") expectContains(t, details, "remote-management.secret-key: updated") + expectContains(t, details, "codex.disable-codex-cloaking: false -> true") expectContains(t, details, "oauth-excluded-models[providera]: updated (1 -> 2 entries)") expectContains(t, details, "oauth-excluded-models[providerb]: added (1 entries)") expectContains(t, details, "openai-compatibility:")