From 2d37088bd95301300bf5a5360aa546766a1720c3 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Sat, 19 Sep 2026 10:36:51 +0800 Subject: [PATCH] fix(settings): preserve client version on invalid user agent --- .../openai_codex_version_sync_service_test.go | 63 ++++++++++++++++++- .../service/setting_gateway_runtime.go | 19 +++--- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/backend/internal/service/openai_codex_version_sync_service_test.go b/backend/internal/service/openai_codex_version_sync_service_test.go index 2a50527b89..b78cd8cc94 100644 --- a/backend/internal/service/openai_codex_version_sync_service_test.go +++ b/backend/internal/service/openai_codex_version_sync_service_test.go @@ -394,13 +394,13 @@ func TestGetOpenAICodexCanonicalUserAgentRebuildsPanelUAVersion(t *testing.T) { ) }) - // 非 `{client}/{version}` 形态无法重建,原样返回,由收口整体回退规范身份。 - t.Run("非 Codex 形态原样返回", func(t *testing.T) { + // 非 Codex UA 回退到规范身份。 + t.Run("非 Codex 形态回退", func(t *testing.T) { svc := NewSettingService(&codexVersionSettingRepoStub{values: map[string]string{ SettingKeyOpenAICodexUserAgent: "not-a-codex-client", }}, nil) - require.Equal(t, "not-a-codex-client", svc.GetOpenAICodexCanonicalUserAgent(context.Background())) + require.Equal(t, codexCLIUserAgent, svc.GetOpenAICodexCanonicalUserAgent(context.Background())) }) } @@ -478,3 +478,60 @@ func TestCodexVersionComparisonIsNumericNotLexical(t *testing.T) { require.Empty(t, repo.syncedWrites(), "0.99.0 低于已同步的 0.146.0,不得写入") } + +func TestGetOpenAICodexCanonicalUserAgentOutboundIdentity(t *testing.T) { + codexCanonicalUAMu.RLock() + previous := codexCanonicalUAResolver + codexCanonicalUAMu.RUnlock() + t.Cleanup(func() { SetCodexCanonicalUserAgentResolver(previous) }) + + for _, version := range []struct { + name, manual, synced, want string + }{ + {"manual", "0.154.0", "0.155.1", "0.154.0"}, + {"synced", "", "0.155.1", "0.155.1"}, + {"invalid_manual", "latest", "0.155.1", "0.155.1"}, + {"default", "", "", codexCLIVersion}, + {"invalid_versions", "latest", "invalid", codexCLIVersion}, + } { + for _, ua := range []struct { + name, value, client, suffix string + }{ + {name: "browser", value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"}, + {name: "arbitrary", value: "not-a-codex-client"}, + {name: "unknown_client", value: "custom/0.140.0 (Linux)"}, + {name: "empty"}, + {name: "missing_version", value: "codex_cli_rs/"}, + {name: "crlf", value: "codex_cli_rs/0.140.0 (Linux)\r\nX-Injected: value"}, + {name: "nul", value: "codex_cli_rs/0.140.0 (Linux\x00)"}, + {name: "del", value: "codex_cli_rs/0.140.0 terminal\x7f"}, + {name: "leading_newline", value: "\ncodex_cli_rs/0.140.0 (Linux)"}, + {name: "trailing_newline", value: "codex_cli_rs/0.140.0 (Linux)\n"}, + {name: "valid_suffix", value: "codex_cli_rs/0.140.0 (Mac OS X 15.1.0; arm64) iTerm.app", client: "codex_cli_rs", suffix: " (Mac OS X 15.1.0; arm64) iTerm.app"}, + {name: "valid_tab", value: "codex_cli_rs/0.140.0 (Linux)\tterminal", client: "codex_cli_rs", suffix: " (Linux)\tterminal"}, + {name: "recognized_trailer", value: "custom/0.140.0 (Linux) (codex-tui; 0.140.0)", client: "codex-tui", suffix: " (Linux) (codex-tui; " + version.want + ")"}, + } { + t.Run(version.name+"/"+ua.name, func(t *testing.T) { + svc := NewSettingService(&codexVersionSettingRepoStub{values: map[string]string{ + SettingKeyOpenAICodexUserAgent: ua.value, + SettingKeyOpenAICodexClientVersion: version.manual, + SettingKeyOpenAICodexClientVersionSynced: version.synced, + }}, nil) + wantUA := buildCodexCLIUserAgent(version.want) + wantOriginator := "codex-tui" + if ua.client != "" { + wantUA = ua.client + "/" + version.want + ua.suffix + wantOriginator = ua.client + } + SetCodexCanonicalUserAgentResolver(func() string { + return svc.GetOpenAICodexCanonicalUserAgent(context.Background()) + }) + require.Equal(t, wantUA, svc.GetOpenAICodexCanonicalUserAgent(context.Background())) + identity := resolveCodexOutboundIdentity("") + require.Equal(t, wantUA, identity.userAgent) + require.Equal(t, wantOriginator, identity.originator) + require.Equal(t, version.want, identity.version) + }) + } + } +} diff --git a/backend/internal/service/setting_gateway_runtime.go b/backend/internal/service/setting_gateway_runtime.go index 40fccd8fe1..bb59ba8526 100644 --- a/backend/internal/service/setting_gateway_runtime.go +++ b/backend/internal/service/setting_gateway_runtime.go @@ -294,7 +294,8 @@ func (s *SettingService) GetOpenAICodexUserAgent(ctx context.Context) string { }) return fallback, nil } - ua := strings.TrimSpace(value) + // Preserve invalid header bytes for canonical identity validation. + ua := strings.Trim(value, " \t") if ua == "" { ua = fallback } @@ -389,16 +390,14 @@ func (s *SettingService) GetOpenAICodexCanonicalUserAgent(ctx context.Context) s return codexCLIUserAgent } version := s.GetOpenAICodexClientVersion(ctx) - ua := strings.TrimSpace(s.GetOpenAICodexUserAgent(ctx)) - if ua == "" { - return buildCodexCLIUserAgent(version) + ua := s.GetOpenAICodexUserAgent(ctx) + if _, pairedUA, ok := openai.PairCodexClientIdentity(ua); ok { + if rebuilt := openai.SetCodexUserAgentVersion(pairedUA, version); rebuilt != "" { + return rebuilt + } } - if rebuilt := openai.SetCodexUserAgentVersion(ua, version); rebuilt != "" { - return rebuilt - } - // 非 `{client}/{version}` 形态:交给 PairCodexClientIdentity 判定, - // 推导不出官方身份时由收口整体回退规范身份。 - return ua + // Invalid fingerprints must not discard the independently resolved version. + return buildCodexCLIUserAgent(version) } var legacyClaudeCodeCodexWhitelistEntry = openai.AllowedClientEntry{