From 8db0ab44adc88e6aa159bc2d1fc34e8564a7d8bb Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:13:55 +0900 Subject: [PATCH 1/2] fix(auth): retain stored sidecar auth for caller-auth Chat --- src/server/chat-completions.ts | 8 +-- .../bearer-admission-routed-provider.test.ts | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/server/chat-completions.ts b/src/server/chat-completions.ts index 0e06f8281a..8ee5f52a49 100644 --- a/src/server/chat-completions.ts +++ b/src/server/chat-completions.ts @@ -258,9 +258,9 @@ async function handleChatCompletionsWithBudget( const value = req.headers.get(name); if (value) headers.set(name, value); } - // Never enrich a caller-auth transport with a credential from another domain. - // Later shadow/thread rewrites strip credentials at the actual Responses boundary. - if (!callerAuthorizationRoute) { + // A noncanonical caller-auth route can use stored main auth only through a sidecar snapshot. + // Later shadow/thread rewrites strip primary credentials at the actual Responses boundary. + if (!callerAuthorizationRoute || (settledRoute && !isCanonicalOpenAiForwardProvider(settledRoute.provider))) { // This enrichment is optional for routed/non-main providers. If native main // is fenced, omit it and let auth-context reject only a final physical-main // selection while healthy pool/provider routes continue. @@ -271,7 +271,7 @@ async function handleChatCompletionsWithBudget( if (token) { const mainHeaders = new Headers({ authorization: `Bearer ${token.accessToken}`, "chatgpt-account-id": token.chatgptAccountId }); openAiSidecarAuth ??= captureExplicitOpenAiCallerAuth(mainHeaders, config); - if (!routeMayChangeCredentialDomain) { + if (!callerAuthorizationRoute && !routeMayChangeCredentialDomain) { headers.set("authorization", `Bearer ${token.accessToken}`); headers.set("chatgpt-account-id", token.chatgptAccountId); } diff --git a/tests/codex-integration/bearer-admission-routed-provider.test.ts b/tests/codex-integration/bearer-admission-routed-provider.test.ts index b07b04c372..859709a122 100644 --- a/tests/codex-integration/bearer-admission-routed-provider.test.ts +++ b/tests/codex-integration/bearer-admission-routed-provider.test.ts @@ -19,6 +19,7 @@ import type { OcxConfig } from "../../src/types"; import { ownedServiceHomeInspection } from "../helpers/owned-service-home-inspection"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { fakeChatGptJwt } from "../helpers/fake-chatgpt-jwt"; +import { resetVisionDescriptionCache } from "../../src/vision"; /** * Issue #2132: bearer admission must not require a stored ChatGPT credential. @@ -151,6 +152,7 @@ async function withCursorCaptureServer( } beforeEach(() => { + resetVisionDescriptionCache(); clearComboTargetCooldowns(); resetSubagentModelFallbackStateForTests(); delete process.env.OPENCODEX_CURSOR_TEST_TOKEN; @@ -186,6 +188,7 @@ beforeEach(() => { }); afterEach(() => { + resetVisionDescriptionCache(); closeRequestHistoryIndex(); clearComboTargetCooldowns(); resetSubagentModelFallbackStateForTests(); @@ -429,6 +432,60 @@ describe("bearer admission is not reused as a Cursor upstream credential", () => }); }); + test.each(["owned", "fenced"])("Chat Cursor keeps stored vision auth off its primary wire (%s)", async ownership => { + await withCursorCaptureServer(async (baseUrl, capturedAuth) => { + const config = cursorForwardConfig(baseUrl); + config.providers.cursorcustom!.noVisionModels = ["auto"]; + config.providers.openai = { + adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", codexAccountMode: "direct", + }; + config.visionSidecar = { enabled: true, backend: "openai", model: "gpt-5.4-mini" }; + saveConfig(config); + const stored = fakeChatGptJwt({ chatgpt_account_id: "stored_main_acc", exp: Math.floor(Date.now() / 1000) + 3600 }); + writeFileSync(join(codexHome, "auth.json"), JSON.stringify({ + tokens: { access_token: stored, account_id: "stored_main_acc" }, + })); + const sidecar: Array<{ authorization: string | null; account: string | null; claimed: boolean }> = []; + globalThis.fetch = (async (input, init) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + if (url.hostname === "chatgpt.com") { + const headers = new Headers(input instanceof Request ? input.headers : init?.headers); + sidecar.push({ authorization: headers.get("authorization"), account: headers.get("chatgpt-account-id"), + claimed: getNativeMainProfileRequestCount() > 0 }); + return new Response(`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "A red square." })}\n\ndata: [DONE]\n\n`, { + headers: { "content-type": "text/event-stream" }, + }); + } + return originalFetch(input, init); + }) as typeof fetch; + const server = ownership === "owned" ? await startOwnedServer() : startServer(0, { + inspectNativeCodexOwnership: () => ({ ownership: "foreign", reason: "fixture owned by another service" }), + }); + try { + if (ownership === "fenced") expect(await waitForNativeMainStartupGate()).toMatchObject({ status: "blocked" }); + const response = await originalFetch(new URL("/v1/chat/completions", server.url), { + method: "POST", + headers: { "content-type": "application/json", "x-opencodex-api-key": ADMISSION_SECRET, + authorization: "Bearer cursor-upstream-token" }, + body: JSON.stringify({ model: "cursorcustom/auto", stream: false, messages: [{ role: "user", content: [ + { type: "text", text: "Describe this image" }, + { type: "image_url", image_url: { url: "data:image/png;base64,aGVsbG8taW1hZ2UtYnl0ZXM=" } }, + ] }] }), + }); + await response.text(); + // The capture-only Cursor fixture ends without a completion frame. + expect(response.status).toBe(502); + expect(sidecar).toEqual(ownership === "owned" + ? [{ authorization: `Bearer ${stored}`, account: "stored_main_acc", claimed: true }] : []); + expect(capturedAuth).toEqual(["Bearer cursor-upstream-token"]); + } finally { + await server.stop(true); + } + expect(getNativeMainProfileRequestCount()).toBe(0); + }); + }); + test("Chat never falls back from missing Cursor auth to stored main auth", async () => { await withCursorCaptureServer(async (baseUrl, capturedAuth) => { saveConfig(cursorForwardConfig(baseUrl)); From 4a201df671bf69c83be1c8da736dd33ec245e214 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:59:10 +0900 Subject: [PATCH 2/2] test(auth): avoid sidecar model migration in credential fixture --- .../codex-integration/bearer-admission-routed-provider.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/codex-integration/bearer-admission-routed-provider.test.ts b/tests/codex-integration/bearer-admission-routed-provider.test.ts index 859709a122..5b87318626 100644 --- a/tests/codex-integration/bearer-admission-routed-provider.test.ts +++ b/tests/codex-integration/bearer-admission-routed-provider.test.ts @@ -440,7 +440,8 @@ describe("bearer admission is not reused as a Cursor upstream credential", () => adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", codexAccountMode: "direct", }; - config.visionSidecar = { enabled: true, backend: "openai", model: "gpt-5.4-mini" }; + // Keep this auth fixture independent of the legacy sidecar model migration. + config.visionSidecar = { enabled: true, backend: "openai", model: "gpt-5.6-luna" }; saveConfig(config); const stored = fakeChatGptJwt({ chatgpt_account_id: "stored_main_acc", exp: Math.floor(Date.now() / 1000) + 3600 }); writeFileSync(join(codexHome, "auth.json"), JSON.stringify({