mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(plugins): honor plugin tool denylists
This commit is contained in:
@@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/sessions: memoize repeated thinking-option enrichment and skip unused cost fallback checks while listing sessions, reducing per-row work on large multi-agent stores. Fixes #76931.
|
||||
- Agents/tools: use config-only runtime snapshots for plugin tool registration and live runtime config getters, avoiding expensive full secrets snapshot clones on the core-plugin-tools prep path. Fixes #76295.
|
||||
- Agents/tools: honor the effective tool denylist before constructing optional PDF/media tool factories, so `tools.deny: ["pdf"]` skips PDF setup before later policy filtering. Fixes #76997.
|
||||
- Plugin tools: honor explicit tool denylists while selecting plugin tool runtimes, so denied plugin tools are not materialized for direct command or gateway surfaces before later policy filtering. Thanks @vincentkoc.
|
||||
- Agents/bootstrap: keep pending `BOOTSTRAP.md` and bootstrap truncation notices in system-prompt Project Context instead of copying setup text or raw warning diagnostics into WebChat user/runtime context. Fixes #76946.
|
||||
- Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc.
|
||||
- Gateway/install: keep `.env`-managed values in the macOS LaunchAgent env file while still tracking `OPENCLAW_SERVICE_MANAGED_ENV_KEYS`, so regenerated services do not boot without managed auth/provider keys. Fixes #75374.
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
type ResolveOpenClawPluginToolsOptions = OpenClawPluginToolOptions & {
|
||||
pluginToolAllowlist?: string[];
|
||||
pluginToolDenylist?: string[];
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string | number;
|
||||
@@ -81,6 +82,7 @@ export function resolveOpenClawPluginToolsForOptions(params: {
|
||||
}),
|
||||
existingToolNames: params.existingToolNames ?? new Set<string>(),
|
||||
toolAllowlist: params.options?.pluginToolAllowlist,
|
||||
toolDenylist: params.options?.pluginToolDenylist,
|
||||
allowGatewaySubagentBinding: params.options?.allowGatewaySubagentBinding,
|
||||
...(authProfileStore
|
||||
? {
|
||||
|
||||
@@ -142,6 +142,31 @@ describe("createOpenClawTools browser plugin integration", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards plugin tool deny policy to plugin resolution", () => {
|
||||
hoisted.resolvePluginTools.mockReturnValue([]);
|
||||
const config = {
|
||||
plugins: {
|
||||
allow: ["browser"],
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
resolveOpenClawPluginToolsForOptions({
|
||||
options: {
|
||||
config,
|
||||
pluginToolAllowlist: ["*"],
|
||||
pluginToolDenylist: ["browser"],
|
||||
},
|
||||
resolvedConfig: config,
|
||||
});
|
||||
|
||||
expect(hoisted.resolvePluginTools).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
toolAllowlist: ["*"],
|
||||
toolDenylist: ["browser"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not pass a stale active snapshot as plugin runtime config for a resolved run config", () => {
|
||||
const staleSourceConfig = {
|
||||
plugins: {
|
||||
|
||||
@@ -649,6 +649,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
|
||||
sandboxed: !!sandbox,
|
||||
pluginToolAllowlist,
|
||||
pluginToolDenylist,
|
||||
currentChannelId: options?.currentChannelId,
|
||||
currentThreadTs: options?.currentThreadTs,
|
||||
currentMessageId: options?.currentMessageId,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "../agents/tool-policy-pipeline.js";
|
||||
import {
|
||||
collectExplicitAllowlist,
|
||||
collectExplicitDenylist,
|
||||
mergeAlsoAllowPolicy,
|
||||
resolveToolProfilePolicy,
|
||||
} from "../agents/tool-policy.js";
|
||||
@@ -108,6 +109,16 @@ export function resolveGatewayScopedTools(params: {
|
||||
subagentPolicy,
|
||||
gatewayRequestedTools.length > 0 ? { allow: gatewayRequestedTools } : undefined,
|
||||
]),
|
||||
pluginToolDenylist: collectExplicitDenylist([
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
subagentPolicy,
|
||||
]),
|
||||
});
|
||||
|
||||
const policyFiltered = applyToolPolicyPipeline({
|
||||
|
||||
@@ -68,6 +68,7 @@ function createContext() {
|
||||
function createResolveToolsParams(params?: {
|
||||
context?: ReturnType<typeof createContext> & Record<string, unknown>;
|
||||
toolAllowlist?: readonly string[];
|
||||
toolDenylist?: readonly string[];
|
||||
existingToolNames?: Set<string>;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
suppressNameConflicts?: boolean;
|
||||
@@ -76,6 +77,7 @@ function createResolveToolsParams(params?: {
|
||||
return {
|
||||
context: (params?.context ?? createContext()) as never,
|
||||
...(params?.toolAllowlist ? { toolAllowlist: [...params.toolAllowlist] } : {}),
|
||||
...(params?.toolDenylist ? { toolDenylist: [...params.toolDenylist] } : {}),
|
||||
...(params?.existingToolNames ? { existingToolNames: params.existingToolNames } : {}),
|
||||
...(params?.env ? { env: params.env } : {}),
|
||||
...(params?.suppressNameConflicts ? { suppressNameConflicts: true } : {}),
|
||||
@@ -2177,6 +2179,30 @@ describe("resolvePluginTools optional tools", () => {
|
||||
expectResolvedToolNames(tools, ["browser"]);
|
||||
});
|
||||
|
||||
it("does not materialize plugin tools blocked by explicit deny policy", () => {
|
||||
const browserFactory = vi.fn(() => makeTool("browser"));
|
||||
const browserEntry: MockRegistryToolEntry = {
|
||||
pluginId: "browser",
|
||||
optional: false,
|
||||
source: "/tmp/browser.js",
|
||||
names: ["browser"],
|
||||
declaredNames: ["browser"],
|
||||
factory: browserFactory,
|
||||
};
|
||||
setRegistry([browserEntry]);
|
||||
|
||||
const tools = resolvePluginTools(
|
||||
createResolveToolsParams({
|
||||
toolAllowlist: ["*"],
|
||||
toolDenylist: ["browser"],
|
||||
}),
|
||||
);
|
||||
|
||||
expectResolvedToolNames(tools, []);
|
||||
expect(browserFactory).not.toHaveBeenCalled();
|
||||
expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("includes optional tools when wildcard allowlist is active (#76507)", () => {
|
||||
setOptionalDemoRegistry();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { compileGlobPatterns, matchesAnyGlobPattern } from "../agents/glob-pattern.js";
|
||||
import { DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, normalizeToolName } from "../agents/tool-policy.js";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
@@ -86,6 +87,39 @@ function normalizeAllowlist(list?: string[]) {
|
||||
return new Set((list ?? []).map(normalizeToolName).filter(Boolean));
|
||||
}
|
||||
|
||||
function normalizeDenylist(list?: string[]) {
|
||||
return compileGlobPatterns({
|
||||
raw: list,
|
||||
normalize: normalizeToolName,
|
||||
});
|
||||
}
|
||||
|
||||
function denylistBlocksName(name: string, denylist: ReturnType<typeof normalizeDenylist>): boolean {
|
||||
const normalized = normalizeToolName(name);
|
||||
return normalized ? matchesAnyGlobPattern(normalized, denylist) : false;
|
||||
}
|
||||
|
||||
function denylistBlocksPlugin(params: {
|
||||
pluginId: string;
|
||||
denylist: ReturnType<typeof normalizeDenylist>;
|
||||
}): boolean {
|
||||
return (
|
||||
denylistBlocksName(params.pluginId, params.denylist) ||
|
||||
matchesAnyGlobPattern("group:plugins", params.denylist)
|
||||
);
|
||||
}
|
||||
|
||||
function denylistBlocksPluginTool(params: {
|
||||
pluginId: string;
|
||||
toolName: string;
|
||||
denylist: ReturnType<typeof normalizeDenylist>;
|
||||
}): boolean {
|
||||
return (
|
||||
denylistBlocksPlugin({ pluginId: params.pluginId, denylist: params.denylist }) ||
|
||||
denylistBlocksName(params.toolName, params.denylist)
|
||||
);
|
||||
}
|
||||
|
||||
function allowlistIncludesDefaultPluginTools(allowlist: Set<string>): boolean {
|
||||
return allowlist.size === 0 || allowlist.has(DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY);
|
||||
}
|
||||
@@ -331,15 +365,6 @@ function listManifestToolNamesForAllowlist(params: {
|
||||
return [...new Set([...defaultToolNames, ...matchedToolNames])];
|
||||
}
|
||||
|
||||
function manifestToolContractMatchesAllowlist(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
toolNames: readonly string[];
|
||||
pluginId: string;
|
||||
allowlist: Set<string>;
|
||||
}): boolean {
|
||||
return listManifestToolNamesForAllowlist(params).length > 0;
|
||||
}
|
||||
|
||||
function listManifestToolNamesForAvailability(params: {
|
||||
plugin: PluginManifestRecord;
|
||||
toolNames: readonly string[];
|
||||
@@ -389,11 +414,13 @@ function resolvePluginToolRuntimePluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
toolAllowlist?: string[];
|
||||
toolDenylist?: string[];
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
snapshot?: PluginMetadataManifestView;
|
||||
}): string[] {
|
||||
const pluginIds = new Set<string>();
|
||||
const allowlist = normalizeAllowlist(params.toolAllowlist);
|
||||
const denylist = normalizeDenylist(params.toolDenylist);
|
||||
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
|
||||
const snapshot =
|
||||
params.snapshot ??
|
||||
@@ -418,22 +445,28 @@ function resolvePluginToolRuntimePluginIds(params: {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (denylistBlocksPlugin({ pluginId: plugin.id, denylist })) {
|
||||
continue;
|
||||
}
|
||||
const toolNames = plugin.contracts?.tools ?? [];
|
||||
const selectedToolNames = listManifestToolNamesForAvailability({
|
||||
toolNames,
|
||||
plugin,
|
||||
pluginId: plugin.id,
|
||||
allowlist,
|
||||
}).filter(
|
||||
(toolName) =>
|
||||
!denylistBlocksPluginTool({
|
||||
pluginId: plugin.id,
|
||||
toolName,
|
||||
denylist,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
manifestToolContractMatchesAllowlist({
|
||||
plugin,
|
||||
toolNames,
|
||||
pluginId: plugin.id,
|
||||
allowlist,
|
||||
}) &&
|
||||
selectedToolNames.length > 0 &&
|
||||
hasManifestToolAvailability({
|
||||
plugin,
|
||||
toolNames: listManifestToolNamesForAvailability({
|
||||
toolNames,
|
||||
plugin,
|
||||
pluginId: plugin.id,
|
||||
allowlist,
|
||||
}),
|
||||
toolNames: selectedToolNames,
|
||||
config: params.availabilityConfig ?? params.config,
|
||||
env: params.env,
|
||||
hasAuthForProvider: params.hasAuthForProvider,
|
||||
@@ -553,6 +586,7 @@ function resolveCachedPluginTools(params: {
|
||||
availabilityConfig: PluginLoadOptions["config"];
|
||||
env: NodeJS.ProcessEnv;
|
||||
allowlist: Set<string>;
|
||||
denylist: ReturnType<typeof normalizeDenylist>;
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
onlyPluginIds: readonly string[];
|
||||
existing: Set<string>;
|
||||
@@ -570,6 +604,9 @@ function resolveCachedPluginTools(params: {
|
||||
if (!onlyPluginIdSet.has(plugin.id)) {
|
||||
continue;
|
||||
}
|
||||
if (denylistBlocksPlugin({ pluginId: plugin.id, denylist: params.denylist })) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!isManifestPluginAvailableForControlPlane({
|
||||
snapshot: params.snapshot,
|
||||
@@ -585,7 +622,14 @@ function resolveCachedPluginTools(params: {
|
||||
toolNames: contractToolNames,
|
||||
pluginId: plugin.id,
|
||||
allowlist: params.allowlist,
|
||||
});
|
||||
}).filter(
|
||||
(toolName) =>
|
||||
!denylistBlocksPluginTool({
|
||||
pluginId: plugin.id,
|
||||
toolName,
|
||||
denylist: params.denylist,
|
||||
}),
|
||||
);
|
||||
const availableToolNames = filterManifestToolNamesForAvailability({
|
||||
plugin,
|
||||
toolNames: allowedToolNames,
|
||||
@@ -639,6 +683,15 @@ function resolveCachedPluginTools(params: {
|
||||
continue;
|
||||
}
|
||||
const normalizedDescriptorName = normalizeToolName(cachedDescriptor.descriptor.name);
|
||||
if (
|
||||
denylistBlocksPluginTool({
|
||||
pluginId: plugin.id,
|
||||
toolName: cachedDescriptor.descriptor.name,
|
||||
denylist: params.denylist,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
localNormalizedNames.has(normalizedDescriptorName) ||
|
||||
params.existingNormalized.has(normalizedDescriptorName)
|
||||
@@ -732,6 +785,7 @@ function registryHasScopedPluginTools(
|
||||
function resolvePluginToolLoadState(params: {
|
||||
context: OpenClawPluginToolContext;
|
||||
toolAllowlist?: string[];
|
||||
toolDenylist?: string[];
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -771,6 +825,7 @@ function resolvePluginToolLoadState(params: {
|
||||
workspaceDir: context.workspaceDir,
|
||||
env,
|
||||
toolAllowlist: params.toolAllowlist,
|
||||
toolDenylist: params.toolDenylist,
|
||||
hasAuthForProvider: params.hasAuthForProvider,
|
||||
snapshot,
|
||||
});
|
||||
@@ -786,6 +841,7 @@ function resolvePluginToolLoadState(params: {
|
||||
export function ensureStandalonePluginToolRegistryLoaded(params: {
|
||||
context: OpenClawPluginToolContext;
|
||||
toolAllowlist?: string[];
|
||||
toolDenylist?: string[];
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -805,6 +861,7 @@ export function resolvePluginTools(params: {
|
||||
context: OpenClawPluginToolContext;
|
||||
existingToolNames?: Set<string>;
|
||||
toolAllowlist?: string[];
|
||||
toolDenylist?: string[];
|
||||
suppressNameConflicts?: boolean;
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
hasAuthForProvider?: (providerId: string) => boolean;
|
||||
@@ -821,6 +878,7 @@ export function resolvePluginTools(params: {
|
||||
const existing = params.existingToolNames ?? new Set<string>();
|
||||
const existingNormalized = new Set(Array.from(existing, (tool) => normalizeToolName(tool)));
|
||||
const allowlist = normalizeAllowlist(params.toolAllowlist);
|
||||
const denylist = normalizeDenylist(params.toolDenylist);
|
||||
const configCacheKeyMemo = createPluginToolDescriptorConfigCacheKeyMemo();
|
||||
let currentRuntimeConfigForDescriptorCache: PluginLoadOptions["config"] | null | undefined =
|
||||
params.context.runtimeConfig;
|
||||
@@ -837,6 +895,7 @@ export function resolvePluginTools(params: {
|
||||
availabilityConfig: params.context.runtimeConfig ?? context.config,
|
||||
env,
|
||||
allowlist,
|
||||
denylist,
|
||||
hasAuthForProvider: params.hasAuthForProvider,
|
||||
onlyPluginIds,
|
||||
existing,
|
||||
@@ -919,6 +978,9 @@ export function resolvePluginTools(params: {
|
||||
if (!scopedPluginIds.has(entry.pluginId)) {
|
||||
continue;
|
||||
}
|
||||
if (denylistBlocksPlugin({ pluginId: entry.pluginId, denylist })) {
|
||||
continue;
|
||||
}
|
||||
if (blockedPlugins.has(entry.pluginId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -948,7 +1010,14 @@ export function resolvePluginTools(params: {
|
||||
config: params.context.runtimeConfig ?? context.config,
|
||||
env,
|
||||
hasAuthForProvider: params.hasAuthForProvider,
|
||||
})
|
||||
}).filter(
|
||||
(toolName) =>
|
||||
!denylistBlocksPluginTool({
|
||||
pluginId: entry.pluginId,
|
||||
toolName,
|
||||
denylist,
|
||||
}),
|
||||
)
|
||||
: declaredNames;
|
||||
if (manifestPlugin && availabilityNames.length > 0 && allowlistNames.length === 0) {
|
||||
continue;
|
||||
@@ -995,15 +1064,23 @@ export function resolvePluginTools(params: {
|
||||
}),
|
||||
)
|
||||
: listRaw;
|
||||
const policyAvailableList = availableList.filter(
|
||||
(tool) =>
|
||||
!denylistBlocksPluginTool({
|
||||
pluginId: entry.pluginId,
|
||||
toolName: readPluginToolName(tool),
|
||||
denylist,
|
||||
}),
|
||||
);
|
||||
const list = entry.optional
|
||||
? availableList.filter((tool) =>
|
||||
? policyAvailableList.filter((tool) =>
|
||||
isOptionalToolAllowed({
|
||||
toolName: readPluginToolName(tool),
|
||||
pluginId: entry.pluginId,
|
||||
allowlist,
|
||||
}),
|
||||
)
|
||||
: availableList;
|
||||
: policyAvailableList;
|
||||
if (list.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -1087,7 +1164,14 @@ export function resolvePluginTools(params: {
|
||||
toolNames: manifestPlugin.contracts?.tools ?? [],
|
||||
pluginId,
|
||||
allowlist,
|
||||
});
|
||||
}).filter(
|
||||
(toolName) =>
|
||||
!denylistBlocksPluginTool({
|
||||
pluginId,
|
||||
toolName,
|
||||
denylist,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
cachedDescriptorsCoverToolNames({
|
||||
descriptors,
|
||||
|
||||
Reference in New Issue
Block a user