Skip to content

Commit 0b02b5a

Browse files
Evasteipete
authored andcommitted
openai-codex: gate scope failures to codex
1 parent 8166d59 commit 0b02b5a

6 files changed

Lines changed: 57 additions & 13 deletions

src/agents/pi-embedded-error-observation.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,14 @@ describe("buildApiErrorObservationFields", () => {
180180
expect(observed.rawErrorPreview).toContain("custom");
181181
});
182182

183-
it("records runtime failure kind for missing-scope auth payloads", () => {
183+
it("keeps provider-less missing-scope auth payloads out of the codex-specific scope lane", () => {
184184
const observed = buildApiErrorObservationFields(
185185
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
186186
);
187187

188188
expect(observed).toMatchObject({
189189
httpCode: "401",
190-
providerRuntimeFailureKind: "auth_scope",
190+
providerRuntimeFailureKind: "unknown",
191191
});
192192
});
193193
});

src/agents/pi-embedded-error-observation.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ function buildObservationFingerprint(params: {
104104
return getApiErrorPayloadFingerprint(params.raw);
105105
}
106106

107-
export function buildApiErrorObservationFields(rawError?: string): {
107+
export function buildApiErrorObservationFields(
108+
rawError?: string,
109+
opts?: { provider?: string },
110+
): {
108111
rawErrorPreview?: string;
109112
rawErrorHash?: string;
110113
rawErrorFingerprint?: string;
@@ -146,6 +149,7 @@ export function buildApiErrorObservationFields(rawError?: string): {
146149
providerRuntimeFailureKind: classifyProviderRuntimeFailureKind({
147150
status: parsed?.httpCode ? Number(parsed.httpCode) : undefined,
148151
message: trimmed,
152+
provider: opts?.provider,
149153
}),
150154
providerErrorType: parsed?.type,
151155
providerErrorMessagePreview: truncateForObservation(
@@ -159,7 +163,10 @@ export function buildApiErrorObservationFields(rawError?: string): {
159163
}
160164
}
161165

162-
export function buildTextObservationFields(text?: string): {
166+
export function buildTextObservationFields(
167+
text?: string,
168+
opts?: { provider?: string },
169+
): {
163170
textPreview?: string;
164171
textHash?: string;
165172
textFingerprint?: string;
@@ -169,7 +176,7 @@ export function buildTextObservationFields(text?: string): {
169176
providerErrorMessagePreview?: string;
170177
requestIdHash?: string;
171178
} {
172-
const observed = buildApiErrorObservationFields(text);
179+
const observed = buildApiErrorObservationFields(text, opts);
173180
return {
174181
textPreview: observed.rawErrorPreview,
175182
textHash: observed.rawErrorHash,

src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,20 @@ describe("formatAssistantErrorText", () => {
228228
const msg = makeAssistantError(
229229
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write model.request"}}',
230230
);
231-
expect(formatAssistantErrorText(msg)).toBe(
231+
expect(formatAssistantErrorText(msg, { provider: "openai-codex" })).toBe(
232232
"Authentication is missing the required OpenAI Codex scopes. Re-run OpenAI/Codex login and try again.",
233233
);
234234
});
235235

236+
it("does not misdiagnose non-Codex permission errors as missing-scope failures", () => {
237+
const msg = makeAssistantError(
238+
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write model.request"}}',
239+
);
240+
expect(formatAssistantErrorText(msg, { provider: "openai" })).not.toContain(
241+
"required OpenAI Codex scopes",
242+
);
243+
});
244+
236245
it("returns an HTML-403 auth message for HTML provider auth failures", () => {
237246
const msg = makeAssistantError("403 <!DOCTYPE html><html><body>Access denied</body></html>");
238247
expect(formatAssistantErrorText(msg)).toBe(

src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,12 +1106,24 @@ describe("classifyFailoverReason", () => {
11061106
describe("classifyProviderRuntimeFailureKind", () => {
11071107
it("classifies missing scope failures", () => {
11081108
expect(
1109-
classifyProviderRuntimeFailureKind(
1110-
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
1111-
),
1109+
classifyProviderRuntimeFailureKind({
1110+
provider: "openai-codex",
1111+
message:
1112+
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
1113+
}),
11121114
).toBe("auth_scope");
11131115
});
11141116

1117+
it("does not classify non-Codex permission errors as missing scope failures", () => {
1118+
expect(
1119+
classifyProviderRuntimeFailureKind({
1120+
provider: "openai",
1121+
message:
1122+
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
1123+
}),
1124+
).not.toBe("auth_scope");
1125+
});
1126+
11151127
it("classifies OAuth refresh failures", () => {
11161128
expect(
11171129
classifyProviderRuntimeFailureKind(

src/agents/pi-embedded-helpers/errors.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,10 +502,22 @@ function isHtmlErrorResponse(raw: string, status?: number): boolean {
502502
return HTML_BODY_RE.test(rest) && HTML_CLOSE_RE.test(rest);
503503
}
504504

505-
function isAuthScopeErrorMessage(raw: string, status?: number): boolean {
505+
function isOpenAICodexScopeContext(raw: string, provider?: string): boolean {
506+
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
507+
return (
508+
normalizedProvider === "openai-codex" ||
509+
/\bopenai\s+codex\b/i.test(raw) ||
510+
/\bcodex\b.*\bscopes?\b/i.test(raw)
511+
);
512+
}
513+
514+
function isAuthScopeErrorMessage(raw: string, status?: number, provider?: string): boolean {
506515
if (!raw) {
507516
return false;
508517
}
518+
if (!isOpenAICodexScopeContext(raw, provider)) {
519+
return false;
520+
}
509521
const inferred =
510522
typeof status === "number" && Number.isFinite(status)
511523
? status
@@ -916,7 +928,7 @@ export function classifyProviderRuntimeFailureKind(
916928
if (message && classifyOAuthRefreshFailure(message)) {
917929
return "auth_refresh";
918930
}
919-
if (message && isAuthScopeErrorMessage(message, status)) {
931+
if (message && isAuthScopeErrorMessage(message, status, normalizedSignal.provider)) {
920932
return "auth_scope";
921933
}
922934
if (message && status === 403 && isHtmlErrorResponse(message, status)) {

src/agents/pi-embedded-subscribe.handlers.lifecycle.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext): void | Promise<
6060
provider: lastAssistant.provider,
6161
});
6262
const errorText = (friendlyError || lastAssistant.errorMessage || "LLM request failed.").trim();
63-
const observedError = buildApiErrorObservationFields(rawError);
63+
const observedError = buildApiErrorObservationFields(rawError, {
64+
provider: lastAssistant.provider,
65+
});
6466
const safeErrorText =
65-
buildTextObservationFields(errorText).textPreview ?? "LLM request failed.";
67+
buildTextObservationFields(errorText, {
68+
provider: lastAssistant.provider,
69+
}).textPreview ?? "LLM request failed.";
6670
lifecycleErrorText = safeErrorText;
6771
const safeRunId = sanitizeForConsole(ctx.params.runId) ?? "-";
6872
const safeModel = sanitizeForConsole(lastAssistant.model) ?? "unknown";

0 commit comments

Comments
 (0)