feat(executor): implement identity obfuscation for Codex requests and responses

- Added `applyCodexIdentityConfuse*` functions for remapping request and response payloads and headers to enhance security.
- Updated WebSocket and HTTP logic to handle identity state transformations seamlessly.
- Introduced unit tests to verify remapping and restoration of identity-related fields.
This commit is contained in:
Luis Pater
2026-05-31 22:59:40 +08:00
parent 33983b6f3e
commit 0f24cafbdd
12 changed files with 397 additions and 91 deletions

View File

@@ -197,7 +197,7 @@ func TestApplyCodexWebsocketHeadersPassesThroughClientIdentityHeaders(t *testing
"Version": "0.115.0-alpha.27",
"X-Codex-Turn-Metadata": `{"turn_id":"turn-1"}`,
"X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d",
"session_id": "sess-client",
"session_id": "legacy-session",
})
headers := applyCodexWebsocketHeaders(ctx, http.Header{}, auth, "", nil)
@@ -217,11 +217,8 @@ func TestApplyCodexWebsocketHeadersPassesThroughClientIdentityHeaders(t *testing
if got := headers.Get("X-Client-Request-Id"); got != "019d2233-e240-7162-992d-38df0a2a0e0d" {
t.Fatalf("X-Client-Request-Id = %s, want %s", got, "019d2233-e240-7162-992d-38df0a2a0e0d")
}
if got := headerValueCaseInsensitive(headers, "session_id"); got != "sess-client" {
t.Fatalf("session_id = %s, want sess-client", got)
}
if _, ok := headers["session_id"]; !ok {
t.Fatalf("expected lowercase session_id header key, got %#v", headers)
if got := headerValueCaseInsensitive(headers, "session_id"); got != "" {
t.Fatalf("session_id = %q, want empty", got)
}
}
@@ -344,22 +341,101 @@ func TestApplyCodexWebsocketHeadersPreservesExplicitAPIKeyUserAgent(t *testing.T
}
}
func TestApplyCodexPromptCacheHeadersSetsLowercaseSessionAndLegacyConversation(t *testing.T) {
func TestApplyCodexPromptCacheHeadersSetsLegacyConversationOnly(t *testing.T) {
req := cliproxyexecutor.Request{Model: "gpt-5-codex", Payload: []byte(`{"prompt_cache_key":"cache-1"}`)}
_, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`))
if got := headerValueCaseInsensitive(headers, "session_id"); got != "cache-1" {
t.Fatalf("session_id = %s, want cache-1", got)
}
if _, ok := headers["session_id"]; !ok {
t.Fatalf("expected lowercase session_id key, got %#v", headers)
if got := headerValueCaseInsensitive(headers, "session_id"); got != "" {
t.Fatalf("session_id = %q, want empty", got)
}
if got := headers.Get("Conversation_id"); got != "cache-1" {
t.Fatalf("Conversation_id = %s, want cache-1", got)
}
}
func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testing.T) {
cfg := &config.Config{
Routing: config.RoutingConfig{SessionAffinity: true},
Codex: config.CodexConfig{IdentityConfuse: true},
}
auth := &cliproxyauth.Auth{ID: "auth-ws-1", Provider: "codex"}
req := cliproxyexecutor.Request{
Model: "gpt-5-codex",
Payload: []byte(`{"prompt_cache_key":"cache-ws-1","client_metadata":{"x-codex-installation-id":"install-ws-1"}}`),
}
body, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`))
body, identityState := applyCodexIdentityConfuseBody(cfg, auth, req.Payload, body)
if identityState.promptCacheKey != "" {
headers.Set("Conversation_id", identityState.promptCacheKey)
}
ctx := contextWithGinHeaders(map[string]string{
"X-Codex-Turn-Metadata": `{"prompt_cache_key":"cache-ws-1"}`,
"X-Client-Request-Id": "client-request-1",
})
headers = applyCodexWebsocketHeaders(ctx, headers, auth, "oauth-token", cfg)
applyCodexIdentityConfuseHeaders(headers, identityState)
expectedPromptCacheKey := codexIdentityConfuseUUID("auth-ws-1", "prompt-cache", "cache-ws-1")
if gotKey := gjson.GetBytes(body, "prompt_cache_key").String(); gotKey != expectedPromptCacheKey {
t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedPromptCacheKey)
}
if gotSession := headerValueCaseInsensitive(headers, "session_id"); gotSession != "" {
t.Fatalf("session_id = %q, want empty", gotSession)
}
if gotRequestID := headers.Get("X-Client-Request-Id"); gotRequestID != expectedPromptCacheKey {
t.Fatalf("X-Client-Request-Id = %q, want %q", gotRequestID, expectedPromptCacheKey)
}
if gotThreadID := headers.Get("Thread-Id"); gotThreadID != expectedPromptCacheKey {
t.Fatalf("Thread-Id = %q, want %q", gotThreadID, expectedPromptCacheKey)
}
if gotWindowID := headers.Get("X-Codex-Window-Id"); gotWindowID != expectedPromptCacheKey+":0" {
t.Fatalf("X-Codex-Window-Id = %q, want %q", gotWindowID, expectedPromptCacheKey+":0")
}
if gotMetadata := headers.Get("X-Codex-Turn-Metadata"); gotMetadata != `{"prompt_cache_key":"`+expectedPromptCacheKey+`"}` {
t.Fatalf("X-Codex-Turn-Metadata = %s", gotMetadata)
}
expectedInstallationID := codexIdentityConfuseUUID("auth-ws-1", "installation", "install-ws-1")
if gotInstallationID := gjson.GetBytes(body, "client_metadata.x-codex-installation-id").String(); gotInstallationID != expectedInstallationID {
t.Fatalf("installation id = %q, want %q", gotInstallationID, expectedInstallationID)
}
}
func TestCodexIdentityConfuseResponsePayloadHidesUpstreamAndRestoresClient(t *testing.T) {
state := codexIdentityConfuseState{
originalPromptCacheKey: "cache-ws-1",
promptCacheKey: codexIdentityConfuseUUID("auth-ws-1", "prompt-cache", "cache-ws-1"),
}
rawPayload := []byte(`{"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1"},"prompt_cache_key":"cache-ws-1"}`)
upstreamPayload := applyCodexIdentityConfuseResponsePayload(rawPayload, state)
if bytes.Contains(upstreamPayload, []byte(`cache-ws-1`)) {
t.Fatalf("upstream payload still contains original prompt_cache_key: %s", string(upstreamPayload))
}
if !bytes.Contains(upstreamPayload, []byte(state.promptCacheKey)) {
t.Fatalf("upstream payload missing confused prompt_cache_key: %s", string(upstreamPayload))
}
clientPayload := applyCodexIdentityExposeResponsePayload(upstreamPayload, state)
if bytes.Contains(clientPayload, []byte(state.promptCacheKey)) {
t.Fatalf("client payload still contains confused prompt_cache_key: %s", string(clientPayload))
}
if !bytes.Contains(clientPayload, []byte(`cache-ws-1`)) {
t.Fatalf("client payload missing original prompt_cache_key: %s", string(clientPayload))
}
rawSSE := []byte(`data: {"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1"}}`)
upstreamSSE := applyCodexIdentityConfuseResponsePayload(rawSSE, state)
if bytes.Contains(upstreamSSE, []byte(`cache-ws-1`)) {
t.Fatalf("upstream SSE still contains original prompt_cache_key: %s", string(upstreamSSE))
}
clientSSE := applyCodexIdentityExposeResponsePayload(upstreamSSE, state)
if !bytes.Contains(clientSSE, []byte(`cache-ws-1`)) || bytes.Contains(clientSSE, []byte(state.promptCacheKey)) {
t.Fatalf("client SSE prompt_cache_key was not restored: %s", string(clientSSE))
}
}
func TestApplyCodexWebsocketHeadersUsesCanonicalAccountHeader(t *testing.T) {
auth := &cliproxyauth.Auth{Provider: "codex", Metadata: map[string]any{"account_id": "acct-1"}}