Merge pull request #2843 from QwenLM/fix/coretools-mcp-bypass

fix(permissions): allow non-core tools to bypass coreTools allowlist
This commit is contained in:
tanzhenxin 2026-04-05 14:09:53 +08:00 committed by GitHub
commit 4e5a8a759b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 93 additions and 3 deletions

View file

@ -1254,6 +1254,57 @@ describe('PermissionManager', () => {
expect(await pm.isToolEnabled('read_file')).toBe(true); expect(await pm.isToolEnabled('read_file')).toBe(true);
expect(await pm.isToolEnabled('run_shell_command')).toBe(true); // not denied, just unreviewed expect(await pm.isToolEnabled('run_shell_command')).toBe(true); // not denied, just unreviewed
}); });
// Non-core tools bypass coreTools allowlist
it('MCP tools bypass coreTools allowlist check', async () => {
pm = new PermissionManager(makeConfig({ coreTools: ['read_file'] }));
pm.initialize();
// MCP tools should be enabled even if not in coreTools
expect(
await pm.isToolEnabled('mcp__markitdown__convert_to_markdown'),
).toBe(true);
expect(await pm.isToolEnabled('mcp__puppeteer__navigate')).toBe(true);
});
it('Skill tool bypasses coreTools allowlist check', async () => {
pm = new PermissionManager(makeConfig({ coreTools: ['read_file'] }));
pm.initialize();
expect(await pm.isToolEnabled('skill')).toBe(true);
});
it('Agent tool bypasses coreTools allowlist check', async () => {
pm = new PermissionManager(makeConfig({ coreTools: ['read_file'] }));
pm.initialize();
expect(await pm.isToolEnabled('agent')).toBe(true);
});
it('exit_plan_mode tool bypasses coreTools allowlist check', async () => {
pm = new PermissionManager(makeConfig({ coreTools: ['read_file'] }));
pm.initialize();
expect(await pm.isToolEnabled('exit_plan_mode')).toBe(true);
});
it('ask_user_question tool bypasses coreTools allowlist check', async () => {
pm = new PermissionManager(makeConfig({ coreTools: ['read_file'] }));
pm.initialize();
expect(await pm.isToolEnabled('ask_user_question')).toBe(true);
});
it('Non-core tools still respect deny rules', async () => {
pm = new PermissionManager(
makeConfig({
coreTools: ['read_file'],
permissionsDeny: ['mcp__markitdown'],
}),
);
pm.initialize();
// MCP tool should be disabled due to deny rule, even though it bypasses coreTools
expect(
await pm.isToolEnabled('mcp__markitdown__convert_to_markdown'),
).toBe(false);
// Other MCP tools without deny rule should still be enabled
expect(await pm.isToolEnabled('mcp__puppeteer__navigate')).toBe(true);
});
}); });
describe('session rules', () => { describe('session rules', () => {

View file

@ -396,6 +396,35 @@ export class PermissionManager {
// Registry-level helper // Registry-level helper
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/**
* Core tools that are subject to the coreTools allowlist check.
* Tools not in this set (MCP, Skill, Agent, etc.) bypass the check.
*/
private static readonly CORE_TOOLS = new Set([
'read_file',
'write_file',
'edit',
'glob',
'grep_search',
'run_shell_command',
'list_directory',
'web_fetch',
'web_search',
'todo_write',
'save_memory',
'lsp',
'cron_create',
'cron_list',
'cron_delete',
]);
/**
* Check if a tool is a core tool subject to the coreTools allowlist check.
*/
private isCoreTool(toolName: string): boolean {
return PermissionManager.CORE_TOOLS.has(toolName);
}
/** /**
* Determine whether a tool should be present in the tool registry. * Determine whether a tool should be present in the tool registry.
* *
@ -403,13 +432,23 @@ export class PermissionManager {
* (i.e. a whole-tool deny) matches. Specifier-based deny rules such as * (i.e. a whole-tool deny) matches. Specifier-based deny rules such as
* `"Bash(rm -rf *)"` do NOT remove the tool from the registry they only * `"Bash(rm -rf *)"` do NOT remove the tool from the registry they only
* deny specific invocations at runtime. * deny specific invocations at runtime.
*
* Non-core tools (MCP, Skill, Agent, etc.) skip the coreTools allowlist
* check because they are dynamically discovered or essential for system
* operation.
*/ */
async isToolEnabled(toolName: string): Promise<boolean> { async isToolEnabled(toolName: string): Promise<boolean> {
const canonicalName = resolveToolName(toolName); const canonicalName = resolveToolName(toolName);
// If a coreTools allowlist is active, only explicitly listed tools are // Non-core tools bypass coreTools allowlist check
// registered. This mirrors the legacy `tools.core` whitelist semantic: if (!this.isCoreTool(canonicalName)) {
// any tool NOT in the allowlist is excluded from the registry entirely. const decision = await this.evaluate({ toolName: canonicalName });
return decision !== 'deny';
}
// Core tools: if a coreTools allowlist is active, only explicitly listed
// tools are registered. This mirrors the legacy `tools.core` whitelist
// semantic: any tool NOT in the allowlist is excluded from the registry.
if (this.coreToolsAllowList !== null && this.coreToolsAllowList.size > 0) { if (this.coreToolsAllowList !== null && this.coreToolsAllowList.size > 0) {
if (!this.coreToolsAllowList.has(canonicalName)) { if (!this.coreToolsAllowList.has(canonicalName)) {
return false; return false;