fix(codex): classify app-server auth refresh failures · openclaw/openclaw@3b8ac38
vincentkoc
·
2026-05-14
·
via Recent Commits to openclaw:main
File tree
extensions/codex/src/app-server
| Original file line number | Diff line number | Diff line change |
|---|
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
12 | 12 | ### Fixes |
13 | 13 | |
14 | 14 | - iOS/chat: resize PhotosPicker image attachments to capped JPEGs before staging and sending, stripping source metadata and keeping oversized camera photos under the chat upload budget. Fixes #68524. Thanks @BunsDev. |
| 15 | +- Codex harness: classify native app-server token-refresh logout and relogin failures as authentication refresh errors, so users get re-authentication guidance instead of a raw runtime failure. |
15 | 16 | - Codex startup: treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair, so Anthropic-primary configs can still switch to OpenAI/Codex cleanly. |
16 | 17 | - Agents: preserve source-reply delivery metadata when merging tool-returned media into the final reply, keeping message-tool-only replies deliverable and mirrored. Thanks @pashpashpash and @vincentkoc. |
17 | 18 | - macOS/companion: require system TLS trust before pinning a first-use direct `wss://` gateway certificate and honor `gateway.remote.tlsFingerprint` as the explicit pin for remote node-mode sessions, so fresh endpoints fail closed when macOS cannot trust the certificate unless configured out of band. Fixes #50642. Thanks @BunsDev. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -127,6 +127,42 @@ describe("CodexAppServerClient", () => {
|
127 | 127 | await expect(request).rejects.toHaveProperty("message", "Method not found"); |
128 | 128 | }); |
129 | 129 | |
| 130 | +it("surfaces relogin details from Codex app-server RPC errors", async () => { |
| 131 | +const harness = createClientHarness(); |
| 132 | +clients.push(harness.client); |
| 133 | + |
| 134 | +const request = harness.client.request("thread/start", {}); |
| 135 | +const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number }; |
| 136 | +harness.send({ |
| 137 | +id: outbound.id, |
| 138 | +error: { |
| 139 | +code: -32602, |
| 140 | +message: "failed to load configuration", |
| 141 | +data: { |
| 142 | +reason: "cloudRequirements", |
| 143 | +errorCode: "Auth", |
| 144 | +action: "relogin", |
| 145 | +statusCode: 401, |
| 146 | +detail: |
| 147 | +"Your authentication session could not be refreshed automatically. Please log out and sign in again.", |
| 148 | +}, |
| 149 | +}, |
| 150 | +}); |
| 151 | + |
| 152 | +await expect(request).rejects.toHaveProperty( |
| 153 | +"message", |
| 154 | +"failed to load configuration: Your authentication session could not be refreshed automatically. Please log out and sign in again.", |
| 155 | +); |
| 156 | +await expect(request).rejects.toHaveProperty("data", { |
| 157 | +reason: "cloudRequirements", |
| 158 | +errorCode: "Auth", |
| 159 | +action: "relogin", |
| 160 | +statusCode: 401, |
| 161 | +detail: |
| 162 | +"Your authentication session could not be refreshed automatically. Please log out and sign in again.", |
| 163 | +}); |
| 164 | +}); |
| 165 | + |
130 | 166 | it("rejects timed-out requests and ignores late responses", async () => { |
131 | 167 | vi.useFakeTimers(); |
132 | 168 | const harness = createClientHarness(); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -42,13 +42,39 @@ export class CodexAppServerRpcError extends Error {
|
42 | 42 | readonly data?: JsonValue; |
43 | 43 | |
44 | 44 | constructor(error: { code?: number; message: string; data?: JsonValue }, method: string) { |
45 | | -super(error.message || `${method} failed`); |
| 45 | +super(formatCodexAppServerRpcErrorMessage(error, method)); |
46 | 46 | this.name = "CodexAppServerRpcError"; |
47 | 47 | this.code = error.code; |
48 | 48 | this.data = error.data; |
49 | 49 | } |
50 | 50 | } |
51 | 51 | |
| 52 | +function formatCodexAppServerRpcErrorMessage( |
| 53 | +error: { message: string; data?: JsonValue }, |
| 54 | +method: string, |
| 55 | +): string { |
| 56 | +const message = error.message || `${method} failed`; |
| 57 | +const detail = readCodexAppServerRpcReloginDetail(error.data); |
| 58 | +return detail && !message.includes(detail) ? `${message}: ${detail}` : message; |
| 59 | +} |
| 60 | + |
| 61 | +function readCodexAppServerRpcReloginDetail(data: JsonValue | undefined): string | undefined { |
| 62 | +const record = isJsonObject(data) ? data : undefined; |
| 63 | +const nested = isJsonObject(record?.error) ? record.error : record; |
| 64 | +if (!nested) { |
| 65 | +return undefined; |
| 66 | +} |
| 67 | +const isRelogin = |
| 68 | +nested.action === "relogin" || |
| 69 | +(nested.reason === "cloudRequirements" && nested.errorCode === "Auth"); |
| 70 | +const detail = typeof nested.detail === "string" ? nested.detail.trim() : ""; |
| 71 | +return isRelogin && detail ? detail : undefined; |
| 72 | +} |
| 73 | + |
| 74 | +function isJsonObject(value: unknown): value is { [key: string]: JsonValue } { |
| 75 | +return Boolean(value && typeof value === "object" && !Array.isArray(value)); |
| 76 | +} |
| 77 | + |
52 | 78 | export function isCodexAppServerConnectionClosedError(error: unknown): boolean { |
53 | 79 | if (!(error instanceof Error)) { |
54 | 80 | return false; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -12,6 +12,15 @@ export type OAuthRefreshFailureReason =
|
12 | 12 | const OAUTH_REFRESH_FAILURE_PROVIDER_RE = /OAuth token refresh failed for ([^:]+):/i; |
13 | 13 | const SAFE_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]*$/; |
14 | 14 | |
| 15 | +function isOAuthRefreshFailureMessage(message: string): boolean { |
| 16 | +const lower = message.toLowerCase(); |
| 17 | +return ( |
| 18 | +lower.includes("oauth token refresh failed") || |
| 19 | +lower.includes("access token could not be refreshed") || |
| 20 | +lower.includes("authentication session could not be refreshed automatically") |
| 21 | +); |
| 22 | +} |
| 23 | + |
15 | 24 | function extractOAuthRefreshFailureProvider(message: string): string | null { |
16 | 25 | const provider = message.match(OAUTH_REFRESH_FAILURE_PROVIDER_RE)?.[1]?.trim(); |
17 | 26 | return provider && provider.length > 0 ? provider : null; |
@@ -49,7 +58,7 @@ export function classifyOAuthRefreshFailure(message: string): {
|
49 | 58 | provider: string | null; |
50 | 59 | reason: OAuthRefreshFailureReason | null; |
51 | 60 | } | null { |
52 | | -if (!/oauth token refresh failed/i.test(message)) { |
| 61 | +if (!isOAuthRefreshFailureMessage(message)) { |
53 | 62 | return null; |
54 | 63 | } |
55 | 64 | return { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -281,6 +281,15 @@ describe("formatAssistantErrorText", () => {
|
281 | 281 | ); |
282 | 282 | }); |
283 | 283 | |
| 284 | +it("returns an explicit re-authentication message for Codex app-server refresh failures", () => { |
| 285 | +const msg = makeAssistantError( |
| 286 | +"Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.", |
| 287 | +); |
| 288 | +expect(formatAssistantErrorText(msg)).toBe( |
| 289 | +"Authentication refresh failed. Re-authenticate this provider and try again.", |
| 290 | +); |
| 291 | +}); |
| 292 | + |
284 | 293 | it("returns a contention-specific message for OAuth refresh lock timeouts", () => { |
285 | 294 | const msg = makeAssistantError("file lock timeout for /tmp/openclaw-oauth-refresh.lock"); |
286 | 295 | expect(formatAssistantErrorText(msg)).toBe( |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1447,6 +1447,16 @@ describe("classifyProviderRuntimeFailureKind", () => {
|
1447 | 1447 | "OAuth token refresh failed for openai-codex: invalid_grant. Please try again or re-authenticate.", |
1448 | 1448 | ), |
1449 | 1449 | ).toBe("auth_refresh"); |
| 1450 | +expect( |
| 1451 | +classifyProviderRuntimeFailureKind( |
| 1452 | +"Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.", |
| 1453 | +), |
| 1454 | +).toBe("auth_refresh"); |
| 1455 | +expect( |
| 1456 | +classifyProviderRuntimeFailureKind( |
| 1457 | +"Your authentication session could not be refreshed automatically. Please log out and sign in again.", |
| 1458 | +), |
| 1459 | +).toBe("auth_refresh"); |
1450 | 1460 | }); |
1451 | 1461 | |
1452 | 1462 | it("classifies OAuth refresh timeouts and lock contention distinctly", () => { |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。