fix(policy): recognize declared tool allowlists (#89596)

Merged via squash.

Prepared head SHA: 3f8628b4fd
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
This commit is contained in:
Gio Della-Libera
2026-06-17 17:01:07 -07:00
committed by GitHub
parent 23eadfa277
commit fadbcf8a4e
9 changed files with 634 additions and 5 deletions

View File

@@ -92,6 +92,7 @@ import {
} from "./tool-description-presets.js";
import { createToolFsPolicy, resolveToolFsConfig } from "./tool-fs-policy.js";
import { resolveToolLoopDetectionConfig } from "./tool-loop-detection-config.js";
import { buildDeclaredToolAllowlistContext } from "./tool-policy-declared-context.js";
import {
applyToolPolicyPipeline,
buildDefaultToolPolicyPipelineSteps,
@@ -1183,6 +1184,11 @@ export function createOpenClawCodingTools(options?: {
{ policy: inheritedToolPolicy, label: "inherited tools", unavailableCoreToolReason },
],
auditLogLevel: options?.toolPolicyAuditLogLevel,
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
config: options?.config,
workspaceDir: workspaceRoot,
toolDenylist: pluginToolDenylist,
}),
});
if (shouldInheritEffectiveToolAllowlist) {
replaceWithEffectiveToolAllowlist(inheritedToolAllowlist, subagentFiltered);

View File

@@ -15,12 +15,17 @@ import {
isSubagentEnvelopeSession,
resolveSubagentCapabilityStore,
} from "../subagent-capabilities.js";
import { buildDeclaredToolAllowlistContext } from "../tool-policy-declared-context.js";
import {
applyToolPolicyPipeline,
buildDefaultToolPolicyPipelineSteps,
type ToolPolicyPipelineStep,
} from "../tool-policy-pipeline.js";
import { mergeAlsoAllowPolicy, resolveToolProfilePolicy } from "../tool-policy.js";
import {
collectExplicitDenylist,
mergeAlsoAllowPolicy,
resolveToolProfilePolicy,
} from "../tool-policy.js";
import type { AnyAgentTool } from "../tools/common.js";
/**
@@ -178,5 +183,9 @@ export function applyFinalEffectiveToolPolicy(
warn: params.warn,
steps: pipelineSteps,
auditLogLevel: params.toolPolicyAuditLogLevel,
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
config: params.config,
toolDenylist: collectExplicitDenylist(pipelineSteps.map((step) => step.policy)),
}),
});
}

View File

@@ -0,0 +1,196 @@
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { normalizeConfiguredMcpServers } from "../config/mcp-config-normalize.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizePluginsConfig } from "../plugins/config-state.js";
import {
isManifestPluginAvailableForControlPlane,
loadManifestMetadataSnapshot,
} from "../plugins/manifest-contract-eligibility.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { hasManifestToolAvailability } from "../plugins/manifest-tool-availability.js";
import { sanitizeServerName, TOOL_NAME_SEPARATOR } from "./agent-bundle-mcp-names.js";
import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js";
import type { DeclaredToolAllowlistContext } from "./tool-policy.js";
import { normalizeToolName } from "./tool-policy.js";
type ToolDenylist = ReturnType<typeof compileGlobPatterns>;
function normalizeToolDenylist(list?: string[]): ToolDenylist {
return compileGlobPatterns({ raw: list, normalize: normalizeToolName });
}
function denylistBlocksName(name: string, denylist: ToolDenylist): boolean {
const normalized = normalizeToolName(name);
return normalized ? matchesAnyGlobPattern(normalized, denylist) : false;
}
function denylistBlocksMcpServerNamespace(params: {
safeServerName: string;
denylist: ToolDenylist;
}): boolean {
const serverPrefix = normalizeToolName(params.safeServerName + TOOL_NAME_SEPARATOR);
if (!serverPrefix) {
return false;
}
return matchesAnyGlobPattern(serverPrefix, params.denylist);
}
function denylistBlocksMcpServer(params: {
safeServerName: string;
denylist: ToolDenylist;
}): boolean {
return (
denylistBlocksName("bundle-mcp", params.denylist) ||
matchesAnyGlobPattern("group:plugins", params.denylist) ||
denylistBlocksMcpServerNamespace({
safeServerName: params.safeServerName,
denylist: params.denylist,
})
);
}
function denylistBlocksPlugin(params: { pluginId: string; denylist: ToolDenylist }): boolean {
return (
denylistBlocksName(params.pluginId, params.denylist) ||
matchesAnyGlobPattern("group:plugins", params.denylist)
);
}
function denylistBlocksPluginTool(params: {
pluginId: string;
toolName: string;
denylist: ToolDenylist;
}): boolean {
return (
denylistBlocksPlugin({ pluginId: params.pluginId, denylist: params.denylist }) ||
denylistBlocksName(params.toolName, params.denylist)
);
}
function collectConfiguredMcpServerNames(params: {
config?: OpenClawConfig;
toolDenylist?: string[];
}): string[] {
const servers = normalizeConfiguredMcpServers(params.config?.mcp?.servers);
const denylist = normalizeToolDenylist(params.toolDenylist);
const usedServerNames = new Set<string>();
const names: string[] = [];
for (const [name, value] of Object.entries(servers)) {
if (!isRecord(value) || value.enabled === false || !name.trim()) {
continue;
}
const safeServerName = sanitizeServerName(name, usedServerNames);
if (
denylistBlocksMcpServer({
safeServerName,
denylist,
})
) {
continue;
}
names.push(safeServerName);
}
return names;
}
function collectAvailableManifestToolNames(params: {
plugin: PluginManifestRecord;
config?: OpenClawConfig;
env: NodeJS.ProcessEnv;
denylist: ToolDenylist;
}): string[] {
return (params.plugin.contracts?.tools ?? [])
.filter(
(toolName) =>
!denylistBlocksPluginTool({
pluginId: params.plugin.id,
toolName,
denylist: params.denylist,
}),
)
.filter((toolName) =>
hasManifestToolAvailability({
plugin: params.plugin,
toolNames: [toolName],
config: params.config,
env: params.env,
}),
)
.map(normalizeToolName)
.filter(Boolean);
}
function collectDeclaredPluginContext(params: {
config?: OpenClawConfig;
workspaceDir?: string;
toolDenylist?: string[];
env?: NodeJS.ProcessEnv;
}): Pick<DeclaredToolAllowlistContext, "pluginIds" | "pluginToolNames"> {
if (params.config?.plugins?.enabled === false) {
return {};
}
const env = params.env ?? process.env;
const snapshot = loadManifestMetadataSnapshot({
config: params.config,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
env,
});
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
const denylist = normalizeToolDenylist(params.toolDenylist);
const pluginIds = new Set<string>();
const pluginToolNames = new Set<string>();
for (const plugin of snapshot.manifestRegistry.plugins) {
if (
!isManifestPluginAvailableForControlPlane({
snapshot,
plugin,
config: params.config,
}) ||
normalizedPlugins.entries[plugin.id]?.enabled === false ||
normalizedPlugins.deny.includes(plugin.id) ||
denylistBlocksPlugin({ pluginId: plugin.id, denylist })
) {
continue;
}
const availableToolNames = collectAvailableManifestToolNames({
plugin,
config: params.config,
env,
denylist,
});
if (availableToolNames.length === 0) {
continue;
}
pluginIds.add(plugin.id);
for (const toolName of availableToolNames) {
pluginToolNames.add(toolName);
}
}
return { pluginIds, pluginToolNames };
}
export function buildDeclaredToolAllowlistContext(params: {
config?: OpenClawConfig;
workspaceDir?: string;
toolDenylist?: string[];
env?: NodeJS.ProcessEnv;
}): DeclaredToolAllowlistContext | undefined {
const mcpServerNames = uniqueStrings(
collectConfiguredMcpServerNames({
config: params.config,
toolDenylist: params.toolDenylist,
}),
);
const pluginContext = collectDeclaredPluginContext(params);
const pluginIds = uniqueStrings(pluginContext.pluginIds ?? []);
const pluginToolNames = uniqueStrings(pluginContext.pluginToolNames ?? []);
if (mcpServerNames.length === 0 && pluginIds.length === 0 && pluginToolNames.length === 0) {
return undefined;
}
return {
...(pluginIds.length > 0 ? { pluginIds } : {}),
...(pluginToolNames.length > 0 ? { pluginToolNames } : {}),
...(mcpServerNames.length > 0 ? { mcpServerNames } : {}),
};
}

View File

@@ -1,6 +1,7 @@
// Tool policy pipeline tests cover profile/allowlist filtering, diagnostics,
// warning dedupe, and plugin-aware policy application.
import { beforeEach, describe, expect, test, vi } from "vitest";
import { buildDeclaredToolAllowlistContext } from "./tool-policy-declared-context.js";
import {
applyToolPolicyPipeline,
buildDefaultToolPolicyPipelineSteps,
@@ -154,6 +155,308 @@ describe("tool-policy-pipeline", () => {
expect(warnings).toStrictEqual([]);
});
test("does not warn for declared plugin tools that are not materialized yet", () => {
const warnings: string[] = [];
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: { pluginToolNames: ["llm-task"] },
steps: [
{
policy: { allow: ["llm-task"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toStrictEqual([]);
});
test("does not warn for declared MCP server namespace globs", () => {
const warnings: string[] = [];
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: { mcpServerNames: ["paperless", "Home Assistant"] },
steps: [
{
policy: { allow: ["paperless__*", "home-assistant__search"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toStrictEqual([]);
});
test("still warns for undeclared MCP namespace globs", () => {
const warnings: string[] = [];
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: { mcpServerNames: ["paperless"] },
steps: [
{
policy: { allow: ["papreless__*"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([
"tools: tools.allow allowlist contains unknown entries (papreless__*). These entries won't match any tool unless the plugin is enabled.",
]);
});
test("declared context excludes disabled plugin tools", () => {
const declared = buildDeclaredToolAllowlistContext({
config: { plugins: { entries: { browser: { enabled: false } } } },
workspaceDir: process.cwd(),
});
expect(Array.from(declared?.pluginToolNames ?? [])).not.toContain("browser");
});
test("declared context excludes denied plugin tools", () => {
const declared = buildDeclaredToolAllowlistContext({
config: { plugins: { entries: { browser: { enabled: true } } } },
workspaceDir: process.cwd(),
toolDenylist: ["browser"],
});
expect(Array.from(declared?.pluginToolNames ?? [])).not.toContain("browser");
});
test("declared context excludes disabled MCP servers", () => {
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: {
servers: {
paperless: { command: "paperless-mcp" },
disabled: { command: "disabled-mcp", enabled: false },
},
},
},
workspaceDir: process.cwd(),
});
expect(Array.from(declared?.mcpServerNames ?? [])).toContain("paperless");
expect(Array.from(declared?.mcpServerNames ?? [])).not.toContain("disabled");
});
test("warns when disabled MCP server namespace is allowlisted", () => {
const warnings: string[] = [];
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: { servers: { disabled: { command: "disabled-mcp", enabled: false } } },
},
workspaceDir: process.cwd(),
});
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: declared,
steps: [
{
policy: { allow: ["disabled__*"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([
"tools: tools.allow allowlist contains unknown entries (disabled__*). These entries won't match any tool unless the plugin is enabled.",
]);
});
test("warns when bundle MCP is denied and allowlisted", () => {
const warnings: string[] = [];
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: { servers: { paperless: { command: "paperless-mcp" } } },
},
workspaceDir: process.cwd(),
toolDenylist: ["bundle-mcp"],
});
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: declared,
steps: [
{
policy: { allow: ["bundle-mcp"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([
"tools: tools.allow allowlist contains unknown entries (bundle-mcp). These entries won't match any tool unless the plugin is enabled.",
]);
});
test("warns when denied MCP server namespace is allowlisted", () => {
const warnings: string[] = [];
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: { servers: { paperless: { command: "paperless-mcp" } } },
},
workspaceDir: process.cwd(),
toolDenylist: ["paperless__*"],
});
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: declared,
steps: [
{
policy: { allow: ["paperless__*"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([
"tools: tools.allow allowlist contains unknown entries (paperless__*). These entries won't match any tool unless the plugin is enabled.",
]);
});
test("warns when broad MCP server wildcard deny covers an allowlisted namespace", () => {
const warnings: string[] = [];
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: { servers: { paperless: { command: "paperless-mcp" } } },
},
workspaceDir: process.cwd(),
toolDenylist: ["paperless*"],
});
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: declared,
steps: [
{
policy: { allow: ["paperless__*"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([
"tools: tools.allow allowlist contains unknown entries (paperless__*). These entries won't match any tool unless the plugin is enabled.",
]);
});
test("does not warn for MCP server namespace allowlist when one exact server tool is denied", () => {
const warnings: string[] = [];
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: { servers: { paperless: { command: "paperless-mcp" } } },
},
workspaceDir: process.cwd(),
toolDenylist: ["paperless__delete"],
});
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: declared,
steps: [
{
policy: { allow: ["paperless__*"], deny: ["paperless__delete"] },
label: "tools",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([]);
});
test("warns when plugin group is denied and MCP server namespace is allowlisted", () => {
const warnings: string[] = [];
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: { servers: { paperless: { command: "paperless-mcp" } } },
},
workspaceDir: process.cwd(),
toolDenylist: ["group:plugins"],
});
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: declared,
steps: [
{
policy: { allow: ["paperless__*"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([
"tools: tools.allow allowlist contains unknown entries (paperless__*). These entries won't match any tool unless the plugin is enabled.",
]);
});
test("warns when denied duplicate-safe MCP server namespace is allowlisted", () => {
const warnings: string[] = [];
const declared = buildDeclaredToolAllowlistContext({
config: {
mcp: {
servers: {
"vigil harbor": { command: "vigil-mcp" },
"vigil:harbor": { command: "vigil-alt-mcp" },
},
},
},
workspaceDir: process.cwd(),
toolDenylist: ["vigil-harbor-2__*"],
});
expect(Array.from(declared?.mcpServerNames ?? [])).toEqual(["vigil-harbor"]);
applyToolPolicyPipeline({
tools: [{ name: "exec" }] as any,
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
declaredToolAllowlist: declared,
steps: [
{
policy: { allow: ["vigil-harbor__*", "vigil-harbor-2__*"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
});
expect(warnings).toEqual([
"tools: tools.allow allowlist contains unknown entries (vigil-harbor-2__*). These entries won't match any tool unless the plugin is enabled.",
]);
});
test("dedupes identical unknown-allowlist warnings across repeated runs", () => {
const warnings: string[] = [];
const tools = [{ name: "exec" }] as unknown as DummyTool[];

View File

@@ -12,6 +12,7 @@ import {
buildPluginToolGroups,
expandPolicyWithPluginGroups,
normalizeToolName,
type DeclaredToolAllowlistContext,
type ToolPolicyLike,
} from "./tool-policy.js";
@@ -129,6 +130,7 @@ export function applyToolPolicyPipeline(params: {
warn: (message: string) => void;
steps: ToolPolicyPipelineStep[];
auditLogLevel?: ToolPolicyAuditLogLevel;
declaredToolAllowlist?: DeclaredToolAllowlistContext;
}): AnyAgentTool[] {
const coreToolNames = new Set(
params.tools
@@ -151,7 +153,12 @@ export function applyToolPolicyPipeline(params: {
let policy: ToolPolicyLike | undefined = step.policy;
if (step.stripPluginOnlyAllowlist) {
// Plugin-only allowlists are valid for deferred tools; warn only for entries that cannot match.
const resolved = analyzeAllowlistByToolType(policy, pluginGroups, coreToolNames);
const resolved = analyzeAllowlistByToolType(
policy,
pluginGroups,
coreToolNames,
params.declaredToolAllowlist,
);
if (resolved.unknownAllowlist.length > 0) {
const unavailableCoreWarningAllowlist = new Set(
(step.suppressUnavailableCoreToolWarningAllowlist ?? []).map((entry) =>

View File

@@ -75,6 +75,40 @@ describe("analyzeAllowlistByToolType", () => {
expect(policy.unknownAllowlist).toEqual(["apply_patch"]);
});
it("recognizes declared plugin tools before they are materialized", () => {
const emptyPlugins: PluginToolGroups = { all: [], byPlugin: new Map() };
const policy = analyzeAllowlistByToolType({ allow: ["llm-task"] }, emptyPlugins, coreTools, {
pluginToolNames: ["llm-task"],
});
expect(policy.policy?.allow).toEqual(["llm-task"]);
expect(policy.pluginOnlyAllowlist).toBe(true);
expect(policy.unknownAllowlist).toStrictEqual([]);
});
it("recognizes declared MCP server namespace allowlists before tools are materialized", () => {
const emptyPlugins: PluginToolGroups = { all: [], byPlugin: new Map() };
const policy = analyzeAllowlistByToolType(
{ allow: ["paperless__*", "home-assistant__search"] },
emptyPlugins,
coreTools,
{ mcpServerNames: ["paperless", "Home Assistant"] },
);
expect(policy.pluginOnlyAllowlist).toBe(true);
expect(policy.unknownAllowlist).toStrictEqual([]);
});
it("still reports undeclared MCP namespace allowlist typos", () => {
const emptyPlugins: PluginToolGroups = { all: [], byPlugin: new Map() };
const policy = analyzeAllowlistByToolType(
{ allow: ["papreless__*"] },
emptyPlugins,
coreTools,
{ mcpServerNames: ["paperless"] },
);
expect(policy.pluginOnlyAllowlist).toBe(false);
expect(policy.unknownAllowlist).toStrictEqual(["papreless__*"]);
});
it("ignores empty plugin ids when building groups", () => {
const groups = buildPluginToolGroups({
tools: [{ name: "lobster" }],

View File

@@ -5,6 +5,7 @@
*/
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { sanitizeServerName, TOOL_NAME_SEPARATOR } from "./agent-bundle-mcp-names.js";
import { IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW } from "./sandbox-tool-policy.js";
import { expandToolGroups, normalizeToolList, normalizeToolName } from "./tool-policy-shared.js";
export {
@@ -37,6 +38,12 @@ type AllowlistResolution = {
pluginOnlyAllowlist: boolean;
};
export type DeclaredToolAllowlistContext = {
pluginToolNames?: Iterable<string>;
pluginIds?: Iterable<string>;
mcpServerNames?: Iterable<string>;
};
/** Synthetic allowlist entry that means "use default plugin tools". */
export const DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY = "__openclaw_default_plugin_tools__";
@@ -188,11 +195,56 @@ export function expandPolicyWithPluginGroups(
};
}
function buildDeclaredMcpToolPrefixes(serverNames?: Iterable<string>): Set<string> {
const prefixes = new Set<string>();
const usedNames = new Set<string>();
for (const serverName of serverNames ?? []) {
const safeName = sanitizeServerName(serverName, usedNames);
const prefix = normalizeToolName(safeName + TOOL_NAME_SEPARATOR);
if (prefix) {
prefixes.add(prefix);
}
}
return prefixes;
}
function normalizeDeclaredPluginIds(values?: Iterable<string>): Set<string> {
return new Set(
Array.from(values ?? [], (value) => normalizeOptionalLowercaseString(value)).filter(
(value): value is string => Boolean(value),
),
);
}
function normalizeDeclaredToolNames(values?: Iterable<string>): Set<string> {
return new Set(
Array.from(values ?? [], (value) => normalizeToolName(value)).filter((value): value is string =>
Boolean(value),
),
);
}
function isDeclaredMcpAllowlistEntry(entry: string, prefixes: Set<string>): boolean {
if (prefixes.size === 0) {
return false;
}
if (entry === "bundle-mcp") {
return true;
}
for (const prefix of prefixes) {
if (entry.length > prefix.length && entry.startsWith(prefix)) {
return true;
}
}
return false;
}
/** Classifies allowlists as core, plugin-only, or unknown for diagnostics. */
export function analyzeAllowlistByToolType(
policy: ToolPolicyLike | undefined,
groups: PluginToolGroups,
coreTools: Set<string>,
declaredTools?: DeclaredToolAllowlistContext,
): AllowlistResolution {
if (!policy?.allow || policy.allow.length === 0) {
return { policy, unknownAllowlist: [], pluginOnlyAllowlist: false };
@@ -201,8 +253,15 @@ export function analyzeAllowlistByToolType(
if (normalized.length === 0) {
return { policy, unknownAllowlist: [], pluginOnlyAllowlist: false };
}
const pluginIds = new Set(groups.byPlugin.keys());
const pluginTools = new Set(groups.all);
const pluginIds = new Set([
...groups.byPlugin.keys(),
...normalizeDeclaredPluginIds(declaredTools?.pluginIds),
]);
const pluginTools = new Set([
...groups.all,
...normalizeDeclaredToolNames(declaredTools?.pluginToolNames),
]);
const mcpToolPrefixes = buildDeclaredMcpToolPrefixes(declaredTools?.mcpServerNames);
const unknownAllowlist: string[] = [];
let hasOnlyPluginEntries = true;
for (const entry of normalized) {
@@ -211,7 +270,10 @@ export function analyzeAllowlistByToolType(
continue;
}
const isPluginEntry =
entry === "group:plugins" || pluginIds.has(entry) || pluginTools.has(entry);
entry === "group:plugins" ||
pluginIds.has(entry) ||
pluginTools.has(entry) ||
isDeclaredMcpAllowlistEntry(entry, mcpToolPrefixes);
const expanded = expandToolGroups([entry]);
const isCoreEntry = expanded.some((tool) => coreTools.has(tool));
if (!isPluginEntry) {

View File

@@ -11,6 +11,7 @@ import {
isSubagentEnvelopeSession,
resolveSubagentCapabilityStore,
} from "../agents/subagent-capabilities.js";
import { buildDeclaredToolAllowlistContext } from "../agents/tool-policy-declared-context.js";
import {
applyToolPolicyPipeline,
buildDefaultToolPolicyPipelineSteps,
@@ -232,6 +233,11 @@ export function resolveGatewayScopedTools(params: {
{ policy: subagentPolicy, label: "subagent tools.allow" },
{ policy: inheritedToolPolicy, label: "inherited tools" },
],
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
config: params.cfg,
workspaceDir,
toolDenylist: explicitDenylist,
}),
});
const gatewayDenySet = new Set([

View File

@@ -13,6 +13,7 @@ import {
isSubagentEnvelopeSession,
resolveSubagentCapabilityStore,
} from "../../agents/subagent-capabilities.js";
import { buildDeclaredToolAllowlistContext } from "../../agents/tool-policy-declared-context.js";
import {
applyToolPolicyPipeline,
buildDefaultToolPolicyPipelineSteps,
@@ -230,6 +231,11 @@ export function resolveSkillDispatchTools(params: {
{ policy: subagentPolicy, label: "subagent tools.allow" },
{ policy: inheritedToolPolicy, label: "inherited tools" },
],
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
config: params.cfg,
workspaceDir: params.workspaceDir,
toolDenylist: explicitDenylist,
}),
});
if (explicitPolicyList.some(hasRestrictiveAllowPolicy)) {
replaceWithEffectiveToolAllowlist(inheritedToolAllowlist, policyFiltered);