From db4d42ead2e2b197c1e66c95c1bf2ab6da03829b Mon Sep 17 00:00:00 2001 From: shaoohh <150606856+shaoohh@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:42:57 +0800 Subject: [PATCH] fix(browser): warn when Chrome extension version drifts after upgrades (#119641) * fix(browser): detect Chrome extension version drift * fix(browser): diagnose paired Chrome extension version drift Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com> --------- Co-authored-by: Peter Steinberger --- docs/install/updating.md | 4 + docs/tools/chrome-extension.md | 3 + .../bootstrap.chromium.test.ts | 58 +++- extensions/browser/src/browser/doctor.test.ts | 53 ++++ extensions/browser/src/browser/doctor.ts | 44 +++ .../routes/basic.existing-session.test.ts | 38 +++ .../browser/src/browser/routes/basic.ts | 9 +- .../src/cli/browser-cli-manage.test.ts | 252 +++++++++++++++--- .../browser/src/cli/browser-cli-manage.ts | 27 +- 9 files changed, 441 insertions(+), 47 deletions(-) diff --git a/docs/install/updating.md b/docs/install/updating.md index e5c00bd8b067..216a227170a7 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -387,6 +387,10 @@ openclaw doctor Migrates config, audits DM policies, and checks gateway health. Details: [Doctor](/gateway/doctor) +If you use the unpacked Chrome extension, also run `openclaw browser doctor --browser-profile chrome`. +For a version-mismatch warning, reload the extension from `chrome://extensions`; +fully restart Chrome if the warning remains. + ### Restart the gateway ```bash diff --git a/docs/tools/chrome-extension.md b/docs/tools/chrome-extension.md index 3eba24907de7..65205eecf204 100644 --- a/docs/tools/chrome-extension.md +++ b/docs/tools/chrome-extension.md @@ -301,6 +301,9 @@ openclaw doctor development fallback after the command says native bootstrap is ready. - **Extension was loaded before native setup:** restart Chrome once to clear its cached native-host miss, then rerun the ordered install flow. +- **Extension version mismatch:** reload the unpacked OpenClaw extension from + `chrome://extensions`, then rerun browser doctor. Fully restart Chrome if the + running and bundled versions still differ. - **Waiting for local OpenClaw:** run `extension status`; install or repair the owned native host. - **Automatic setup disabled:** enable it in Settings or click **Use local diff --git a/extensions/browser/chrome-extension/bootstrap.chromium.test.ts b/extensions/browser/chrome-extension/bootstrap.chromium.test.ts index 8b584de6c3fb..9fd07e4c1791 100644 --- a/extensions/browser/chrome-extension/bootstrap.chromium.test.ts +++ b/extensions/browser/chrome-extension/bootstrap.chromium.test.ts @@ -19,6 +19,7 @@ import { createBrowserRouteDispatcher } from "../src/browser/routes/dispatcher.j import { createBrowserRouteContext } from "../src/browser/server-context.js"; import { getFreePort } from "../src/browser/test-port.js"; import { getBrowserControlState, stopBrowserControlService } from "../src/control-service.js"; +import chromeExtensionManifest from "./manifest.json" with { type: "json" }; import { relayTestKey } from "./relay-key.test-support.js"; declare const chrome: { @@ -387,6 +388,24 @@ describe.runIf(runE2E)("Chrome native bootstrap Chromium E2E", () => { refreshConfigFromDisk: false, }); const dispatcher = createBrowserRouteDispatcher(routeContext); + const matchingDoctor = await dispatcher.dispatch({ + method: "GET", + path: "/doctor", + query: { profile: "e2e" }, + }); + expect(matchingDoctor.status).toBe(200); + expect(matchingDoctor.body).toMatchObject({ + checks: expect.arrayContaining([ + expect.objectContaining({ + id: "extension-version", + status: "pass", + summary: `running ${chromeExtensionManifest.version}; bundled ${chromeExtensionManifest.version} (match)`, + }), + ]), + }); + process.stderr.write( + `[browser-extension-e2e] doctor version match ${chromeExtensionManifest.version}\n`, + ); const tabsResponse = await dispatcher.dispatch({ method: "GET", path: "/tabs", @@ -632,8 +651,45 @@ describe.runIf(runE2E)("Chrome native bootstrap Chromium E2E", () => { .poll(() => relay.bridge.accessibleTabs().some((tab) => tab.tabId === tabId)) .toBe(false); + const installedManifestPath = path.join(installed, "manifest.json"); + const installedManifest = JSON.parse(await fs.readFile(installedManifestPath, "utf8")) as { + version: string; + }; + const outdatedVersion = chromeExtensionManifest.version === "2.0.0" ? "1.0.0" : "2.0.0"; + await fs.writeFile( + installedManifestPath, + `${JSON.stringify({ ...installedManifest, version: outdatedVersion }, null, 2)}\n`, + ); + await context.close(); + context = await launchChromium(); + await loadUnpackedExtension(context, installed); + expect(await waitForExtensionId(context, installed)).toBe(extensionId); + const outdatedExtensionPage = await context.newPage(); + await outdatedExtensionPage.goto(`chrome-extension://${extensionId}/options.html`); + await expect.poll(() => relay.bridge.identity?.extensionVersion).toBe(outdatedVersion); + const outdatedDoctor = await dispatcher.dispatch({ + method: "GET", + path: "/doctor", + query: { profile: "e2e" }, + }); + expect(outdatedDoctor.status).toBe(200); + expect(outdatedDoctor.body).toMatchObject({ + ok: true, + checks: expect.arrayContaining([ + expect.objectContaining({ + id: "extension-version", + status: "warn", + summary: `running ${outdatedVersion}; bundled ${chromeExtensionManifest.version} (mismatch)`, + fixHint: expect.stringMatching(/reload/i), + }), + ]), + }); + process.stderr.write( + `[browser-extension-e2e] doctor version mismatch running=${outdatedVersion} bundled=${chromeExtensionManifest.version} status=WARN\n`, + ); + const extensionContext = routeContext.forProfile("e2e"); - await extensionPage.evaluate( + await outdatedExtensionPage.evaluate( async () => await chrome.runtime.sendMessage({ type: "unpair" }), ); await expect.poll(() => relay.bridge.extensionConnected).toBe(false); diff --git a/extensions/browser/src/browser/doctor.test.ts b/extensions/browser/src/browser/doctor.test.ts index bdf2803ae404..63f9e32500da 100644 --- a/extensions/browser/src/browser/doctor.test.ts +++ b/extensions/browser/src/browser/doctor.test.ts @@ -1,7 +1,14 @@ // Browser tests cover doctor plugin behavior. import { describe, expect, it } from "vitest"; +import chromeExtensionManifest from "../../chrome-extension/manifest.json" with { type: "json" }; import { buildBrowserDoctorReport } from "./doctor.js"; +const outdatedExtensionVersion = chromeExtensionManifest.version === "2.0.0" ? "1.0.0" : "2.0.0"; +const equivalentExtensionVersion = + chromeExtensionManifest.version.split(".").length < 4 + ? `${chromeExtensionManifest.version}.0` + : chromeExtensionManifest.version.replace(/\.0$/, ""); + function collectWarningCheckIds(checks: readonly { id: string; status: string }[]): string[] { const ids: string[] = []; for (const check of checks) { @@ -46,6 +53,7 @@ describe("buildBrowserDoctorReport", () => { const websocketCheck = report.checks.find((check) => check.id === "cdp-websocket"); expect(websocketCheck?.status).toBe("info"); expect(websocketCheck?.summary).toBe("Browser is launchable but not running"); + expect(report.checks.find((check) => check.id === "extension-version")).toBeUndefined(); }); it("fails when Chrome MCP attach is not ready", () => { @@ -77,6 +85,7 @@ describe("buildBrowserDoctorReport", () => { expect(report.ok).toBe(false); const attachCheck = report.checks.find((check) => check.id === "attach-target"); expect(attachCheck?.status).toBe("fail"); + expect(report.checks.find((check) => check.id === "extension-version")).toBeUndefined(); }); it("keeps managed launch warnings non-fatal", () => { @@ -242,4 +251,48 @@ describe("buildBrowserDoctorReport", () => { summary: "unavailable: SystemInfo domain unavailable", }); }); + + it.each([ + ["outdated", outdatedExtensionVersion, "warn"], + ["current", chromeExtensionManifest.version, "pass"], + ["equivalent missing version component", equivalentExtensionVersion, "pass"], + ["maximum valid version", "65535.65535.65535.65535", "warn"], + ["unavailable", undefined, "info"], + ["terminal-control input", "2.0.0\u001b[31m", "info"], + ["oversized version component", "65536.0", "info"], + ["nonzero leading zero", "02.0.0", "info"], + ["all-zero version", "0.0.0.0", "info"], + ["too many version components", "2.0.0.0.0", "info"], + ] as const)("classifies %s extension version evidence", (_label, extensionVersion, severity) => { + const report = buildBrowserDoctorReport({ + status: { + enabled: true, + profile: "chrome", + driver: "extension", + transport: "extension", + running: true, + pid: null, + cdpPort: 18792, + chosenBrowser: null, + userDataDir: null, + color: "#00AA00", + headless: false, + attachOnly: true, + }, + extensionVersion, + }); + + const versionCheck = report.checks.find((check) => check.id === "extension-version"); + expect(versionCheck?.status).toBe(severity); + if (severity === "warn") { + expect(versionCheck?.summary).toContain( + `running ${extensionVersion}; bundled ${chromeExtensionManifest.version}`, + ); + expect(versionCheck?.fixHint).toMatch(/reload/i); + } else { + expect(versionCheck?.fixHint).toBeUndefined(); + expect(versionCheck?.summary).not.toContain("\u001b"); + } + expect(report.ok).toBe(true); + }); }); diff --git a/extensions/browser/src/browser/doctor.ts b/extensions/browser/src/browser/doctor.ts index 86b172810cad..f05a97032ff1 100644 --- a/extensions/browser/src/browser/doctor.ts +++ b/extensions/browser/src/browser/doctor.ts @@ -4,6 +4,7 @@ * Turns BrowserStatus into profile-aware diagnostic checks and fix hints for * CLI, tool, and HTTP doctor responses. */ +import chromeExtensionManifest from "../../chrome-extension/manifest.json" with { type: "json" }; import { formatBrowserGraphicsSummary } from "./chrome.graphics.js"; import type { BrowserStatus, BrowserTransport } from "./client.types.js"; @@ -27,9 +28,24 @@ export type BrowserDoctorReport = { status: BrowserStatus; }; +function isChromeExtensionVersion(value: unknown): value is string { + if (typeof value !== "string") { + return false; + } + const components = value.split("."); + return ( + components.length <= 4 && + components.every( + (component) => /^(?:0|[1-9]\d{0,4})$/.test(component) && Number(component) <= 65_535, + ) && + components.some((component) => component !== "0") + ); +} + /** Build a browser doctor report from a status response and environment facts. */ export function buildBrowserDoctorReport(params: { status: BrowserStatus; + extensionVersion?: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; uid?: number; @@ -88,6 +104,34 @@ export function buildBrowserDoctorReport(params: { "Install the OpenClaw Chrome extension (openclaw browser extension path), run openclaw browser extension pair, and paste the pairing string into the extension popup.", }), }); + + const runningVersion = isChromeExtensionVersion(params.extensionVersion) + ? params.extensionVersion + : undefined; + const bundledVersion = isChromeExtensionVersion(chromeExtensionManifest.version) + ? chromeExtensionManifest.version + : undefined; + // Chrome treats absent version components as zero, so trailing zeroes do not indicate drift. + const mismatch = Boolean( + runningVersion && + bundledVersion && + runningVersion.replace(/(?:\.0)+$/, "") !== bundledVersion.replace(/(?:\.0)+$/, ""), + ); + checks.push({ + id: "extension-version", + label: "Chrome extension version", + status: !runningVersion || !bundledVersion ? "info" : mismatch ? "warn" : "pass", + summary: + runningVersion && bundledVersion + ? `running ${runningVersion}; bundled ${bundledVersion} (${mismatch ? "mismatch" : "match"})` + : "version data unavailable", + ...(mismatch + ? { + fixHint: + "Reload the OpenClaw extension from chrome://extensions. If the versions still differ, fully quit and reopen Chrome.", + } + : {}), + }); } else { checks.push({ id: "managed-executable", diff --git a/extensions/browser/src/browser/routes/basic.existing-session.test.ts b/extensions/browser/src/browser/routes/basic.existing-session.test.ts index 3ee62f871b4e..5ad290b28460 100644 --- a/extensions/browser/src/browser/routes/basic.existing-session.test.ts +++ b/extensions/browser/src/browser/routes/basic.existing-session.test.ts @@ -1,5 +1,6 @@ // Browser tests cover basic.existing session plugin behavior. import { beforeEach, describe, expect, it, vi } from "vitest"; +import chromeExtensionManifest from "../../../chrome-extension/manifest.json" with { type: "json" }; import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js"; const { inspectChromeGraphicsDiagnosticsMock } = vi.hoisted(() => ({ @@ -214,6 +215,43 @@ describe("basic browser routes", () => { inspectChromeGraphicsDiagnosticsMock.mockReset(); }); + it("reports version drift only from the selected extension profile owner", async () => { + const outdatedVersion = chromeExtensionManifest.version === "2.0.0" ? "1.0.0" : "2.0.0"; + const state = { + ...createManagedProfileState( + { name: "chrome", driver: "extension", attachOnly: true }, + { + isHttpReachable: async () => true, + isTransportAvailable: async () => true, + }, + ), + extensionRelays: new Map([ + ["chrome", { bridge: { identity: { extensionVersion: outdatedVersion } } }], + ["other", { bridge: { identity: { extensionVersion: chromeExtensionManifest.version } } }], + ]), + }; + + const response = await callBasicRouteWithState({ + route: "/doctor", + query: { profile: "chrome" }, + state, + }); + const report = responseBodyRecord(response); + expect(response.statusCode).toBe(200); + expect(report.checks).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "extension-version", + status: "warn", + summary: expect.stringContaining( + `running ${outdatedVersion}; bundled ${chromeExtensionManifest.version}`, + ), + }), + ]), + ); + expect(report.status).not.toHaveProperty("chromeExtension"); + }); + it("releases the doctor transaction, restarts once, and retries the live probe", async () => { const ensureBrowserAvailable = vi.fn(async () => {}); const ensureTabAvailable = vi diff --git a/extensions/browser/src/browser/routes/basic.ts b/extensions/browser/src/browser/routes/basic.ts index 830df1e53e40..d93ddfbf761a 100644 --- a/extensions/browser/src/browser/routes/basic.ts +++ b/extensions/browser/src/browser/routes/basic.ts @@ -397,7 +397,14 @@ export function registerBrowserBasicRoutes(app: BrowserRouteRegistrar, ctx: Brow signal: req.signal, run: async (signal) => { const status = await buildBrowserStatus(ctx, profileCtx, signal); - const doctorReport = buildBrowserDoctorReport({ status }); + const doctorReport = buildBrowserDoctorReport({ + status, + extensionVersion: + status.transport === "extension" + ? ctx.state().extensionRelays?.get(profileCtx.profile.name)?.bridge.identity + ?.extensionVersion + : undefined, + }); if (toBoolean(req.query.deep) === true || toBoolean(req.query.live) === true) { doctorReport.checks.push(await runBrowserLiveProbe(profileCtx, signal)); doctorReport.ok = doctorReport.checks.every((check) => check.status !== "fail"); diff --git a/extensions/browser/src/cli/browser-cli-manage.test.ts b/extensions/browser/src/cli/browser-cli-manage.test.ts index fd1d7682b260..edf6c734ac21 100644 --- a/extensions/browser/src/cli/browser-cli-manage.test.ts +++ b/extensions/browser/src/cli/browser-cli-manage.test.ts @@ -490,41 +490,203 @@ describe("browser manage output", () => { expect(getBrowserManageCallBrowserRequestMock()).not.toHaveBeenCalled(); }); + it("prints authenticated extension drift from the canonical browser doctor report", async () => { + getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => { + if (req.path === "/doctor") { + return { + ok: true, + checks: [ + { + id: "extension-version", + label: "Chrome extension version", + status: "warn", + summary: "running 2.0.0; bundled 2.2.0 (mismatch)", + fixHint: "Reload the OpenClaw extension.", + }, + ], + status: { + enabled: true, + profile: "chrome", + driver: "extension", + transport: "extension", + running: true, + cdpReady: true, + }, + }; + } + if (req.path === "/profiles") { + return { profiles: [{ name: "chrome", running: true }] }; + } + if (req.path === "/tabs") { + return { running: true, tabs: [] }; + } + throw new Error(`unexpected browser route: ${req.path}`); + }); + + const program = createBrowserManageProgram(); + await program.parseAsync(["browser", "--browser-profile", "chrome", "doctor"], { + from: "user", + }); + + expect(lastRuntimeLog()).toContain( + "WARN extension-version: running 2.0.0; bundled 2.2.0 (mismatch); Reload the OpenClaw extension.", + ); + expect(process.exitCode).toBeUndefined(); + expect(getBrowserManageCallBrowserRequestMock().mock.calls[0]?.[1]).toMatchObject({ + path: "/doctor", + query: { profile: "chrome" }, + }); + }); + + it("keeps unavailable extension version evidence informational and nonfatal", async () => { + getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => { + if (req.path === "/doctor") { + return { + checks: [ + { + id: "extension-version", + label: "Chrome extension version", + status: "info", + summary: "version data unavailable", + }, + ], + status: { + enabled: true, + profile: "chrome", + transport: "extension", + running: true, + }, + }; + } + return req.path === "/profiles" + ? { profiles: [{ name: "chrome", running: true }] } + : { running: true, tabs: [] }; + }); + + const program = createBrowserManageProgram(); + await program.parseAsync(["browser", "--browser-profile", "chrome", "doctor"], { + from: "user", + }); + + expect(lastRuntimeLog()).toContain("INFO extension-version: version data unavailable"); + expect(lastRuntimeLog()).not.toContain("WARN extension-version"); + expect(process.exitCode).toBeUndefined(); + }); + + it("preserves one nonfatal JSON report for confirmed extension version drift", async () => { + getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => { + if (req.path === "/doctor") { + return { + checks: [ + { + id: "extension-version", + label: "Chrome extension version", + status: "warn", + summary: "running 2.0.0; bundled 2.2.0 (mismatch)", + fixHint: "Reload the OpenClaw extension.", + }, + ], + status: { + enabled: true, + profile: "chrome", + transport: "extension", + running: true, + }, + }; + } + return req.path === "/profiles" + ? { profiles: [{ name: "chrome", running: true }] } + : { running: true, tabs: [] }; + }); + + const program = createBrowserManageProgram(); + await program.parseAsync(["browser", "--json", "doctor"], { from: "user" }); + + expect(parseSingleRuntimeJson()).toMatchObject({ + ok: true, + checks: expect.arrayContaining([ + expect.objectContaining({ name: "extension-version", ok: true, warning: true }), + ]), + }); + expect(getBrowserCliRuntime().writeJson).toHaveBeenCalledTimes(1); + expect(process.exitCode).toBeUndefined(); + }); + + it("runs exactly one deep snapshot after consuming the canonical doctor report", async () => { + getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => { + if (req.path === "/doctor") { + return { + checks: [], + status: { + enabled: true, + profile: "chrome", + transport: "extension", + running: true, + }, + }; + } + if (req.path === "/profiles") { + return { profiles: [{ name: "chrome", running: true }] }; + } + if (req.path === "/tabs") { + return { running: true, tabs: [] }; + } + if (req.path === "/snapshot") { + return { ok: true, format: "aria", nodes: [{ role: "document" }] }; + } + throw new Error(`unexpected browser route: ${req.path}`); + }); + + const program = createBrowserManageProgram(); + await program.parseAsync(["browser", "--browser-profile", "chrome", "doctor", "--deep"], { + from: "user", + }); + + expect(lastRuntimeLog()).toContain("OK live-snapshot: 1 nodes/lines"); + const snapshotCalls = getBrowserManageCallBrowserRequestMock().mock.calls.filter( + ([, request]) => request.path === "/snapshot", + ); + expect(snapshotCalls).toHaveLength(1); + }); + it("prints a readable browser doctor report", async () => { getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => { - if (req.path === "/") { + if (req.path === "/doctor") { return { - enabled: true, - profile: "openclaw", - driver: "openclaw", - transport: "cdp", - running: true, - cdpReady: true, - cdpHttp: true, - pid: 4321, - cdpPort: 18792, - cdpUrl: "http://127.0.0.1:18792", - chosenBrowser: "chrome", - userDataDir: null, - color: "#00AA00", - headless: false, - noSandbox: false, - executablePath: null, - attachOnly: false, - graphics: { - status: "available", - observedAt: 123, - acceleration: "software", - renderer: "ANGLE (Google, SwiftShader Device)", - vendor: "Google Inc.", - version: "OpenGL ES 3.0", - backend: "(gl=angle,angle=swiftshader)", - devices: [], - featureStatus: {}, - disabledFeatures: [], - driverBugWorkarounds: [], - videoDecoding: [], - videoEncoding: [], + checks: [], + status: { + enabled: true, + profile: "openclaw", + driver: "openclaw", + transport: "cdp", + running: true, + cdpReady: true, + cdpHttp: true, + pid: 4321, + cdpPort: 18792, + cdpUrl: "http://127.0.0.1:18792", + chosenBrowser: "chrome", + userDataDir: null, + color: "#00AA00", + headless: false, + noSandbox: false, + executablePath: null, + attachOnly: false, + graphics: { + status: "available", + observedAt: 123, + acceleration: "software", + renderer: "ANGLE (Google, SwiftShader Device)", + vendor: "Google Inc.", + version: "OpenGL ES 3.0", + backend: "(gl=angle,angle=swiftshader)", + devices: [], + featureStatus: {}, + disabledFeatures: [], + driverBugWorkarounds: [], + videoDecoding: [], + videoEncoding: [], + }, }, }; } @@ -562,12 +724,15 @@ describe("browser manage output", () => { it("prints one complete JSON browser doctor failure before setting exit status", async () => { getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => { - if (req.path === "/") { + if (req.path === "/doctor") { return { - enabled: false, - profile: "openclaw", - transport: "cdp", - running: false, + checks: [], + status: { + enabled: false, + profile: "openclaw", + transport: "cdp", + running: false, + }, }; } if (req.path === "/profiles") { @@ -596,12 +761,15 @@ describe("browser manage output", () => { it("prints one JSON browser doctor report and succeeds when every check passes", async () => { getBrowserManageCallBrowserRequestMock().mockImplementation(async (_opts: unknown, req) => { - if (req.path === "/") { + if (req.path === "/doctor") { return { - enabled: true, - profile: "openclaw", - transport: "cdp", - running: true, + checks: [], + status: { + enabled: true, + profile: "openclaw", + transport: "cdp", + running: true, + }, }; } if (req.path === "/profiles") { diff --git a/extensions/browser/src/cli/browser-cli-manage.ts b/extensions/browser/src/cli/browser-cli-manage.ts index e108f16dfe3a..0fa6973acc23 100644 --- a/extensions/browser/src/cli/browser-cli-manage.ts +++ b/extensions/browser/src/cli/browser-cli-manage.ts @@ -4,6 +4,7 @@ */ import type { Command } from "commander"; import { formatBrowserGraphicsSummary } from "../browser/chrome.graphics.js"; +import type { BrowserDoctorReport } from "../browser/doctor.js"; import { BROWSER_TAB_REFERENCE_HELP, callBrowserRequest, @@ -37,6 +38,7 @@ type BrowserDoctorCheck = { ok: boolean; detail?: string; warning?: boolean; + info?: boolean; }; function sanitizeTableCell(value: string): string { @@ -132,7 +134,7 @@ function logBrowserTabs(tabs: BrowserTab[], json?: boolean) { } function formatDoctorLine(check: BrowserDoctorCheck): string { - const prefix = check.warning ? "WARN" : check.ok ? "OK" : "FAIL"; + const prefix = check.warning ? "WARN" : check.info ? "INFO" : check.ok ? "OK" : "FAIL"; return `${prefix} ${check.name}${check.detail ? `: ${check.detail}` : ""}`; } @@ -156,10 +158,18 @@ function formatBrowserDoctorGatewayError(error: unknown): string { async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, deep?: boolean) { const checks: BrowserDoctorCheck[] = []; - let status: BrowserStatus | null; + let report: BrowserDoctorReport; try { - status = await fetchBrowserStatus(parent, profile); + report = await callBrowserRequest( + parent, + { + method: "GET", + path: "/doctor", + query: resolveProfileQuery(profile), + }, + { timeoutMs: BROWSER_MANAGE_REQUEST_TIMEOUT_MS }, + ); checks.push({ name: "gateway", ok: true, @@ -174,6 +184,7 @@ async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, dee return { ok: false, checks }; } + const status = report.status; checks.push({ name: "plugin", ok: status.enabled, @@ -191,6 +202,16 @@ async function runBrowserDoctor(parent: BrowserParentOpts, profile?: string, dee ? `running${status.cdpReady === false ? ", CDP not ready" : ""}` : "not running; run `openclaw browser start`", }); + const extensionVersionCheck = report.checks.find((check) => check.id === "extension-version"); + if (extensionVersionCheck) { + checks.push({ + name: extensionVersionCheck.id, + ok: extensionVersionCheck.status !== "fail", + warning: extensionVersionCheck.status === "warn", + info: extensionVersionCheck.status === "info", + detail: `${extensionVersionCheck.summary}${extensionVersionCheck.fixHint ? `; ${extensionVersionCheck.fixHint}` : ""}`, + }); + } if (status.graphics) { checks.push({ name: "graphics",