From 47d66fe3434cf4a20ea67f763e2b9e4e3f22302a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 22 May 2026 14:07:19 +0100 Subject: [PATCH] fix(infra): allow macos browser open over ssh env (#85340) --- CHANGELOG.md | 1 + src/infra/browser-open.test.ts | 28 ++++++++++++++++++++++++++++ src/infra/browser-open.ts | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a046425e5c2..985a368cda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai - CLI/update: start managed Gateway update handoff helpers from a stable existing directory and tolerate deleted cwd/package roots during macOS LaunchAgent handoff. Fixes #83808. (#83875) Thanks @jason-allen-oneal. - Cron: honor `cron.retry.retryOn: ["network"]` for common network error codes such as `EAI_AGAIN`, `EHOSTUNREACH`, and `ENETUNREACH`. - Gateway chat: broadcast returned agent-run error payloads after an agent starts so ACP/WebChat clients receive terminal idle-timeout errors. Fixes #84945. +- Dashboard/CLI: allow macOS browser launching through `open` even when SSH environment variables are present, while preserving Linux SSH no-display protection. Fixes #67088. Thanks @theglove44. - Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures. - Doctor/Codex: point native Codex asset warnings at the canonical `openclaw migrate plan codex` preview command. Fixes #84948. Thanks @markoa. - CLI/models: make `capability model auth logout --agent` remove auth profiles from the selected non-default agent store. Fixes #85092. Thanks @islandpreneur007. diff --git a/src/infra/browser-open.test.ts b/src/infra/browser-open.test.ts index 4f1d9af0eaa..980566b29e6 100644 --- a/src/infra/browser-open.test.ts +++ b/src/infra/browser-open.test.ts @@ -1,11 +1,19 @@ import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; + +const detectBinaryMock = vi.hoisted(() => vi.fn(async () => false)); + +vi.mock("./detect-binary.js", () => ({ + detectBinary: detectBinaryMock, +})); + import { resolveBrowserOpenCommand } from "./browser-open.js"; import { resetWindowsInstallRootsForTests } from "./windows-install-roots.js"; afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); + detectBinaryMock.mockReset().mockResolvedValue(false); resetWindowsInstallRootsForTests(); }); @@ -44,4 +52,24 @@ describe("resolveBrowserOpenCommand", () => { expect(resolved.argv).toEqual([rundll32, "url.dll,FileProtocolHandler"]); expect(resolved.command).toBe(rundll32); }); + + it("resolves macOS open even when SSH environment variables are present", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("darwin"); + vi.stubEnv("SSH_CONNECTION", "192.0.2.1 12345 192.0.2.2 22"); + detectBinaryMock.mockResolvedValueOnce(true); + + const resolved = await resolveBrowserOpenCommand(); + + expect(detectBinaryMock).toHaveBeenCalledWith("open"); + expect(resolved).toEqual({ argv: ["open"], command: "open" }); + }); + + it("still refuses browser launch over Linux SSH without a display", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("linux"); + vi.stubEnv("SSH_CONNECTION", "192.0.2.1 12345 192.0.2.2 22"); + + const resolved = await resolveBrowserOpenCommand(); + + expect(resolved).toEqual({ argv: null, reason: "ssh-no-display" }); + }); }); diff --git a/src/infra/browser-open.ts b/src/infra/browser-open.ts index 13d29ab8671..c4e01532947 100644 --- a/src/infra/browser-open.ts +++ b/src/infra/browser-open.ts @@ -48,7 +48,7 @@ export async function resolveBrowserOpenCommand(): Promise { Boolean(process.env.SSH_TTY) || Boolean(process.env.SSH_CONNECTION); - if (isSsh && !hasDisplay && platform !== "win32") { + if (isSsh && !hasDisplay && platform !== "win32" && platform !== "darwin") { return { argv: null, reason: "ssh-no-display" }; }