diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c5bf834196..7b330ecfb052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai - Canvas: return not found for malformed percent-encoded Canvas/A2UI/document asset paths and keep decoded parent traversal blocked before path normalization. - Agents: allow dot-dot-prefixed filenames such as `..note.txt` through sandbox FS bridge, remote sandbox reads, and apply_patch summaries without mistaking the name for parent traversal. - CLI/migrate: hide per-item source/plugin hints on non-conflicting Codex skill and plugin selection prompts, keeping the hint text reserved for rows that actually need attention. Thanks @sjf. +- Codex harness: treat high-confidence app-server OAuth refresh invalidation as a terminal auth-profile failure, stopping repeated raw token-refresh errors without turning entitlement or usage-limit payloads into re-auth prompts. - CLI/migrate: humanize Codex conflict-status messaging across the migrate UI so selection prompts and plan/result rows say "Codex skill already installed in workspace" instead of surfacing internal `MIGRATION_REASON_*` codes. Thanks @sjf. - CLI/migrate: render migrate result rows with distinct glyphs for manual-review (🔍) and archive (📖) items instead of the misleading "skipped" and "migrated" checkmarks, so users can see which entries still need attention versus which were filed away. Thanks @sjf. - CLI/migrate: split Codex migrate output into separate preview and result phases so the Before plan and After result render through clack with independently tunable copy. Thanks @sjf. @@ -43,7 +44,7 @@ Docs: https://docs.openclaw.ai - 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. - Update: snapshot config before update-time repair and restart writes, preserve plugin install records through doctor cleanup, and keep update-time config size drops from blocking the update while pointing users to the pre-update backup. Fixes #80077. (#80257) Thanks @Jerry-Xin and @vincentkoc. - WebChat/TUI: route Codex `tools.message` source replies to the active internal UI turn and mirror them to session history, so message-tool-only harness replies, including rich presentation and button-only replies, no longer disappear while WebChat and TUI remain non-targetable outbound channels. (#81586) Thanks @pashpashpash. -- Codex auth: accept OAuth profiles backed by `oauthRef` during runtime auth selection, so official Codex OAuth logins are used by app-server agent runs. (#81633) +- Codex auth: accept OAuth profiles backed by `oauthRef` during runtime auth selection, so official Codex OAuth logins are used by app-server agent runs. (#81633) Thanks @obviyus. - Sessions/status: classify ACP spawn-child sessions as `kind: "spawn-child"` instead of `"direct"` in `openclaw sessions` and status output; extract the duplicated session-kind classifier into a shared helper (`src/sessions/classify-session-kind.ts`) so both surfaces stay in sync. Fixes catalog #19. (#79544) - Sessions/Gateway: report `agentRuntime.id: "acpx"` (or stored backend id) with `source: "session-key"` for ACP control-plane session rows in `openclaw sessions --json`, `openclaw status`, and Gateway session RPC responses instead of the incorrect `"auto"` / `"pi"` implicit fallback. Fixes catalog #18. (#79550) - Telegram: delete tool-progress-only draft bubbles before rotating to the real answer, preventing orphaned progress messages in streamed replies. diff --git a/extensions/codex/src/app-server/auth-bridge.test.ts b/extensions/codex/src/app-server/auth-bridge.test.ts index f39e9f00dafb..9589a05f1850 100644 --- a/extensions/codex/src/app-server/auth-bridge.test.ts +++ b/extensions/codex/src/app-server/auth-bridge.test.ts @@ -676,6 +676,41 @@ describe("bridgeCodexAppServerStartOptions", () => { ).toBe("openai-codex:default"); }); + it("answers refresh requests from a discovered oauthRef-backed Codex profile", async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); + oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({ + access: "refreshed-ref-backed-access-token", + refresh: "refreshed-ref-backed-refresh-token", + expires: Date.now() + 60_000, + accountId: "account-ref-backed-refreshed", + }); + try { + upsertAuthProfile({ + agentDir, + profileId: "openai-codex:default", + credential: { + type: "oauth", + provider: "openai-codex", + access: "ref-backed-access-token", + refresh: "ref-backed-refresh-token", + expires: Date.now() + 60_000, + accountId: "account-ref-backed", + email: "codex@example.test", + }, + }); + + await expect(refreshCodexAppServerAuthTokens({ agentDir })).resolves.toEqual({ + accessToken: "refreshed-ref-backed-access-token", + chatgptAccountId: "account-ref-backed-refreshed", + chatgptPlanType: null, + }); + + expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("ref-backed-refresh-token"); + } finally { + await fs.rm(agentDir, { recursive: true, force: true }); + } + }); + it("applies native Codex CLI OAuth when no OpenClaw auth profile exists", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-")); const agentDir = path.join(root, "agent"); diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 0e8d25358422..2989c742b7d8 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -1442,21 +1442,34 @@ describe("classifyProviderRuntimeFailureKind", () => { }); it("classifies OAuth refresh failures", () => { - expect( - classifyProviderRuntimeFailureKind( - "OAuth token refresh failed for openai-codex: invalid_grant. Please try again or re-authenticate.", - ), - ).toBe("auth_refresh"); - expect( - classifyProviderRuntimeFailureKind( - "Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.", - ), - ).toBe("auth_refresh"); - expect( - classifyProviderRuntimeFailureKind( - "Your authentication session could not be refreshed automatically. Please log out and sign in again.", - ), - ).toBe("auth_refresh"); + const refreshFailures = [ + "OAuth token refresh failed for openai-codex: invalid_grant. Please try again or re-authenticate.", + "Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.", + "Your authentication session could not be refreshed automatically. Please log out and sign in again.", + ]; + for (const message of refreshFailures) { + expect(classifyProviderRuntimeFailureKind(message)).toBe("auth_refresh"); + expect(classifyFailoverReason(message, { provider: "openai-codex" })).toBe("auth_permanent"); + } + }); + + it("does not make uncertain OAuth refresh wrappers terminal", () => { + const message = + "OAuth token refresh failed for openai-codex: file lock timeout for /tmp/agent/auth-profiles.json. Please try again or re-authenticate."; + expect(classifyProviderRuntimeFailureKind(message)).toBe("auth_refresh"); + expect(classifyFailoverReason(message, { provider: "openai-codex" })).toBe("auth"); + }); + + it("keeps Codex entitlement and usage-limit payloads out of terminal auth", () => { + const entitlementMessages = [ + "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://chatgpt.com/explore/plus), try again after 11:34 AM.", + "You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits, try again later.", + '429 {"type":"error","error":{"type":"rate_limit_error","message":"You\\u0027ve hit your usage limit. Upgrade to Plus to continue using Codex (https://chatgpt.com/explore/plus), try again after 11:34 AM."}}', + ]; + for (const message of entitlementMessages) { + expect(classifyProviderRuntimeFailureKind(message)).not.toBe("auth_refresh"); + expect(classifyFailoverReason(message, { provider: "openai-codex" })).toBe("rate_limit"); + } }); it("classifies OAuth refresh timeouts and lock contention distinctly", () => { diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 513c425517f6..9dca15a9b307 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -854,6 +854,10 @@ function classifyFailoverClassificationFromMessage( if (isBillingErrorMessage(raw)) { return toReasonClassification("billing"); } + const oauthRefreshFailure = classifyOAuthRefreshFailure(raw); + if (oauthRefreshFailure?.reason) { + return toReasonClassification("auth_permanent"); + } if (isAuthPermanentErrorMessage(raw)) { return toReasonClassification("auth_permanent"); }