fix: keep core tools when allowlist is plugin-only

This commit is contained in:
Peter Steinberger
2026-01-23 09:01:41 +00:00
parent 310a248a44
commit dc07f1e021
6 changed files with 73 additions and 6 deletions

View File

@@ -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

View File

@@ -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` (providerspecific allow/deny)
- `tools.sandbox.tools.*` (sandbox tool policy when sandboxed)

View File

@@ -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:

View File

@@ -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);

View File

@@ -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"]);
});
});

View File

@@ -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];