diff --git a/CHANGELOG.md b/CHANGELOG.md index b616b6e9141..75512053c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,6 +159,7 @@ Docs: https://docs.openclaw.ai - CLI/agents: default new omitted-account bindings to all accounts when the channel has multiple configured accounts, and clarify account-scope docs. (#49769) Thanks @Gcaufy. - Codex app-server: let authorized `/codex` control commands such as `/codex detach` escape plugin-owned conversation bindings while keeping unknown or unauthorized slash text routed to the bound plugin. Fixes #85157. (#85188) Thanks @TurboTheTurtle. - Auto-reply/models: keep `/models` browse replies fast by sharing the bounded read-only catalog path with Gateway model listing. (#84735) Thanks @safrano9999. +- Browser/Doctor: read macOS Chrome app bundle versions from `Info.plist` before spawning Chrome and extend the fallback version probe timeout, avoiding false cold-cache warnings from Gatekeeper latency. Fixes #85418. Thanks @davidcittadini. - Codex app-server: disable native Code Mode when the effective exec host is `node` and keep OpenClaw `exec`/`process` available, so `/exec host=node` routes shell commands through the selected node instead of the gateway. Fixes #85012. (#85090) Thanks @sahilsatralkar. - Agents: bound embedded auto-compaction session write-lock watchdogs to the compaction timeout instead of the full run timeout, so stuck compaction cannot hold the live session lock for the whole run window. (#84949) Thanks @luoyanglang. - Gateway/agents: return phase-aware `agent.wait` timeout attribution and only cool auth profiles on provider-started timeouts. Refs #65504. Thanks @100yenadmin. diff --git a/extensions/browser/src/browser/chrome.executables.ts b/extensions/browser/src/browser/chrome.executables.ts index 9b6485b6a40..188e9c112a5 100644 --- a/extensions/browser/src/browser/chrome.executables.ts +++ b/extensions/browser/src/browser/chrome.executables.ts @@ -15,6 +15,8 @@ export type BrowserExecutable = { const CHROME_VERSION_RE = /\b(\d+)(?:\.\d+){1,3}\b/g; const PLAYWRIGHT_BROWSERS_PATH_ENV = "PLAYWRIGHT_BROWSERS_PATH"; +const BROWSER_VERSION_TIMEOUT_MS = 6000; +const MAC_PLISTBUDDY_TIMEOUT_MS = 800; const CHROMIUM_BUNDLE_IDS = new Set([ "com.google.Chrome", @@ -732,13 +734,42 @@ export function resolveGoogleChromeExecutableForPlatform( } export function readBrowserVersion(executablePath: string): string | null { - const output = execText(executablePath, ["--version"], 2000); + if (process.platform === "darwin") { + const bundleVersion = readMacBundleBrowserVersion(executablePath); + if (bundleVersion) { + return bundleVersion; + } + } + + const output = execText(executablePath, ["--version"], BROWSER_VERSION_TIMEOUT_MS); if (!output) { return null; } return output.replace(/\s+/g, " ").trim(); } +function readMacBundleBrowserVersion(executablePath: string): string | null { + const appBundlePath = resolveMacAppBundlePath(executablePath); + if (!appBundlePath) { + return null; + } + const plistPath = path.join(appBundlePath, "Contents", "Info.plist"); + return execText( + "/usr/libexec/PlistBuddy", + ["-c", "Print :CFBundleShortVersionString", plistPath], + MAC_PLISTBUDDY_TIMEOUT_MS, + ); +} + +function resolveMacAppBundlePath(executablePath: string): string | null { + const parts = path.normalize(executablePath).split(path.sep); + const appIndex = parts.findIndex((part) => part.endsWith(".app")); + if (appIndex < 0) { + return null; + } + return parts.slice(0, appIndex + 1).join(path.sep) || path.sep; +} + export function parseBrowserMajorVersion(rawVersion: string | null | undefined): number | null { const matches = [...(rawVersion ?? "").matchAll(CHROME_VERSION_RE)]; const match = matches.at(-1); diff --git a/extensions/browser/src/browser/chrome.version.test.ts b/extensions/browser/src/browser/chrome.version.test.ts new file mode 100644 index 00000000000..ba748c9c1fb --- /dev/null +++ b/extensions/browser/src/browser/chrome.version.test.ts @@ -0,0 +1,86 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const execFileSyncMock = vi.hoisted(() => vi.fn()); + +vi.mock("node:child_process", async () => { + const actual = await vi.importActual("node:child_process"); + return { + ...actual, + execFileSync: (...args: unknown[]) => execFileSyncMock(...args), + }; +}); + +import { readBrowserVersion } from "./chrome.executables.js"; + +function stubPlatform(platform: NodeJS.Platform): void { + Object.defineProperty(process, "platform", { + configurable: true, + value: platform, + }); +} + +describe("readBrowserVersion", () => { + const originalPlatform = process.platform; + + afterEach(() => { + stubPlatform(originalPlatform); + execFileSyncMock.mockReset(); + vi.restoreAllMocks(); + }); + + it("reads macOS app bundle versions from Info.plist before spawning Chrome", () => { + stubPlatform("darwin"); + execFileSyncMock.mockReturnValue("148.0.7778.179\n"); + + const version = readBrowserVersion( + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + ); + + expect(version).toBe("148.0.7778.179"); + expect(execFileSyncMock).toHaveBeenCalledTimes(1); + expect(execFileSyncMock).toHaveBeenCalledWith( + "/usr/libexec/PlistBuddy", + [ + "-c", + "Print :CFBundleShortVersionString", + "/Applications/Google Chrome.app/Contents/Info.plist", + ], + expect.objectContaining({ timeout: 800 }), + ); + }); + + it("falls back to a slower --version probe when macOS bundle metadata is unavailable", () => { + stubPlatform("darwin"); + execFileSyncMock + .mockImplementationOnce(() => { + throw new Error("plist unavailable"); + }) + .mockReturnValueOnce("Google Chrome 148.0.7778.179\n"); + + const version = readBrowserVersion( + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + ); + + expect(version).toBe("Google Chrome 148.0.7778.179"); + expect(execFileSyncMock).toHaveBeenNthCalledWith( + 2, + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + ["--version"], + expect.objectContaining({ timeout: 6000 }), + ); + }); + + it("uses the slower --version probe for non-bundle paths", () => { + stubPlatform("darwin"); + execFileSyncMock.mockReturnValue("Chromium 148.0.7778.179\n"); + + const version = readBrowserVersion("/opt/chromium/chrome"); + + expect(version).toBe("Chromium 148.0.7778.179"); + expect(execFileSyncMock).toHaveBeenCalledWith( + "/opt/chromium/chrome", + ["--version"], + expect.objectContaining({ timeout: 6000 }), + ); + }); +});