From 1e4098134aa829d59fa3490c7a0700cd713f84b3 Mon Sep 17 00:00:00 2001 From: Alex Knight Date: Sun, 3 May 2026 21:12:30 +1000 Subject: [PATCH] fix: enable browser tools with full profile (#76557) Summary: - The PR makes `tools.profile: "full"` resolve to a wildcard allowlist, teaches plugin optional-tool allowlist checks to honor `*`, and updates regression tests, docs, and the changelog for browser tool availability. - Reproducibility: yes. source-level reproduction is high confidence: current main makes `full` resolve to no ... plugin allowlist helpers do not accept `*`. I did not run a live browser session in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: docs: update full profile description and add changelog for #76507 Validation: - ClawSweeper review passed for head b5329de33c0b1b670c438a0d51baff53c4a3e071. - Required merge gates passed before the squash merge. Prepared head SHA: b5329de33c0b1b670c438a0d51baff53c4a3e071 Review: https://github.com/openclaw/openclaw/pull/76557#issuecomment-4365736091 Co-authored-by: Alex Knight --- CHANGELOG.md | 1 + docs/tools/index.md | 2 +- ...tools.create-openclaw-coding-tools.test.ts | 48 +++++++++++++++ src/agents/tool-catalog.test.ts | 6 ++ src/agents/tool-catalog.ts | 4 +- src/plugins/tools.optional.test.ts | 61 +++++++++++++++++++ src/plugins/tools.ts | 6 ++ 7 files changed, 126 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86fdc7a3980..8c904fa5902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,7 @@ Docs: https://docs.openclaw.ai - Active Memory: preserve the target agent context when building embedded recall plugin tools so `memory_search` and `memory_get` stay available for explicit recall sessions. Fixes #76343. Thanks @Countermarch. - Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc. - Plugins/install: allow official catalog-matched npm channel plugins such as Feishu to pass the trusted install scanner path while keeping spoofed package names blocked. Thanks @vincentkoc. +- Tools/profiles: make `tools.profile: "full"` grant all tools including optional plugin tools such as browser, so the full profile no longer silently drops plugin-provided tools that require an explicit allowlist entry. Fixes #76507. Thanks @amknight. - Feishu: keep timeout env parsing separate from the HTTP client wrapper so package security scans no longer report a false env-harvesting hit during install. Thanks @vincentkoc. - Upgrade/config: validate configured web-search providers and statically suppressed model/provider pairs against the active plugin set at config load, so stale plugin state fails loud before runtime fallback. - Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses. diff --git a/docs/tools/index.md b/docs/tools/index.md index 7238b92da73..6bc9205de7e 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -146,7 +146,7 @@ Per-agent override: `agents.list[].tools.profile`. | Profile | What it includes | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| `full` | Unrestricted baseline for broader command/control access; same as leaving `tools.profile` unset | +| `full` | All core and optional plugin tools; unrestricted baseline for broader command/control access | | `coding` | `group:fs`, `group:runtime`, `group:web`, `group:sessions`, `group:memory`, `cron`, `image`, `image_generate`, `music_generate`, `video_generate` | | `messaging` | `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` | | `minimal` | `session_status` only | diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts b/src/agents/pi-tools.create-openclaw-coding-tools.test.ts index cd255c3b7df..715d7945133 100644 --- a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts +++ b/src/agents/pi-tools.create-openclaw-coding-tools.test.ts @@ -481,6 +481,54 @@ describe("createOpenClawCodingTools", () => { expect(names.has("browser")).toBe(false); }); + it("includes browser tool with full profile when browser is configured (#76507)", () => { + const tools = createOpenClawCodingTools({ + config: { + tools: { profile: "full" }, + browser: { enabled: true }, + plugins: { entries: { browser: { enabled: true } } }, + } as OpenClawConfig, + senderIsOwner: true, + }); + const names = new Set(tools.map((tool) => tool.name)); + // full profile must not filter any tools — browser, canvas, etc. must be present. + expect(names.has("browser")).toBe(true); + expect(names.has("canvas")).toBe(true); + expect(names.has("exec")).toBe(true); + expect(names.has("message")).toBe(true); + }); + + it("includes browser tool with full profile for non-owner senders (#76507)", () => { + const tools = createOpenClawCodingTools({ + config: { + tools: { profile: "full" }, + browser: { enabled: true }, + plugins: { entries: { browser: { enabled: true } } }, + } as OpenClawConfig, + senderIsOwner: false, + }); + const names = new Set(tools.map((tool) => tool.name)); + // browser is NOT owner-only; it must be available to non-owner senders. + expect(names.has("browser")).toBe(true); + expect(names.has("canvas")).toBe(true); + // owner-only tools should be filtered for non-owners + expect(names.has("gateway")).toBe(false); + expect(names.has("cron")).toBe(false); + expect(names.has("nodes")).toBe(false); + }); + + it("includes browser tool without explicit profile (defaults to no filtering) (#76507)", () => { + const tools = createOpenClawCodingTools({ + config: { + browser: { enabled: true }, + plugins: { entries: { browser: { enabled: true } } }, + } as OpenClawConfig, + }); + const names = new Set(tools.map((tool) => tool.name)); + // No profile means no profile filtering — all tools pass. + expect(names.has("browser")).toBe(true); + }); + it("keeps browser out of coding-profile subagents unless profile-stage alsoAllow adds it", () => { const baseConfig = { browser: { enabled: true }, diff --git a/src/agents/tool-catalog.test.ts b/src/agents/tool-catalog.test.ts index c14f90a4bc8..9c997a3ce78 100644 --- a/src/agents/tool-catalog.test.ts +++ b/src/agents/tool-catalog.test.ts @@ -21,4 +21,10 @@ describe("tool-catalog", () => { expect(resolveCoreToolProfilePolicy("messaging")?.allow).toContain("bundle-mcp"); expect(resolveCoreToolProfilePolicy("minimal")?.allow).not.toContain("bundle-mcp"); }); + + it("full profile uses wildcard to grant all tools (#76507)", () => { + const policy = resolveCoreToolProfilePolicy("full"); + expect(policy).toBeDefined(); + expect(policy!.allow).toContain("*"); + }); }); diff --git a/src/agents/tool-catalog.ts b/src/agents/tool-catalog.ts index 6e143032f28..c47b0d491b2 100644 --- a/src/agents/tool-catalog.ts +++ b/src/agents/tool-catalog.ts @@ -331,7 +331,9 @@ const CORE_TOOL_PROFILES: Record = { messaging: { allow: [...listCoreToolIdsForProfile("messaging"), "bundle-mcp"], }, - full: {}, + full: { + allow: ["*"], + }, }; function buildCoreToolGroupMap() { diff --git a/src/plugins/tools.optional.test.ts b/src/plugins/tools.optional.test.ts index f9d8cb9d33a..7c25ecf220f 100644 --- a/src/plugins/tools.optional.test.ts +++ b/src/plugins/tools.optional.test.ts @@ -1313,6 +1313,67 @@ describe("resolvePluginTools optional tools", () => { expect(loadOpenClawPluginsMock).not.toHaveBeenCalled(); }); + + it("includes non-optional browser tool when toolAllowlist is empty (full profile)", () => { + const browserEntry: MockRegistryToolEntry = { + pluginId: "browser", + optional: false, + source: "/tmp/browser.js", + names: ["browser"], + declaredNames: ["browser"], + factory: () => makeTool("browser"), + }; + setRegistry([browserEntry]); + + // Empty toolAllowlist simulates tools.profile: "full" where no explicit + // allow list exists. Non-optional plugin tools must still be resolved. + const tools = resolvePluginTools(createResolveToolsParams({ toolAllowlist: [] })); + + expectResolvedToolNames(tools, ["browser"]); + }); + + it("includes non-optional browser tool when toolAllowlist is undefined (full profile)", () => { + const browserEntry: MockRegistryToolEntry = { + pluginId: "browser", + optional: false, + source: "/tmp/browser.js", + names: ["browser"], + declaredNames: ["browser"], + factory: () => makeTool("browser"), + }; + setRegistry([browserEntry]); + + // Undefined toolAllowlist is the other variant of "no explicit allowlist". + const tools = resolvePluginTools(createResolveToolsParams()); + + expectResolvedToolNames(tools, ["browser"]); + }); + + it("includes non-optional browser tool when toolAllowlist has wildcard (#76507)", () => { + const browserEntry: MockRegistryToolEntry = { + pluginId: "browser", + optional: false, + source: "/tmp/browser.js", + names: ["browser"], + declaredNames: ["browser"], + factory: () => makeTool("browser"), + }; + setRegistry([browserEntry]); + + // Wildcard allowlist from tools.profile: "full" explicitly grants all tools. + const tools = resolvePluginTools(createResolveToolsParams({ toolAllowlist: ["*"] })); + + expectResolvedToolNames(tools, ["browser"]); + }); + + it("includes optional tools when wildcard allowlist is active (#76507)", () => { + setOptionalDemoRegistry(); + + // Wildcard must grant optional tools too. + const tools = resolvePluginTools(createResolveToolsParams({ toolAllowlist: ["*"] })); + + expectResolvedToolNames(tools, ["optional_tool"]); + }); }); describe("buildPluginToolMetadataKey", () => { diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index a04c33f3b6a..6a7fb0fee74 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -94,6 +94,9 @@ function isOptionalToolAllowed(params: { if (params.allowlist.size === 0) { return false; } + if (params.allowlist.has("*")) { + return true; + } const toolName = normalizeToolName(params.toolName); if (params.allowlist.has(toolName)) { return true; @@ -113,6 +116,9 @@ function isOptionalToolEntryPotentiallyAllowed(params: { if (params.allowlist.size === 0) { return false; } + if (params.allowlist.has("*")) { + return true; + } const pluginKey = normalizeToolName(params.pluginId); if (params.allowlist.has(pluginKey) || params.allowlist.has("group:plugins")) { return true;