From b9452d2df8ff3c5ef1863130c6fc49e4d25370c5 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Wed, 22 Apr 2026 02:37:23 -0500 Subject: [PATCH] fix(openai): harden codex device auth prep --- CHANGELOG.md | 11 +++++++ .../openai/openai-codex-device-code.test.ts | 30 +++++++++++++++++++ extensions/openai/openai-codex-device-code.ts | 4 ++- .../openai/openai-codex-provider.test.ts | 8 +++++ extensions/openai/openai-codex-provider.ts | 5 +++- 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 143bfe8ed26..fb4b33a4131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,6 +185,17 @@ Docs: https://docs.openclaw.ai - Control UI/device pairing: explain scope and role approval upgrades during reconnects, and show requested versus approved access in the Control UI and `openclaw devices` so broader reconnects no longer look like lost pairings. (#69221) Thanks @obviyus. - Gateway/Control UI: surface pending scope, role, and device-metadata pairing approvals in auth errors and Control UI hints so broader reconnects no longer look like random auth breakage. (#69226) Thanks @obviyus. +## 2026.4.19-beta.2 + +### Fixes + +- Agents/openai-completions: always send `stream_options.include_usage` on streaming requests, so local and custom OpenAI-compatible backends report real context usage instead of showing 0%. (#68746) Thanks @kagura-agent. +- Agents/nested lanes: scope nested agent work per target session so a long-running nested run on one session no longer head-of-line blocks unrelated sessions across the gateway. (#67785) Thanks @stainlu. +- Agents/status: preserve carried-forward session token totals for providers that omit usage metadata, so `/status` and `openclaw sessions` keep showing the last known context usage instead of dropping back to unknown/0%. (#67695) Thanks @stainlu. +- Install/update: keep legacy update verification compatible with the QA Lab runtime shim, so updating older global installs to beta no longer fails after npm installs the package successfully. + +## 2026.4.19-beta.1 + ### Fixes - Agents/channels: route cross-agent subagent spawns through the target agent's bound channel account while preserving peer and workspace/role-scoped bindings, so child sessions no longer inherit the caller's account in shared rooms, workspaces, or multi-account setups. (#67508) Thanks @lukeboyett and @gumadeiras. diff --git a/extensions/openai/openai-codex-device-code.test.ts b/extensions/openai/openai-codex-device-code.test.ts index 2c5bb34efc9..e1b3e4df389 100644 --- a/extensions/openai/openai-codex-device-code.test.ts +++ b/extensions/openai/openai-codex-device-code.test.ts @@ -182,4 +182,34 @@ describe("loginOpenAICodexDeviceCode", () => { "OpenAI device authorization failed: authorization_declined spoofed (Denied next line)", ); }); + + it("strips C1 terminal controls from reflected device-code errors", async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + createJsonResponse({ + device_auth_id: "device-auth-123", + user_code: "CODE-12345", + interval: "0", + }), + ) + .mockResolvedValueOnce( + createJsonResponse( + { + error: `authorization_declined${String.fromCharCode(0x9b)}spoofed`, + error_description: `Denied${String.fromCharCode(0x9d)}next line`, + }, + { status: 401 }, + ), + ); + + await expect( + loginOpenAICodexDeviceCode({ + fetchFn: fetchMock as typeof fetch, + onVerification: async () => {}, + }), + ).rejects.toThrow( + "OpenAI device authorization failed: authorization_declined spoofed (Denied next line)", + ); + }); }); diff --git a/extensions/openai/openai-codex-device-code.ts b/extensions/openai/openai-codex-device-code.ts index 9e452b144a7..0e24f59bf32 100644 --- a/extensions/openai/openai-codex-device-code.ts +++ b/extensions/openai/openai-codex-device-code.ts @@ -88,7 +88,9 @@ function sanitizeDeviceCodeErrorText(value: string): string { const c0Start = String.fromCharCode(0x00); const c0End = String.fromCharCode(0x1f); const del = String.fromCharCode(0x7f); - const controlCharsRegex = new RegExp(`[${c0Start}-${c0End}${del}]`, "g"); + const c1Start = String.fromCharCode(0x80); + const c1End = String.fromCharCode(0x9f); + const controlCharsRegex = new RegExp(`[${c0Start}-${c0End}${del}${c1Start}-${c1End}]`, "g"); return value .replace(osc8Regex, "") .replace(ansiCsiRegex, "") diff --git a/extensions/openai/openai-codex-provider.test.ts b/extensions/openai/openai-codex-provider.test.ts index a9f0fccb1d7..a31f7375e9d 100644 --- a/extensions/openai/openai-codex-provider.test.ts +++ b/extensions/openai/openai-codex-provider.test.ts @@ -333,6 +333,14 @@ describe("openai codex provider", () => { const logOutput = runtime.log.mock.calls.flat().join("\n"); expect(logOutput).toContain("https://auth.openai.com/codex/device"); expect(logOutput).not.toContain("CODE-12345"); + expect(note).toHaveBeenCalledWith( + expect.stringContaining("Code: [shown on the local device only]"), + "OpenAI Codex device code", + ); + expect(note).not.toHaveBeenCalledWith( + expect.stringContaining("Code: CODE-12345"), + "OpenAI Codex device code", + ); }); it("exposes Codex CLI auth as a runtime-only external profile", () => { diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index a0143657559..49cf0f4ff45 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -319,13 +319,16 @@ async function runOpenAICodexDeviceCode(ctx: ProviderAuthContext) { onProgress: (message) => spin.update(message), onVerification: async ({ verificationUrl, userCode, expiresInMs }) => { const expiresInMinutes = Math.max(1, Math.round(expiresInMs / 60_000)); + const codeLine = ctx.isRemote + ? "Code: [shown on the local device only]" + : `Code: ${userCode}`; await ctx.prompter.note( [ ctx.isRemote ? "Open this URL in your LOCAL browser and enter the code below." : "Open this URL in your browser and enter the code below.", `URL: ${verificationUrl}`, - `Code: ${userCode}`, + codeLine, `Code expires in ${expiresInMinutes} minutes. Never share it.`, ].join("\n"), "OpenAI Codex device code",