From 0b02b5abd21131cb3d9a79cf773bcfd6cb1d0f05 Mon Sep 17 00:00:00 2001 From: Eva Date: Fri, 10 Apr 2026 19:24:21 +0700 Subject: [PATCH] openai-codex: gate scope failures to codex --- .../pi-embedded-error-observation.test.ts | 4 ++-- src/agents/pi-embedded-error-observation.ts | 13 ++++++++++--- ...ed-helpers.formatassistanterrortext.test.ts | 11 ++++++++++- ...edded-helpers.isbillingerrormessage.test.ts | 18 +++++++++++++++--- src/agents/pi-embedded-helpers/errors.ts | 16 ++++++++++++++-- ...pi-embedded-subscribe.handlers.lifecycle.ts | 8 ++++++-- 6 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/agents/pi-embedded-error-observation.test.ts b/src/agents/pi-embedded-error-observation.test.ts index e70c5e21fcb..3b1d32d5875 100644 --- a/src/agents/pi-embedded-error-observation.test.ts +++ b/src/agents/pi-embedded-error-observation.test.ts @@ -180,14 +180,14 @@ describe("buildApiErrorObservationFields", () => { expect(observed.rawErrorPreview).toContain("custom"); }); - it("records runtime failure kind for missing-scope auth payloads", () => { + it("keeps provider-less missing-scope auth payloads out of the codex-specific scope lane", () => { const observed = buildApiErrorObservationFields( '401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}', ); expect(observed).toMatchObject({ httpCode: "401", - providerRuntimeFailureKind: "auth_scope", + providerRuntimeFailureKind: "unknown", }); }); }); diff --git a/src/agents/pi-embedded-error-observation.ts b/src/agents/pi-embedded-error-observation.ts index a3b14f4c4e3..73feaba066a 100644 --- a/src/agents/pi-embedded-error-observation.ts +++ b/src/agents/pi-embedded-error-observation.ts @@ -104,7 +104,10 @@ function buildObservationFingerprint(params: { return getApiErrorPayloadFingerprint(params.raw); } -export function buildApiErrorObservationFields(rawError?: string): { +export function buildApiErrorObservationFields( + rawError?: string, + opts?: { provider?: string }, +): { rawErrorPreview?: string; rawErrorHash?: string; rawErrorFingerprint?: string; @@ -146,6 +149,7 @@ export function buildApiErrorObservationFields(rawError?: string): { providerRuntimeFailureKind: classifyProviderRuntimeFailureKind({ status: parsed?.httpCode ? Number(parsed.httpCode) : undefined, message: trimmed, + provider: opts?.provider, }), providerErrorType: parsed?.type, providerErrorMessagePreview: truncateForObservation( @@ -159,7 +163,10 @@ export function buildApiErrorObservationFields(rawError?: string): { } } -export function buildTextObservationFields(text?: string): { +export function buildTextObservationFields( + text?: string, + opts?: { provider?: string }, +): { textPreview?: string; textHash?: string; textFingerprint?: string; @@ -169,7 +176,7 @@ export function buildTextObservationFields(text?: string): { providerErrorMessagePreview?: string; requestIdHash?: string; } { - const observed = buildApiErrorObservationFields(text); + const observed = buildApiErrorObservationFields(text, opts); return { textPreview: observed.rawErrorPreview, textHash: observed.rawErrorHash, diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index 89c8331a0d2..a09c2702d90 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -228,11 +228,20 @@ describe("formatAssistantErrorText", () => { const msg = makeAssistantError( '401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write model.request"}}', ); - expect(formatAssistantErrorText(msg)).toBe( + expect(formatAssistantErrorText(msg, { provider: "openai-codex" })).toBe( "Authentication is missing the required OpenAI Codex scopes. Re-run OpenAI/Codex login and try again.", ); }); + it("does not misdiagnose non-Codex permission errors as missing-scope failures", () => { + const msg = makeAssistantError( + '401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write model.request"}}', + ); + expect(formatAssistantErrorText(msg, { provider: "openai" })).not.toContain( + "required OpenAI Codex scopes", + ); + }); + it("returns an HTML-403 auth message for HTML provider auth failures", () => { const msg = makeAssistantError("403 Access denied"); expect(formatAssistantErrorText(msg)).toBe( diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 184e3530616..1bdbab37dc6 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -1106,12 +1106,24 @@ describe("classifyFailoverReason", () => { describe("classifyProviderRuntimeFailureKind", () => { it("classifies missing scope failures", () => { expect( - classifyProviderRuntimeFailureKind( - '401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}', - ), + classifyProviderRuntimeFailureKind({ + provider: "openai-codex", + message: + '401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}', + }), ).toBe("auth_scope"); }); + it("does not classify non-Codex permission errors as missing scope failures", () => { + expect( + classifyProviderRuntimeFailureKind({ + provider: "openai", + message: + '401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}', + }), + ).not.toBe("auth_scope"); + }); + it("classifies OAuth refresh failures", () => { expect( classifyProviderRuntimeFailureKind( diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 861c71de7ae..9b91a903aa4 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -502,10 +502,22 @@ function isHtmlErrorResponse(raw: string, status?: number): boolean { return HTML_BODY_RE.test(rest) && HTML_CLOSE_RE.test(rest); } -function isAuthScopeErrorMessage(raw: string, status?: number): boolean { +function isOpenAICodexScopeContext(raw: string, provider?: string): boolean { + const normalizedProvider = normalizeLowercaseStringOrEmpty(provider); + return ( + normalizedProvider === "openai-codex" || + /\bopenai\s+codex\b/i.test(raw) || + /\bcodex\b.*\bscopes?\b/i.test(raw) + ); +} + +function isAuthScopeErrorMessage(raw: string, status?: number, provider?: string): boolean { if (!raw) { return false; } + if (!isOpenAICodexScopeContext(raw, provider)) { + return false; + } const inferred = typeof status === "number" && Number.isFinite(status) ? status @@ -916,7 +928,7 @@ export function classifyProviderRuntimeFailureKind( if (message && classifyOAuthRefreshFailure(message)) { return "auth_refresh"; } - if (message && isAuthScopeErrorMessage(message, status)) { + if (message && isAuthScopeErrorMessage(message, status, normalizedSignal.provider)) { return "auth_scope"; } if (message && status === 403 && isHtmlErrorResponse(message, status)) { diff --git a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts b/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts index 2f046c78289..8ce525b337d 100644 --- a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts +++ b/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts @@ -60,9 +60,13 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext): void | Promise< provider: lastAssistant.provider, }); const errorText = (friendlyError || lastAssistant.errorMessage || "LLM request failed.").trim(); - const observedError = buildApiErrorObservationFields(rawError); + const observedError = buildApiErrorObservationFields(rawError, { + provider: lastAssistant.provider, + }); const safeErrorText = - buildTextObservationFields(errorText).textPreview ?? "LLM request failed."; + buildTextObservationFields(errorText, { + provider: lastAssistant.provider, + }).textPreview ?? "LLM request failed."; lifecycleErrorText = safeErrorText; const safeRunId = sanitizeForConsole(ctx.params.runId) ?? "-"; const safeModel = sanitizeForConsole(lastAssistant.model) ?? "unknown";