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 <noreply@anthropic.com>
This commit is contained in:
Devin FoleyandClaude Opus 5.5 authored and GitHub committed 2026-11-04 17:27:48 -08:00
1 parent 482ec0aea9
commit 6fa54d5577
2 files changed
+41

No files matched your search

@@ -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");
@@ -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)