From 6fa54d55779295d90c455389cebdd3da96607d20 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Wed, 4 Nov 2026 17:27:48 -0800 Subject: [PATCH] fix(codex-local): route typed ACP credential rejections to sign-in recovery (#19146) Classify a typed Codex ACP `access` failure as `acpx_auth_required` when the provider text reports `401 Unauthorized`, `invalid_api_key`, or an incorrect/invalid API key. Other `access` failures keep `acpx_turn_failed`. Co-Authored-By: Claude Opus 5.5 --- .../codex-local/src/server/acp.quota.test.ts | 34 +++++++++++++++++++ .../adapters/codex-local/src/server/acp.ts | 7 ++++ 2 files changed, 41 insertions(+) diff --git a/packages/adapters/codex-local/src/server/acp.quota.test.ts b/packages/adapters/codex-local/src/server/acp.quota.test.ts index fe371d95f1f..4484dff17cf 100644 --- a/packages/adapters/codex-local/src/server/acp.quota.test.ts +++ b/packages/adapters/codex-local/src/server/acp.quota.test.ts @@ -160,6 +160,40 @@ it("keeps the historical generic terminal-service error unclassified", () => { }, now)).toBeNull(); }); +it.each([ + "unexpected status 401 Unauthorized: Incorrect API key provided: sk-test...abcd., auth error code: invalid_api_key", + "Incorrect API key provided", + "Invalid API key", + "auth error code: invalid_api_key", +])("routes a typed Codex credential rejection to sign-in recovery: %s", (title) => { + expect(classifyCodexTerminalSessionFailure({ category: "access", title }, now)) + .toEqual({ errorCode: "acpx_auth_required" }); + expect(classifyCodexTerminalSessionFailure({ category: "access", title: "Codex turn failed", details: title }, now)) + .toEqual({ errorCode: "acpx_auth_required" }); +}); + +it.each([ + ["ACP agent reported a terminal access failure.", "access"], + ["403 Forbidden: project does not have access to model gpt-5", "access"], + ["Permission denied for tool shell", "access"], + ["Incorrect API key provided", "request"], + ["Incorrect API key provided", "service"], + ["Incorrect API key provided", "connection"], +])("keeps other typed failures out of sign-in recovery: %s (%s)", (title, category) => { + expect(classifyCodexTerminalSessionFailure({ category, title }, now)).toBeNull(); +}); + +it("requires sign-in for a typed Codex credential rejection instead of a turn failure", async () => { + const title = "unexpected status 401 Unauthorized: Incorrect API key provided, auth error code: invalid_api_key"; + const { result } = await executeFailure(title, "access"); + expect(result).toMatchObject({ + exitCode: 1, + errorCode: "acpx_auth_required", + resultJson: { terminalSessionFailure: { category: "access" } }, + }); + expect(result.errorFamily).toBeUndefined(); +}); + it("arms the bounded transient retry for a typed Codex model capacity failure", async () => { const title = "Selected model is at capacity. Please try a different model."; const { result } = await executeFailure(title, "service"); diff --git a/packages/adapters/codex-local/src/server/acp.ts b/packages/adapters/codex-local/src/server/acp.ts index bb6efe3d985..814694b1637 100644 --- a/packages/adapters/codex-local/src/server/acp.ts +++ b/packages/adapters/codex-local/src/server/acp.ts @@ -269,10 +269,17 @@ async function prepareCodexRemoteManagedHome( const CODEX_MODEL_CAPACITY_RE = /\b(?:selected model is at capacity|model is at capacity|at capacity for this model|serverOverloaded|server overloaded)\b/i; +const CODEX_PROVIDER_AUTH_RE = + /\b(?:401 unauthorized|invalid_api_key|(?:incorrect|invalid) api key)\b/i; + export function classifyCodexTerminalSessionFailure( failure: AcpxTerminalSessionFailure, now: Date, ): AcpxTerminalFailureClassification | null { + if (failure.category === "access") { + const text = [failure.title, failure.details].filter(Boolean).join("\n"); + return CODEX_PROVIDER_AUTH_RE.test(text) ? { errorCode: "acpx_auth_required" } : null; + } if (failure.category === "service") { const text = [failure.title, failure.details].filter(Boolean).join("\n"); return CODEX_MODEL_CAPACITY_RE.test(text)