diff --git a/src/server/responses/compact.ts b/src/server/responses/compact.ts index b14800fc7b..696cf42944 100644 --- a/src/server/responses/compact.ts +++ b/src/server/responses/compact.ts @@ -178,8 +178,22 @@ function pruneCompactHandoffRoutes(now: number): void { } } -function rememberCompactHandoffRoute(req: Request, model: string, now = Date.now()): void { - const key = sessionLaneIdFromRequest(req.headers); +function compactHandoffRouteKey(req: Request, admission?: DataPlaneAdmission): string | null { + const lane = sessionLaneIdFromRequest(req.headers); + // A loopback request has no authenticated principal to bind this cross-request + // state to. Fail closed rather than treating a caller-controlled lane as identity. + if (!lane || !admission || admission.kind === "loopback") return null; + const principal = admission.kind === "configured" ? `configured:${admission.keyId}` : "environment"; + return `${principal}\u0000${lane}`; +} + +function rememberCompactHandoffRoute( + req: Request, + admission: DataPlaneAdmission | undefined, + model: string, + now = Date.now(), +): void { + const key = compactHandoffRouteKey(req, admission); if (!key || model.length > COMPACT_HANDOFF_MODEL_MAX_LENGTH) return; pruneCompactHandoffRoutes(now); compactHandoffRoutes.delete(key); @@ -187,13 +201,18 @@ function rememberCompactHandoffRoute(req: Request, model: string, now = Date.now pruneCompactHandoffRoutes(now); } -function forgetCompactHandoffRoute(req: Request): void { - const key = sessionLaneIdFromRequest(req.headers); +function forgetCompactHandoffRoute(req: Request, admission?: DataPlaneAdmission): void { + const key = compactHandoffRouteKey(req, admission); if (key) compactHandoffRoutes.delete(key); } -function compactHandoffRoute(req: Request, previousModel: string, now = Date.now()): string | null { - const key = sessionLaneIdFromRequest(req.headers); +function compactHandoffRoute( + req: Request, + admission: DataPlaneAdmission | undefined, + previousModel: string, + now = Date.now(), +): string | null { + const key = compactHandoffRouteKey(req, admission); if (!key) return null; pruneCompactHandoffRoutes(now); const entry = compactHandoffRoutes.get(key); @@ -953,9 +972,9 @@ export async function handleResponsesCompact( // synthetic buffer errors are not upstream bodies and stay uninspected. if (buffered.ok) { inspectResponseLogJson(logCtx, await buffered.clone().text()); - forgetCompactHandoffRoute(req); + forgetCompactHandoffRoute(req, admission); } else if (quotaFailure && !storedPool401ReplayAttempted) { - const fallbackModel = compactHandoffRoute(req, raw.model); + const fallbackModel = compactHandoffRoute(req, admission, raw.model); if (fallbackModel && !req.signal.aborted) { const fallbackReq = new Request(req.url, { method: "POST", @@ -1075,7 +1094,7 @@ export async function handleResponsesCompact( const result = new Response(JSON.stringify({ output: compactionItems }), { headers: { "Content-Type": "application/json" }, }); - rememberCompactHandoffRoute(req, raw.model); + rememberCompactHandoffRoute(req, admission, raw.model); return result; } const encrypted = compactionItems[0]!.encrypted_content; @@ -1086,6 +1105,6 @@ export async function handleResponsesCompact( } const summary = decoded; const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary); - rememberCompactHandoffRoute(req, raw.model); + rememberCompactHandoffRoute(req, admission, raw.model); return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } }); } diff --git a/tests/responses-compaction-routing.test.ts b/tests/responses-compaction-routing.test.ts index d97154c661..97f00aa7b7 100644 --- a/tests/responses-compaction-routing.test.ts +++ b/tests/responses-compaction-routing.test.ts @@ -1027,6 +1027,7 @@ describe("compact alternate-account attempt (#913)", () => { models: ["gpt-5.6-sol"], }; const headers = { "x-codex-parent-thread-id": "compact-routed-handoff-thread" }; + const admission = { kind: "configured", keyId: "compact-client", source: "dedicated" } as const; const calls: Array<{ model: string; nativeCompact: boolean }> = []; globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { const url = typeof input === "string" @@ -1053,6 +1054,8 @@ describe("compact alternate-account attempt (#913)", () => { ), config, { model: "", provider: "" }, + undefined, + admission, ); expect(manual.status).toBe(200); expect(calls).toEqual([{ model: "deepseek-v4-flash", nativeCompact: false }]); @@ -1066,6 +1069,8 @@ describe("compact alternate-account attempt (#913)", () => { ), config, { model: "", provider: "" }, + undefined, + admission, ); expect(unrelated.status).toBe(502); expect(calls.length).toBeGreaterThan(0); @@ -1081,6 +1086,8 @@ describe("compact alternate-account attempt (#913)", () => { ), config, logCtx, + undefined, + admission, ); expect(automatic.status).toBe(200); @@ -1092,6 +1099,36 @@ describe("compact alternate-account attempt (#913)", () => { expect(calls.slice(0, -1).every(call => ( call.model === "gpt-5.6-sol" && call.nativeCompact ))).toBe(true); + + calls.length = 0; + const otherPrincipal = await handleResponsesCompact( + compactionRequest( + baseCompactionBody({ model: "openai-apikey/gpt-5.6-sol" }), + undefined, + headers, + ), + config, + { model: "", provider: "" }, + undefined, + { kind: "configured", keyId: "different-client", source: "dedicated" }, + ); + expect(otherPrincipal.status).toBe(502); + expect(calls.every(call => call.model === "gpt-5.6-sol" && call.nativeCompact)).toBe(true); + + calls.length = 0; + const unauthenticatedLoopback = await handleResponsesCompact( + compactionRequest( + baseCompactionBody({ model: "openai-apikey/gpt-5.6-sol" }), + undefined, + headers, + ), + config, + { model: "", provider: "" }, + undefined, + { kind: "loopback", source: "loopback" }, + ); + expect(unauthenticatedLoopback.status).toBe(502); + expect(calls.every(call => call.model === "gpt-5.6-sol" && call.nativeCompact)).toBe(true); }); });