From dc07f1e0213eb0c4ccb9b02eef1aa97c9e890978 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 23 Jan 2026 09:01:41 +0000 Subject: [PATCH] fix: keep core tools when allowlist is plugin-only --- CHANGELOG.md | 1 + docs/plugins/agent-tools.md | 2 ++ docs/tools/lobster.md | 4 +++ src/agents/pi-tools.ts | 31 +++++++++++++++---- .../tool-policy.plugin-only-allowlist.test.ts | 25 +++++++++++++++ src/agents/tool-policy.ts | 16 ++++++++++ 6 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 src/agents/tool-policy.plugin-only-allowlist.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ee92edf62..1a66a3f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot ### Fixes - Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla. +- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467) ## 2026.1.22 diff --git a/docs/plugins/agent-tools.md b/docs/plugins/agent-tools.md index 71d44d155..b0d91dfa9 100644 --- a/docs/plugins/agent-tools.md +++ b/docs/plugins/agent-tools.md @@ -82,6 +82,8 @@ Enable optional tools in `agents.list[].tools.allow` (or global `tools.allow`): ``` Other config knobs that affect tool availability: +- Allowlists that only name plugin tools are treated as plugin opt-ins; core tools remain + enabled unless you also include core tools or groups in the allowlist. - `tools.profile` / `agents.list[].tools.profile` (base allowlist) - `tools.byProvider` / `agents.list[].tools.byProvider` (provider‑specific allow/deny) - `tools.sandbox.tools.*` (sandbox tool policy when sandboxed) diff --git a/docs/tools/lobster.md b/docs/tools/lobster.md index 7b88f5073..0f4760399 100644 --- a/docs/tools/lobster.md +++ b/docs/tools/lobster.md @@ -121,6 +121,10 @@ Lobster is an **optional** plugin tool (not enabled by default). Allow it per ag You can also allow it globally with `tools.allow` if every agent should see it. +Note: allowlists are opt-in for optional plugins. If your allowlist only names +plugin tools (like `lobster`), Clawdbot keeps core tools enabled. To restrict core +tools, include the core tools or groups you want in the allowlist too. + ## Example: Email triage Without Lobster: diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index c61e3b694..2831aec99 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -44,6 +44,7 @@ import { collectExplicitAllowlist, expandPolicyWithPluginGroups, resolveToolProfilePolicy, + stripPluginOnlyAllowlist, } from "./tool-policy.js"; import { getPluginToolMeta } from "../plugins/tools.js"; @@ -298,12 +299,30 @@ export function createClawdbotCodingTools(options?: { tools, toolMeta: (tool) => getPluginToolMeta(tool as AnyAgentTool), }); - const profilePolicyExpanded = expandPolicyWithPluginGroups(profilePolicy, pluginGroups); - const providerProfileExpanded = expandPolicyWithPluginGroups(providerProfilePolicy, pluginGroups); - const globalPolicyExpanded = expandPolicyWithPluginGroups(globalPolicy, pluginGroups); - const globalProviderExpanded = expandPolicyWithPluginGroups(globalProviderPolicy, pluginGroups); - const agentPolicyExpanded = expandPolicyWithPluginGroups(agentPolicy, pluginGroups); - const agentProviderExpanded = expandPolicyWithPluginGroups(agentProviderPolicy, pluginGroups); + const profilePolicyExpanded = expandPolicyWithPluginGroups( + stripPluginOnlyAllowlist(profilePolicy, pluginGroups), + pluginGroups, + ); + const providerProfileExpanded = expandPolicyWithPluginGroups( + stripPluginOnlyAllowlist(providerProfilePolicy, pluginGroups), + pluginGroups, + ); + const globalPolicyExpanded = expandPolicyWithPluginGroups( + stripPluginOnlyAllowlist(globalPolicy, pluginGroups), + pluginGroups, + ); + const globalProviderExpanded = expandPolicyWithPluginGroups( + stripPluginOnlyAllowlist(globalProviderPolicy, pluginGroups), + pluginGroups, + ); + const agentPolicyExpanded = expandPolicyWithPluginGroups( + stripPluginOnlyAllowlist(agentPolicy, pluginGroups), + pluginGroups, + ); + const agentProviderExpanded = expandPolicyWithPluginGroups( + stripPluginOnlyAllowlist(agentProviderPolicy, pluginGroups), + pluginGroups, + ); const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups); const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups); diff --git a/src/agents/tool-policy.plugin-only-allowlist.test.ts b/src/agents/tool-policy.plugin-only-allowlist.test.ts new file mode 100644 index 000000000..b5f6c9d42 --- /dev/null +++ b/src/agents/tool-policy.plugin-only-allowlist.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; + +import { stripPluginOnlyAllowlist, type PluginToolGroups } from "./tool-policy.js"; + +const pluginGroups: PluginToolGroups = { + all: ["lobster", "workflow_tool"], + byPlugin: new Map([["lobster", ["lobster", "workflow_tool"]]]), +}; + +describe("stripPluginOnlyAllowlist", () => { + it("strips allowlist when it only targets plugin tools", () => { + const policy = stripPluginOnlyAllowlist({ allow: ["lobster"] }, pluginGroups); + expect(policy?.allow).toBeUndefined(); + }); + + it("strips allowlist when it only targets plugin groups", () => { + const policy = stripPluginOnlyAllowlist({ allow: ["group:plugins"] }, pluginGroups); + expect(policy?.allow).toBeUndefined(); + }); + + it("keeps allowlist when it mixes plugin and core entries", () => { + const policy = stripPluginOnlyAllowlist({ allow: ["lobster", "read"] }, pluginGroups); + expect(policy?.allow).toEqual(["lobster", "read"]); + }); +}); diff --git a/src/agents/tool-policy.ts b/src/agents/tool-policy.ts index 4988c6877..d5e7e887c 100644 --- a/src/agents/tool-policy.ts +++ b/src/agents/tool-policy.ts @@ -178,6 +178,22 @@ export function expandPolicyWithPluginGroups( }; } +export function stripPluginOnlyAllowlist( + policy: ToolPolicyLike | undefined, + groups: PluginToolGroups, +): ToolPolicyLike | undefined { + if (!policy?.allow || policy.allow.length === 0) return policy; + const normalized = normalizeToolList(policy.allow); + if (normalized.length === 0) return policy; + const pluginIds = new Set(groups.byPlugin.keys()); + const pluginTools = new Set(groups.all); + const isPluginEntry = (entry: string) => + entry === "group:plugins" || pluginIds.has(entry) || pluginTools.has(entry); + const isPluginOnly = normalized.every((entry) => isPluginEntry(entry)); + if (!isPluginOnly) return policy; + return { ...policy, allow: undefined }; +} + export function resolveToolProfilePolicy(profile?: string): ToolProfilePolicy | undefined { if (!profile) return undefined; const resolved = TOOL_PROFILES[profile as ToolProfileId];