From 2dd29db464255a8442571baa033fd12da3bb673b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 29 Mar 2026 22:39:13 +0900 Subject: [PATCH] fix: ease bundled browser plugin recovery --- CHANGELOG.md | 1 + src/cli/run-main.test.ts | 37 ++++++++++++++++ src/cli/run-main.ts | 33 ++++++++++++++ src/commands/doctor-config-flow.test.ts | 18 ++++++++ src/config/plugin-auto-enable.test.ts | 51 ++++++++++++++++++++++ src/config/plugin-auto-enable.ts | 58 +++++++++++++++++++++++++ 6 files changed, 198 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fccdc6c2d17..9d39362fd8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai - Memory/LanceDB: resolve runtime dependency manifest lookup from the bundled `extensions/memory-lancedb` path (including flattened dist chunks) so startup no longer fails with a missing `@lancedb/lancedb` dependency error. (#56623) Thanks @LUKSOAgent. - Tools/web_search: localize the shared search cache to module scope so same-process global symbol lookups can no longer inspect or mutate cached web-search responses. Thanks @vincentkoc. - Agents/silent turns: fail closed on silent memory-flush runs so narrated `NO_REPLY` self-talk cannot stream or finalize into external replies even when block streaming is enabled. (#52593) +- Browser/plugins: auto-enable the bundled browser plugin when browser config or browser tool policy already references it, and show a clearer CLI error when `plugins.allow` excludes `browser`. ## 2026.3.28 diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index 63259259134..5ffc6693919 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { rewriteUpdateFlagArgv, + resolveMissingBrowserCommandMessage, shouldEnsureCliPath, shouldRegisterPrimarySubcommand, shouldSkipPluginCommandRegistration, @@ -136,3 +137,39 @@ describe("shouldUseRootHelpFastPath", () => { expect(shouldUseRootHelpFastPath(["node", "openclaw", "--help", "status"])).toBe(false); }); }); + +describe("resolveMissingBrowserCommandMessage", () => { + it("explains plugins.allow misses for the browser command", () => { + expect( + resolveMissingBrowserCommandMessage({ + plugins: { + allow: ["telegram"], + }, + }), + ).toContain('`plugins.allow` excludes "browser"'); + }); + + it("explains explicit bundled browser disablement", () => { + expect( + resolveMissingBrowserCommandMessage({ + plugins: { + entries: { + browser: { + enabled: false, + }, + }, + }, + }), + ).toContain("plugins.entries.browser.enabled=false"); + }); + + it("returns null when browser is already allowed", () => { + expect( + resolveMissingBrowserCommandMessage({ + plugins: { + allow: ["browser"], + }, + }), + ).toBeNull(); + }); +}); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 254dfdb0b31..9da6d9526cb 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -2,6 +2,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import { normalizeEnv } from "../infra/env.js"; import { formatUncaughtError } from "../infra/errors.js"; @@ -9,6 +10,7 @@ import { isMainModule } from "../infra/is-main.js"; import { ensureOpenClawCliOnPath } from "../infra/path-env.js"; import { assertSupportedRuntime } from "../infra/runtime-guard.js"; import { enableConsoleCapture } from "../logging.js"; +import { normalizePluginId } from "../plugins/config-state.js"; import { hasMemoryRuntime } from "../plugins/memory-state.js"; import { getCommandPathWithRootOptions, @@ -86,6 +88,28 @@ export function shouldUseRootHelpFastPath(argv: string[]): boolean { return isRootHelpInvocation(argv); } +export function resolveMissingBrowserCommandMessage(config?: OpenClawConfig): string | null { + const allow = + Array.isArray(config?.plugins?.allow) && config.plugins.allow.length > 0 + ? config.plugins.allow + .filter((entry): entry is string => typeof entry === "string") + .map((entry) => normalizePluginId(entry)) + : []; + if (allow.length > 0 && !allow.includes("browser")) { + return ( + 'The `openclaw browser` command is unavailable because `plugins.allow` excludes "browser". ' + + 'Add "browser" to `plugins.allow` if you want the bundled browser CLI and tool.' + ); + } + if (config?.plugins?.entries?.browser?.enabled === false) { + return ( + "The `openclaw browser` command is unavailable because `plugins.entries.browser.enabled=false`. " + + "Re-enable that entry if you want the bundled browser CLI and tool." + ); + } + return null; +} + function shouldLoadCliDotEnv(env: NodeJS.ProcessEnv = process.env): boolean { if (existsSync(path.join(process.cwd(), ".env"))) { return true; @@ -190,6 +214,15 @@ export async function runCli(argv: string[] = process.argv) { const config = await loadValidatedConfigForPluginRegistration(); if (config) { registerPluginCliCommands(program, config); + if ( + primary === "browser" && + !program.commands.some((command) => command.name() === "browser") + ) { + const browserCommandMessage = resolveMissingBrowserCommandMessage(config); + if (browserCommandMessage) { + throw new Error(browserCommandMessage); + } + } } } diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index 2dc467637c0..c5c7ce6a40f 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -286,6 +286,24 @@ describe("doctor config flow", () => { ).toBe("existing-session"); }); + it("repairs restrictive plugins.allow when browser is referenced via tools.alsoAllow", async () => { + const result = await runDoctorConfigWithInput({ + repair: true, + config: { + tools: { + alsoAllow: ["browser"], + }, + plugins: { + allow: ["telegram"], + }, + }, + run: loadAndMaybeMigrateDoctorConfig, + }); + + expect(result.cfg.plugins?.allow).toEqual(["telegram", "browser"]); + expect(result.cfg.plugins?.entries?.browser?.enabled).toBe(true); + }); + it("previews Matrix legacy sync-store migration in read-only mode", async () => { const noteSpy = vi.spyOn(noteModule, "note").mockImplementation(() => {}); try { diff --git a/src/config/plugin-auto-enable.test.ts b/src/config/plugin-auto-enable.test.ts index c3ccebb4740..bf8e4798a01 100644 --- a/src/config/plugin-auto-enable.test.ts +++ b/src/config/plugin-auto-enable.test.ts @@ -135,6 +135,57 @@ describe("applyPluginAutoEnable", () => { expect(result.config.plugins?.allow).toBeUndefined(); }); + it("auto-enables browser when browser config exists under a restrictive plugins.allow", () => { + const result = applyPluginAutoEnable({ + config: { + browser: { + defaultProfile: "openclaw", + }, + plugins: { + allow: ["telegram"], + }, + }, + env: {}, + }); + + expect(result.config.plugins?.allow).toEqual(["telegram", "browser"]); + expect(result.config.plugins?.entries?.browser?.enabled).toBe(true); + expect(result.changes).toContain("browser configured, enabled automatically."); + }); + + it("auto-enables browser when tools.alsoAllow references browser", () => { + const result = applyPluginAutoEnable({ + config: { + tools: { + alsoAllow: ["browser"], + }, + plugins: { + allow: ["telegram"], + }, + }, + env: {}, + }); + + expect(result.config.plugins?.allow).toEqual(["telegram", "browser"]); + expect(result.config.plugins?.entries?.browser?.enabled).toBe(true); + expect(result.changes).toContain("browser tool referenced, enabled automatically."); + }); + + it("keeps restrictive plugins.allow unchanged when browser is not referenced", () => { + const result = applyPluginAutoEnable({ + config: { + plugins: { + allow: ["telegram"], + }, + }, + env: {}, + }); + + expect(result.config.plugins?.allow).toEqual(["telegram"]); + expect(result.config.plugins?.entries?.browser).toBeUndefined(); + expect(result.changes).toEqual([]); + }); + it("ignores channels.modelByChannel for plugin auto-enable", () => { const result = applyPluginAutoEnable({ config: { diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts index faedf423ebb..9a18731bf89 100644 --- a/src/config/plugin-auto-enable.ts +++ b/src/config/plugin-auto-enable.ts @@ -319,6 +319,59 @@ function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean { return false; } +function listContainsBrowser(value: unknown): boolean { + return ( + Array.isArray(value) && + value.some((entry) => typeof entry === "string" && entry.trim().toLowerCase() === "browser") + ); +} + +function toolPolicyReferencesBrowser(value: unknown): boolean { + if (!isRecord(value)) { + return false; + } + return listContainsBrowser(value.allow) || listContainsBrowser(value.alsoAllow); +} + +function hasBrowserToolReference(cfg: OpenClawConfig): boolean { + if (toolPolicyReferencesBrowser(cfg.tools)) { + return true; + } + + const agentList = cfg.agents?.list; + if (!Array.isArray(agentList)) { + return false; + } + + return agentList.some((entry) => isRecord(entry) && toolPolicyReferencesBrowser(entry.tools)); +} + +function hasExplicitBrowserPluginEntry(cfg: OpenClawConfig): boolean { + return Boolean( + cfg.plugins?.entries && Object.prototype.hasOwnProperty.call(cfg.plugins.entries, "browser"), + ); +} + +function resolveBrowserAutoEnableReason(cfg: OpenClawConfig): string | null { + if (cfg.browser?.enabled === false || cfg.plugins?.entries?.browser?.enabled === false) { + return null; + } + + if (Object.prototype.hasOwnProperty.call(cfg, "browser")) { + return "browser configured"; + } + + if (hasExplicitBrowserPluginEntry(cfg)) { + return "browser plugin configured"; + } + + if (hasBrowserToolReference(cfg)) { + return "browser tool referenced"; + } + + return null; +} + function resolveConfiguredPlugins( cfg: OpenClawConfig, env: NodeJS.ProcessEnv, @@ -334,6 +387,11 @@ function resolveConfiguredPlugins( } } + const browserReason = resolveBrowserAutoEnableReason(cfg); + if (browserReason) { + changes.push({ pluginId: "browser", reason: browserReason }); + } + for (const [providerId, pluginId] of Object.entries( resolveAutoEnableProviderPluginIds(registry), )) {