diff --git a/CHANGELOG.md b/CHANGELOG.md index 23e9317eb9f..e6e27fa254a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ Docs: https://docs.openclaw.ai - Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) Thanks @luzhidong. - Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom. - Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46580) Fixes #46532. Thanks @vincentkoc. +- Models/OpenAI Codex OAuth: start the remote manual-input race for Codex login and keep the pasted-input prompt aligned with the actual accepted values, so remote/VPS auth no longer stalls waiting on an unreachable localhost callback. (#51631) Thanks @cash-echo-bot. - Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults. - Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles. - Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#46596) Fixes #45777. Thanks @odysseus0. diff --git a/src/commands/openai-codex-oauth.test.ts b/src/commands/openai-codex-oauth.test.ts index 43f1ac41f8a..07c7dc88a0a 100644 --- a/src/commands/openai-codex-oauth.test.ts +++ b/src/commands/openai-codex-oauth.test.ts @@ -4,7 +4,6 @@ import type { WizardPrompter } from "../wizard/prompts.js"; const mocks = vi.hoisted(() => ({ loginOpenAICodex: vi.fn(), - createVpsAwareOAuthHandlers: vi.fn(), runOpenAIOAuthTlsPreflight: vi.fn(), formatOpenAIOAuthTlsPreflightFix: vi.fn(), })); @@ -13,22 +12,19 @@ vi.mock("@mariozechner/pi-ai/oauth", () => ({ loginOpenAICodex: mocks.loginOpenAICodex, })); -vi.mock("./oauth-flow.js", () => ({ - createVpsAwareOAuthHandlers: mocks.createVpsAwareOAuthHandlers, -})); - -vi.mock("./oauth-tls-preflight.js", () => ({ +vi.mock("../plugins/provider-openai-codex-oauth-tls.js", () => ({ runOpenAIOAuthTlsPreflight: mocks.runOpenAIOAuthTlsPreflight, formatOpenAIOAuthTlsPreflightFix: mocks.formatOpenAIOAuthTlsPreflightFix, })); -import { loginOpenAICodexOAuth } from "./openai-codex-oauth.js"; +import { loginOpenAICodexOAuth } from "../plugins/provider-openai-codex-oauth.js"; function createPrompter() { const spin = { update: vi.fn(), stop: vi.fn() }; - const prompter: Pick = { + const prompter: Pick = { note: vi.fn(async () => {}), progress: vi.fn(() => spin), + text: vi.fn(async () => "http://localhost:1455/auth/callback?code=test"), }; return { prompter: prompter as unknown as WizardPrompter, spin }; } @@ -43,14 +39,17 @@ function createRuntime(): RuntimeEnv { }; } -async function runCodexOAuth(params: { isRemote: boolean }) { +async function runCodexOAuth(params: { + isRemote: boolean; + openUrl?: (url: string) => Promise; +}) { const { prompter, spin } = createPrompter(); const runtime = createRuntime(); const result = await loginOpenAICodexOAuth({ prompter, runtime, isRemote: params.isRemote, - openUrl: async () => {}, + openUrl: params.openUrl ?? (async () => {}), }); return { result, prompter, spin, runtime }; } @@ -70,10 +69,6 @@ describe("loginOpenAICodexOAuth", () => { expires: Date.now() + 60_000, email: "user@example.com", }; - mocks.createVpsAwareOAuthHandlers.mockReturnValue({ - onAuth: vi.fn(), - onPrompt: vi.fn(), - }); mocks.loginOpenAICodex.mockResolvedValue(creds); const { result, spin, runtime } = await runCodexOAuth({ isRemote: false }); @@ -92,11 +87,6 @@ describe("loginOpenAICodexOAuth", () => { expires: Date.now() + 60_000, email: "user@example.com", }; - const onAuthSpy = vi.fn(); - mocks.createVpsAwareOAuthHandlers.mockReturnValue({ - onAuth: onAuthSpy, - onPrompt: vi.fn(), - }); mocks.loginOpenAICodex.mockImplementation( async (opts: { onAuth: (event: { url: string }) => Promise }) => { await opts.onAuth({ @@ -106,20 +96,18 @@ describe("loginOpenAICodexOAuth", () => { }, ); - await runCodexOAuth({ isRemote: false }); + const openUrl = vi.fn(async () => {}); + const { runtime } = await runCodexOAuth({ isRemote: false, openUrl }); - expect(onAuthSpy).toHaveBeenCalledTimes(1); - const event = onAuthSpy.mock.calls[0]?.[0] as { url: string }; - expect(event.url).toBe( + expect(openUrl).toHaveBeenCalledWith( "https://auth.openai.com/oauth/authorize?scope=openid+profile+email+offline_access&state=abc", ); + expect(runtime.log).toHaveBeenCalledWith( + "Open: https://auth.openai.com/oauth/authorize?scope=openid+profile+email+offline_access&state=abc", + ); }); it("reports oauth errors and rethrows", async () => { - mocks.createVpsAwareOAuthHandlers.mockReturnValue({ - onAuth: vi.fn(), - onPrompt: vi.fn(), - }); mocks.loginOpenAICodex.mockRejectedValue(new Error("oauth failed")); const { prompter, spin } = createPrompter(); @@ -141,6 +129,37 @@ describe("loginOpenAICodexOAuth", () => { ); }); + it("passes manual code input hook for remote oauth flows", async () => { + const creds = { + provider: "openai-codex" as const, + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + email: "user@example.com", + }; + mocks.loginOpenAICodex.mockImplementation( + async (opts: { + onAuth: (event: { url: string }) => Promise; + onManualCodeInput?: () => Promise; + }) => { + await opts.onAuth({ + url: "https://auth.openai.com/oauth/authorize?state=abc", + }); + expect(opts.onManualCodeInput).toBeTypeOf("function"); + await expect(opts.onManualCodeInput?.()).resolves.toContain("code=test"); + return creds; + }, + ); + + const { result, prompter } = await runCodexOAuth({ isRemote: true }); + + expect(result).toEqual(creds); + expect(prompter.text).toHaveBeenCalledWith({ + message: "Paste the authorization code (or full redirect URL):", + validate: expect.any(Function), + }); + }); + it("continues OAuth flow on non-certificate preflight failures", async () => { const creds = { provider: "openai-codex" as const, @@ -154,10 +173,6 @@ describe("loginOpenAICodexOAuth", () => { kind: "network", message: "Client network socket disconnected before secure TLS connection was established", }); - mocks.createVpsAwareOAuthHandlers.mockReturnValue({ - onAuth: vi.fn(), - onPrompt: vi.fn(), - }); mocks.loginOpenAICodex.mockResolvedValue(creds); const { result, prompter, runtime } = await runCodexOAuth({ isRemote: false }); diff --git a/src/plugins/provider-openai-codex-oauth.ts b/src/plugins/provider-openai-codex-oauth.ts index 6e16cf863f0..f15f7999e2c 100644 --- a/src/plugins/provider-openai-codex-oauth.ts +++ b/src/plugins/provider-openai-codex-oauth.ts @@ -7,6 +7,8 @@ import { runOpenAIOAuthTlsPreflight, } from "./provider-openai-codex-oauth-tls.js"; +const manualInputPromptMessage = "Paste the authorization code (or full redirect URL):"; + export async function loginOpenAICodexOAuth(params: { prompter: WizardPrompter; runtime: RuntimeEnv; @@ -47,11 +49,18 @@ export async function loginOpenAICodexOAuth(params: { spin, openUrl, localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…", + manualPromptMessage: manualInputPromptMessage, }); const creds = await loginOpenAICodex({ onAuth: baseOnAuth, onPrompt, + onManualCodeInput: isRemote + ? async () => + await onPrompt({ + message: manualInputPromptMessage, + }) + : undefined, onProgress: (msg: string) => spin.update(msg), }); spin.stop("OpenAI OAuth complete");