diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fb23444b8a..c58162ad9f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Windows/onboarding: open provider OAuth and sign-in URLs with `explorer.exe` instead of routing them through `cmd /c start`, so quoted provider URLs cannot break out into host command execution. (#64161) Thanks @coygeek and @vincentkoc. - OpenAI/Codex OAuth: stop rewriting the upstream authorize URL scopes so new Codex sign-ins do not fail with `invalid_scope` before returning an authorization code. (#64713) Thanks @fuller-stack-dev. - Audio transcription: disable pinned DNS only for OpenAI-compatible multipart requests, while still validating hostnames, so OpenAI, Groq, and Mistral transcription works again without weakening other request paths. (#64766) Thanks @GodsBoy. - macOS/Talk Mode: after granting microphone permission on first enable, continue starting Talk Mode instead of requiring a second toggle. (#62459) Thanks @ggarber. diff --git a/src/commands/onboard-helpers.test.ts b/src/commands/onboard-helpers.test.ts index 0d761aa7dc0..d4c658adf24 100644 --- a/src/commands/onboard-helpers.test.ts +++ b/src/commands/onboard-helpers.test.ts @@ -44,7 +44,7 @@ afterEach(() => { }); describe("openUrl", () => { - it("quotes URLs on win32 so '&' is not treated as cmd separator", async () => { + it("passes OAuth URLs to explorer.exe on win32 without cmd parsing", async () => { vi.stubEnv("VITEST", ""); vi.stubEnv("NODE_ENV", ""); const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); @@ -59,23 +59,20 @@ describe("openUrl", () => { expect(mocks.runCommandWithTimeout).toHaveBeenCalledTimes(1); const [argv, options] = mocks.runCommandWithTimeout.mock.calls[0] ?? []; - expect(argv?.slice(0, 4)).toEqual(["cmd", "/c", "start", '""']); - expect(argv?.at(-1)).toBe(`"${url}"`); - expect(options).toMatchObject({ - timeoutMs: 5_000, - windowsVerbatimArguments: true, - }); + expect(argv).toEqual(["explorer.exe", url]); + expect(options).toMatchObject({ timeoutMs: 5_000 }); + expect(options?.windowsVerbatimArguments).toBeUndefined(); platformSpy.mockRestore(); }); }); describe("resolveBrowserOpenCommand", () => { - it("marks win32 commands as quoteUrl=true", async () => { + it("uses explorer.exe on win32", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const resolved = await resolveBrowserOpenCommand(); - expect(resolved.argv).toEqual(["cmd", "/c", "start", ""]); - expect(resolved.quoteUrl).toBe(true); + expect(resolved.argv).toEqual(["explorer.exe"]); + expect(resolved.command).toBe("explorer.exe"); platformSpy.mockRestore(); }); }); diff --git a/src/infra/browser-open.ts b/src/infra/browser-open.ts index 3aa04f086c4..3a4ea559f9d 100644 --- a/src/infra/browser-open.ts +++ b/src/infra/browser-open.ts @@ -6,7 +6,6 @@ export type BrowserOpenCommand = { argv: string[] | null; reason?: string; command?: string; - quoteUrl?: boolean; }; export type BrowserOpenSupport = { @@ -36,9 +35,8 @@ export async function resolveBrowserOpenCommand(): Promise { if (platform === "win32") { return { - argv: ["cmd", "/c", "start", ""], - command: "cmd", - quoteUrl: true, + argv: ["explorer.exe"], + command: "explorer.exe", }; } @@ -86,21 +84,10 @@ export async function openUrl(url: string): Promise { if (!resolved.argv) { return false; } - const quoteUrl = resolved.quoteUrl === true; const command = [...resolved.argv]; - if (quoteUrl) { - if (command.at(-1) === "") { - command[command.length - 1] = '""'; - } - command.push(`"${url}"`); - } else { - command.push(url); - } + command.push(url); try { - await runCommandWithTimeout(command, { - timeoutMs: 5_000, - windowsVerbatimArguments: quoteUrl, - }); + await runCommandWithTimeout(command, { timeoutMs: 5_000 }); return true; } catch { return false;