Skip to content

Commit fadbcf8

Browse files
authored
fix(policy): recognize declared tool allowlists (#89596)
Merged via squash. Prepared head SHA: 3f8628b Co-authored-by: giodl73-repo <[email protected]> Co-authored-by: giodl73-repo <[email protected]> Reviewed-by: @giodl73-repo
1 parent 23eadfa commit fadbcf8

9 files changed

Lines changed: 634 additions & 5 deletions

src/agents/agent-tools.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import {
9292
} from "./tool-description-presets.js";
9393
import { createToolFsPolicy, resolveToolFsConfig } from "./tool-fs-policy.js";
9494
import { resolveToolLoopDetectionConfig } from "./tool-loop-detection-config.js";
95+
import { buildDeclaredToolAllowlistContext } from "./tool-policy-declared-context.js";
9596
import {
9697
applyToolPolicyPipeline,
9798
buildDefaultToolPolicyPipelineSteps,
@@ -1183,6 +1184,11 @@ export function createOpenClawCodingTools(options?: {
11831184
{ policy: inheritedToolPolicy, label: "inherited tools", unavailableCoreToolReason },
11841185
],
11851186
auditLogLevel: options?.toolPolicyAuditLogLevel,
1187+
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
1188+
config: options?.config,
1189+
workspaceDir: workspaceRoot,
1190+
toolDenylist: pluginToolDenylist,
1191+
}),
11861192
});
11871193
if (shouldInheritEffectiveToolAllowlist) {
11881194
replaceWithEffectiveToolAllowlist(inheritedToolAllowlist, subagentFiltered);

src/agents/embedded-agent-runner/effective-tool-policy.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ import {
1515
isSubagentEnvelopeSession,
1616
resolveSubagentCapabilityStore,
1717
} from "../subagent-capabilities.js";
18+
import { buildDeclaredToolAllowlistContext } from "../tool-policy-declared-context.js";
1819
import {
1920
applyToolPolicyPipeline,
2021
buildDefaultToolPolicyPipelineSteps,
2122
type ToolPolicyPipelineStep,
2223
} from "../tool-policy-pipeline.js";
23-
import { mergeAlsoAllowPolicy, resolveToolProfilePolicy } from "../tool-policy.js";
24+
import {
25+
collectExplicitDenylist,
26+
mergeAlsoAllowPolicy,
27+
resolveToolProfilePolicy,
28+
} from "../tool-policy.js";
2429
import type { AnyAgentTool } from "../tools/common.js";
2530

2631
/**
@@ -178,5 +183,9 @@ export function applyFinalEffectiveToolPolicy(
178183
warn: params.warn,
179184
steps: pipelineSteps,
180185
auditLogLevel: params.toolPolicyAuditLogLevel,
186+
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
187+
config: params.config,
188+
toolDenylist: collectExplicitDenylist(pipelineSteps.map((step) => step.policy)),
189+
}),
181190
});
182191
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { isRecord } from "@openclaw/normalization-core/record-coerce";
2+
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
3+
import { normalizeConfiguredMcpServers } from "../config/mcp-config-normalize.js";
4+
import type { OpenClawConfig } from "../config/types.openclaw.js";
5+
import { normalizePluginsConfig } from "../plugins/config-state.js";
6+
import {
7+
isManifestPluginAvailableForControlPlane,
8+
loadManifestMetadataSnapshot,
9+
} from "../plugins/manifest-contract-eligibility.js";
10+
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
11+
import { hasManifestToolAvailability } from "../plugins/manifest-tool-availability.js";
12+
import { sanitizeServerName, TOOL_NAME_SEPARATOR } from "./agent-bundle-mcp-names.js";
13+
import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js";
14+
import type { DeclaredToolAllowlistContext } from "./tool-policy.js";
15+
import { normalizeToolName } from "./tool-policy.js";
16+
17+
type ToolDenylist = ReturnType<typeof compileGlobPatterns>;
18+
19+
function normalizeToolDenylist(list?: string[]): ToolDenylist {
20+
return compileGlobPatterns({ raw: list, normalize: normalizeToolName });
21+
}
22+
23+
function denylistBlocksName(name: string, denylist: ToolDenylist): boolean {
24+
const normalized = normalizeToolName(name);
25+
return normalized ? matchesAnyGlobPattern(normalized, denylist) : false;
26+
}
27+
28+
function denylistBlocksMcpServerNamespace(params: {
29+
safeServerName: string;
30+
denylist: ToolDenylist;
31+
}): boolean {
32+
const serverPrefix = normalizeToolName(params.safeServerName + TOOL_NAME_SEPARATOR);
33+
if (!serverPrefix) {
34+
return false;
35+
}
36+
return matchesAnyGlobPattern(serverPrefix, params.denylist);
37+
}
38+
39+
function denylistBlocksMcpServer(params: {
40+
safeServerName: string;
41+
denylist: ToolDenylist;
42+
}): boolean {
43+
return (
44+
denylistBlocksName("bundle-mcp", params.denylist) ||
45+
matchesAnyGlobPattern("group:plugins", params.denylist) ||
46+
denylistBlocksMcpServerNamespace({
47+
safeServerName: params.safeServerName,
48+
denylist: params.denylist,
49+
})
50+
);
51+
}
52+
53+
function denylistBlocksPlugin(params: { pluginId: string; denylist: ToolDenylist }): boolean {
54+
return (
55+
denylistBlocksName(params.pluginId, params.denylist) ||
56+
matchesAnyGlobPattern("group:plugins", params.denylist)
57+
);
58+
}
59+
60+
function denylistBlocksPluginTool(params: {
61+
pluginId: string;
62+
toolName: string;
63+
denylist: ToolDenylist;
64+
}): boolean {
65+
return (
66+
denylistBlocksPlugin({ pluginId: params.pluginId, denylist: params.denylist }) ||
67+
denylistBlocksName(params.toolName, params.denylist)
68+
);
69+
}
70+
71+
function collectConfiguredMcpServerNames(params: {
72+
config?: OpenClawConfig;
73+
toolDenylist?: string[];
74+
}): string[] {
75+
const servers = normalizeConfiguredMcpServers(params.config?.mcp?.servers);
76+
const denylist = normalizeToolDenylist(params.toolDenylist);
77+
const usedServerNames = new Set<string>();
78+
const names: string[] = [];
79+
for (const [name, value] of Object.entries(servers)) {
80+
if (!isRecord(value) || value.enabled === false || !name.trim()) {
81+
continue;
82+
}
83+
const safeServerName = sanitizeServerName(name, usedServerNames);
84+
if (
85+
denylistBlocksMcpServer({
86+
safeServerName,
87+
denylist,
88+
})
89+
) {
90+
continue;
91+
}
92+
names.push(safeServerName);
93+
}
94+
return names;
95+
}
96+
97+
function collectAvailableManifestToolNames(params: {
98+
plugin: PluginManifestRecord;
99+
config?: OpenClawConfig;
100+
env: NodeJS.ProcessEnv;
101+
denylist: ToolDenylist;
102+
}): string[] {
103+
return (params.plugin.contracts?.tools ?? [])
104+
.filter(
105+
(toolName) =>
106+
!denylistBlocksPluginTool({
107+
pluginId: params.plugin.id,
108+
toolName,
109+
denylist: params.denylist,
110+
}),
111+
)
112+
.filter((toolName) =>
113+
hasManifestToolAvailability({
114+
plugin: params.plugin,
115+
toolNames: [toolName],
116+
config: params.config,
117+
env: params.env,
118+
}),
119+
)
120+
.map(normalizeToolName)
121+
.filter(Boolean);
122+
}
123+
124+
function collectDeclaredPluginContext(params: {
125+
config?: OpenClawConfig;
126+
workspaceDir?: string;
127+
toolDenylist?: string[];
128+
env?: NodeJS.ProcessEnv;
129+
}): Pick<DeclaredToolAllowlistContext, "pluginIds" | "pluginToolNames"> {
130+
if (params.config?.plugins?.enabled === false) {
131+
return {};
132+
}
133+
const env = params.env ?? process.env;
134+
const snapshot = loadManifestMetadataSnapshot({
135+
config: params.config,
136+
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
137+
env,
138+
});
139+
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
140+
const denylist = normalizeToolDenylist(params.toolDenylist);
141+
const pluginIds = new Set<string>();
142+
const pluginToolNames = new Set<string>();
143+
for (const plugin of snapshot.manifestRegistry.plugins) {
144+
if (
145+
!isManifestPluginAvailableForControlPlane({
146+
snapshot,
147+
plugin,
148+
config: params.config,
149+
}) ||
150+
normalizedPlugins.entries[plugin.id]?.enabled === false ||
151+
normalizedPlugins.deny.includes(plugin.id) ||
152+
denylistBlocksPlugin({ pluginId: plugin.id, denylist })
153+
) {
154+
continue;
155+
}
156+
const availableToolNames = collectAvailableManifestToolNames({
157+
plugin,
158+
config: params.config,
159+
env,
160+
denylist,
161+
});
162+
if (availableToolNames.length === 0) {
163+
continue;
164+
}
165+
pluginIds.add(plugin.id);
166+
for (const toolName of availableToolNames) {
167+
pluginToolNames.add(toolName);
168+
}
169+
}
170+
return { pluginIds, pluginToolNames };
171+
}
172+
173+
export function buildDeclaredToolAllowlistContext(params: {
174+
config?: OpenClawConfig;
175+
workspaceDir?: string;
176+
toolDenylist?: string[];
177+
env?: NodeJS.ProcessEnv;
178+
}): DeclaredToolAllowlistContext | undefined {
179+
const mcpServerNames = uniqueStrings(
180+
collectConfiguredMcpServerNames({
181+
config: params.config,
182+
toolDenylist: params.toolDenylist,
183+
}),
184+
);
185+
const pluginContext = collectDeclaredPluginContext(params);
186+
const pluginIds = uniqueStrings(pluginContext.pluginIds ?? []);
187+
const pluginToolNames = uniqueStrings(pluginContext.pluginToolNames ?? []);
188+
if (mcpServerNames.length === 0 && pluginIds.length === 0 && pluginToolNames.length === 0) {
189+
return undefined;
190+
}
191+
return {
192+
...(pluginIds.length > 0 ? { pluginIds } : {}),
193+
...(pluginToolNames.length > 0 ? { pluginToolNames } : {}),
194+
...(mcpServerNames.length > 0 ? { mcpServerNames } : {}),
195+
};
196+
}

0 commit comments

Comments
 (0)