diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 644fc050c..7d4dd2163 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -983,7 +983,7 @@ describe('mergeExcludeTools', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments(); const config = await loadCliConfig(settings, argv, undefined, []); - expect(config.getExcludeTools()).toEqual([]); + expect(config.getPermissionsDeny()).toEqual([]); }); it('should return default excludes when no excludeTools are specified and it is not interactive', async () => { @@ -992,7 +992,7 @@ describe('mergeExcludeTools', () => { process.argv = ['node', 'script.js', '-p', 'test']; const argv = await parseArguments(); const config = await loadCliConfig(settings, argv, undefined, []); - expect(config.getExcludeTools()).toEqual(defaultExcludes); + expect(config.getPermissionsDeny()).toEqual(defaultExcludes); }); it('should handle settings with excludeTools but no extensions', async () => { @@ -1000,10 +1000,10 @@ describe('mergeExcludeTools', () => { const argv = await parseArguments(); const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } }; const config = await loadCliConfig(settings, argv, undefined, []); - expect(config.getExcludeTools()).toEqual( + expect(config.getPermissionsDeny()).toEqual( expect.arrayContaining(['tool1', 'tool2']), ); - expect(config.getExcludeTools()).toHaveLength(2); + expect(config.getPermissionsDeny()).toHaveLength(2); }); }); @@ -1028,7 +1028,7 @@ describe('Approval mode tool exclusion logic', () => { const settings: Settings = {}; const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).toContain(ShellTool.Name); expect(excludedTools).toContain(EditTool.Name); expect(excludedTools).toContain(WriteFileTool.Name); @@ -1047,7 +1047,7 @@ describe('Approval mode tool exclusion logic', () => { const settings: Settings = {}; const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).toContain(ShellTool.Name); expect(excludedTools).toContain(EditTool.Name); expect(excludedTools).toContain(WriteFileTool.Name); @@ -1067,7 +1067,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).toContain(ShellTool.Name); expect(excludedTools).toContain(EditTool.Name); expect(excludedTools).toContain(WriteFileTool.Name); @@ -1084,7 +1084,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).not.toContain(ShellTool.Name); expect(excludedTools).toContain(EditTool.Name); expect(excludedTools).toContain(WriteFileTool.Name); @@ -1101,7 +1101,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).not.toContain(ShellTool.Name); expect(excludedTools).toContain(EditTool.Name); expect(excludedTools).toContain(WriteFileTool.Name); @@ -1121,7 +1121,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).toContain(ShellTool.Name); expect(excludedTools).not.toContain(EditTool.Name); expect(excludedTools).not.toContain(WriteFileTool.Name); @@ -1141,7 +1141,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).not.toContain(ShellTool.Name); expect(excludedTools).not.toContain(EditTool.Name); expect(excludedTools).not.toContain(WriteFileTool.Name); @@ -1154,7 +1154,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).not.toContain(ShellTool.Name); expect(excludedTools).not.toContain(EditTool.Name); expect(excludedTools).not.toContain(WriteFileTool.Name); @@ -1179,7 +1179,7 @@ describe('Approval mode tool exclusion logic', () => { const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).not.toContain(ShellTool.Name); expect(excludedTools).not.toContain(EditTool.Name); expect(excludedTools).not.toContain(WriteFileTool.Name); @@ -1199,7 +1199,7 @@ describe('Approval mode tool exclusion logic', () => { const settings: Settings = { tools: { exclude: ['custom_tool'] } }; const config = await loadCliConfig(settings, argv, undefined, []); - const excludedTools = config.getExcludeTools(); + const excludedTools = config.getPermissionsDeny(); expect(excludedTools).toContain('custom_tool'); // From settings expect(excludedTools).toContain(ShellTool.Name); // From approval mode expect(excludedTools).not.toContain(EditTool.Name); // Should be allowed in auto-edit @@ -1795,9 +1795,9 @@ describe('loadCliConfig tool exclusions', () => { process.argv = ['node', 'script.js']; const argv = await parseArguments(); const config = await loadCliConfig({}, argv, undefined, []); - expect(config.getExcludeTools()).not.toContain('run_shell_command'); - expect(config.getExcludeTools()).not.toContain('replace'); - expect(config.getExcludeTools()).not.toContain('write_file'); + expect(config.getPermissionsDeny()).not.toContain('run_shell_command'); + expect(config.getPermissionsDeny()).not.toContain('replace'); + expect(config.getPermissionsDeny()).not.toContain('write_file'); }); it('should not exclude interactive tools in interactive mode with YOLO', async () => { @@ -1805,9 +1805,9 @@ describe('loadCliConfig tool exclusions', () => { process.argv = ['node', 'script.js', '--yolo']; const argv = await parseArguments(); const config = await loadCliConfig({}, argv, undefined, []); - expect(config.getExcludeTools()).not.toContain('run_shell_command'); - expect(config.getExcludeTools()).not.toContain('replace'); - expect(config.getExcludeTools()).not.toContain('write_file'); + expect(config.getPermissionsDeny()).not.toContain('run_shell_command'); + expect(config.getPermissionsDeny()).not.toContain('replace'); + expect(config.getPermissionsDeny()).not.toContain('write_file'); }); it('should exclude interactive tools in non-interactive mode without YOLO', async () => { @@ -1815,9 +1815,9 @@ describe('loadCliConfig tool exclusions', () => { process.argv = ['node', 'script.js', '-p', 'test']; const argv = await parseArguments(); const config = await loadCliConfig({}, argv, undefined, []); - expect(config.getExcludeTools()).toContain('run_shell_command'); - expect(config.getExcludeTools()).toContain('edit'); - expect(config.getExcludeTools()).toContain('write_file'); + expect(config.getPermissionsDeny()).toContain('run_shell_command'); + expect(config.getPermissionsDeny()).toContain('edit'); + expect(config.getPermissionsDeny()).toContain('write_file'); }); it('should not exclude interactive tools in non-interactive mode with YOLO', async () => { @@ -1825,9 +1825,9 @@ describe('loadCliConfig tool exclusions', () => { process.argv = ['node', 'script.js', '-p', 'test', '--yolo']; const argv = await parseArguments(); const config = await loadCliConfig({}, argv, undefined, []); - expect(config.getExcludeTools()).not.toContain('run_shell_command'); - expect(config.getExcludeTools()).not.toContain('replace'); - expect(config.getExcludeTools()).not.toContain('write_file'); + expect(config.getPermissionsDeny()).not.toContain('run_shell_command'); + expect(config.getPermissionsDeny()).not.toContain('replace'); + expect(config.getPermissionsDeny()).not.toContain('write_file'); }); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index f945d8cc2..571d81285 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -30,6 +30,7 @@ import { NativeLspClient, createDebugLogger, NativeLspService, + isToolEnabled, } from '@qwen-code/qwen-code-core'; import { extensionsCommand } from '../commands/extensions.js'; import { hooksCommand } from '../commands/hooks.js'; @@ -837,9 +838,16 @@ export async function loadCliConfig( // Start from settings-level rules. // Read from both new `permissions` and legacy `tools` paths for compatibility. + // Note: settings.tools.core / argv.coreTools are intentionally NOT merged into + // mergedAllow — they have whitelist semantics (only listed tools are registered), + // not auto-approve semantics. They are passed via the `coreTools` Config param + // and handled by PermissionManager.coreToolsAllowList. + const resolvedCoreTools: string[] = [ + ...(argv.coreTools ?? []), + ...(settings.tools?.core ?? []), + ]; const mergedAllow: string[] = [ ...(settings.permissions?.allow ?? []), - ...(settings.tools?.core ?? []), ...(settings.tools?.allowed ?? []), ]; const mergedAsk: string[] = [...(settings.permissions?.ask ?? [])]; @@ -848,10 +856,7 @@ export async function loadCliConfig( ...(settings.tools?.exclude ?? []), ]; - // argv.coreTools and argv.allowedTools both add allow rules. - for (const t of argv.coreTools ?? []) { - if (t && !mergedAllow.includes(t)) mergedAllow.push(t); - } + // argv.allowedTools adds allow rules (auto-approve). for (const t of argv.allowedTools ?? []) { if (t && !mergedAllow.includes(t)) mergedAllow.push(t); } @@ -861,15 +866,30 @@ export async function loadCliConfig( if (t && !mergedDeny.includes(t)) mergedDeny.push(t); } - // Helper: check if a tool is covered by any allow rule (tool-level, no specifier). + // Helper: check if a tool is explicitly covered by an allow rule OR by the + // coreTools whitelist. Uses alias matching for coreTools (via isToolEnabled) + // to preserve the original behaviour where "ShellTool", "Shell", and + // "run_shell_command" are all accepted as the same tool. const isExplicitlyAllowed = (toolName: ToolName): boolean => { const name = toolName as string; - return mergedAllow.some((rule) => { - const openParen = rule.indexOf('('); - const ruleName = - openParen === -1 ? rule.trim() : rule.substring(0, openParen).trim(); - return ruleName === name; - }); + // 1. Check permissions.allow / allowedTools rules. + if ( + mergedAllow.some((rule) => { + const openParen = rule.indexOf('('); + const ruleName = + openParen === -1 ? rule.trim() : rule.substring(0, openParen).trim(); + return ruleName === name; + }) + ) { + return true; + } + // 2. Check coreTools whitelist (with alias matching). + // If coreTools is non-empty and explicitly includes this tool, it is + // considered allowed for non-interactive mode exclusion purposes. + if (resolvedCoreTools.length > 0) { + return isToolEnabled(toolName, resolvedCoreTools, []); + } + return false; }; // In non-interactive mode, tools that require a user prompt are denied unless @@ -994,7 +1014,7 @@ export async function loadCliConfig( importFormat: settings.context?.importFormat || 'tree', debugMode, question, - // Legacy fields – kept for backward compatibility with getExcludeTools() etc. + // Legacy fields – kept for backward compatibility with getCoreTools() etc. coreTools: argv.coreTools || settings.tools?.core || undefined, allowedTools: argv.allowedTools || settings.tools?.allowed || undefined, excludeTools: mergedDeny, diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index f9120e217..75a268ff9 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1240,8 +1240,6 @@ export default { 'Dieses Verzeichnis ist bereits im Arbeitsbereich.', 'Already covered by existing directory: {{dir}}': 'Bereits durch vorhandenes Verzeichnis abgedeckt: {{dir}}', - 'Add directories to the workspace (alias for /directory add)': - 'Verzeichnisse zum Arbeitsbereich hinzufügen (Alias für /directory add)', // ============================================================================ // Status Bar diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index f9b716a2b..e617a1a18 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1291,8 +1291,6 @@ export default { 'This directory is already in the workspace.', 'Already covered by existing directory: {{dir}}': 'Already covered by existing directory: {{dir}}', - 'Add directories to the workspace (alias for /directory add)': - 'Add directories to the workspace (alias for /directory add)', // ============================================================================ // Status Bar diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index e0b650f15..4eaedd1e0 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -929,8 +929,6 @@ export default { 'このディレクトリはすでにワークスペースに含まれています。', 'Already covered by existing directory: {{dir}}': '既存のディレクトリによって既にカバーされています: {{dir}}', - 'Add directories to the workspace (alias for /directory add)': - 'ワークスペースにディレクトリを追加(/directory add のエイリアス)', // Status Bar 'Using:': '使用中:', '{{count}} open file': '{{count}} 個のファイルを開いています', diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 5474093a7..d7864b79a 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1244,8 +1244,6 @@ export default { 'Este diretório já está na área de trabalho.', 'Already covered by existing directory: {{dir}}': 'Já coberto pelo diretório existente: {{dir}}', - 'Add directories to the workspace (alias for /directory add)': - 'Adicionar diretórios à área de trabalho (apelido para /directory add)', // ============================================================================ // Status Bar diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index a7a1d4a71..6dbb32481 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1167,8 +1167,6 @@ export default { 'Этот каталог уже есть в рабочей области.', 'Already covered by existing directory: {{dir}}': 'Уже охвачен существующим каталогом: {{dir}}', - 'Add directories to the workspace (alias for /directory add)': - 'Добавить каталоги в рабочую область (псевдоним для /directory add)', // ============================================================================ // Строка состояния diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index e103b8ea7..bd8413dfd 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1219,8 +1219,6 @@ export default { 'Path is not a directory.': '路径不是目录。', 'This directory is already in the workspace.': '此目录已在工作区中。', 'Already covered by existing directory: {{dir}}': '已被现有目录覆盖:{{dir}}', - 'Add directories to the workspace (alias for /directory add)': - '将目录添加到工作区(/directory add 的别名)', // ============================================================================ // Status Bar diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 83459882a..fcdc18804 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -8,7 +8,6 @@ import type { ICommandLoader } from './types.js'; import type { SlashCommand } from '../ui/commands/types.js'; import type { Config } from '@qwen-code/qwen-code-core'; import { aboutCommand } from '../ui/commands/aboutCommand.js'; -import { addDirCommand } from '../ui/commands/addDirCommand.js'; import { agentsCommand } from '../ui/commands/agentsCommand.js'; import { approvalModeCommand } from '../ui/commands/approvalModeCommand.js'; import { authCommand } from '../ui/commands/authCommand.js'; @@ -62,7 +61,6 @@ export class BuiltinCommandLoader implements ICommandLoader { async loadCommands(_signal: AbortSignal): Promise { const allDefinitions: Array = [ aboutCommand, - addDirCommand, agentsCommand, approvalModeCommand, authCommand, diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts index 68ca60656..fa2afe4fd 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts @@ -72,7 +72,7 @@ describe('ShellProcessor', () => { getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getShouldUseNodePtyShell: vi.fn().mockReturnValue(false), getShellExecutionConfig: vi.fn().mockReturnValue({}), - getAllowedTools: vi.fn().mockReturnValue([]), + getPermissionsAllow: vi.fn().mockReturnValue([]), // Default: no permission manager (tests that need one set it explicitly) getPermissionManager: vi.fn().mockReturnValue(null), }; diff --git a/packages/cli/src/ui/commands/addDirCommand.tsx b/packages/cli/src/ui/commands/addDirCommand.tsx deleted file mode 100644 index 810dcf889..000000000 --- a/packages/cli/src/ui/commands/addDirCommand.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { SlashCommand, CommandContext } from './types.js'; -import { CommandKind } from './types.js'; -import { directoryCommand } from './directoryCommand.js'; -import { t } from '../../i18n/index.js'; - -/** - * `/add-dir` — a convenience alias that delegates to `/directory add`. - * - * Usage: `/add-dir /path/to/dir` (equivalent to `/directory add /path/to/dir`) - */ -export const addDirCommand: SlashCommand = { - name: 'add-dir', - altNames: [], - get description() { - return t('Add directories to the workspace (alias for /directory add)'); - }, - kind: CommandKind.BUILT_IN, - action: async (context: CommandContext, args: string) => { - // Delegate to the `add` subcommand of `/directory` - const addSubCommand = directoryCommand.subCommands?.find( - (sub) => sub.name === 'add', - ); - if (!addSubCommand?.action) { - return; - } - return addSubCommand.action(context, args); - }, -}; diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index 1fcd83dd3..ca57ad10d 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -7,6 +7,7 @@ import type { SlashCommand, CommandContext } from './types.js'; import { CommandKind } from './types.js'; import { MessageType } from '../types.js'; +import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { loadServerHierarchicalMemory } from '@qwen-code/qwen-code-core'; @@ -25,6 +26,44 @@ export function expandHomeDir(p: string): string { return path.normalize(expandedPath); } +/** + * Returns directory path completions for the given partial argument. + * Supports comma-separated paths by completing only the last segment. + */ +export function getDirPathCompletions(partialArg: string): string[] { + const lastComma = partialArg.lastIndexOf(','); + const prefix = lastComma >= 0 ? partialArg.substring(0, lastComma + 1) : ''; + const partial = + lastComma >= 0 + ? partialArg.substring(lastComma + 1).trimStart() + : partialArg; + + const trimmed = partial.trim(); + if (!trimmed) return []; + + const expanded = trimmed.startsWith('~') + ? trimmed.replace(/^~/, os.homedir()) + : trimmed; + const endsWithSep = expanded.endsWith('/') || expanded.endsWith(path.sep); + const searchDir = endsWithSep ? expanded : path.dirname(expanded); + const namePrefix = endsWithSep ? '' : path.basename(expanded); + + try { + return fs + .readdirSync(searchDir, { withFileTypes: true }) + .filter( + (e) => + e.isDirectory() && + e.name.startsWith(namePrefix) && + !e.name.startsWith('.'), + ) + .map((e) => prefix + path.join(searchDir, e.name)) + .slice(0, 8); + } catch { + return []; + } +} + export const directoryCommand: SlashCommand = { name: 'directory', altNames: ['dir'], @@ -41,6 +80,8 @@ export const directoryCommand: SlashCommand = { ); }, kind: CommandKind.BUILT_IN, + completion: async (_context: CommandContext, partialArg: string) => + getDirPathCompletions(partialArg), action: async (context: CommandContext, args: string) => { const { ui: { addItem }, diff --git a/packages/cli/src/ui/hooks/useToolScheduler.test.ts b/packages/cli/src/ui/hooks/useToolScheduler.test.ts index 17d20e522..3d6dc9507 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.test.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.test.ts @@ -59,7 +59,7 @@ const mockConfig = { }, getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, - getAllowedTools: vi.fn(() => []), + getPermissionsAllow: vi.fn(() => []), getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 2a8daea59..7e82c2ff9 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1256,32 +1256,25 @@ export class Config { return this.coreTools; } - /** @deprecated Use getPermissionsAllow() instead. */ - getAllowedTools(): string[] | undefined { - return this.allowedTools; - } - - /** @deprecated Use getPermissionsDeny() instead. */ - getExcludeTools(): string[] | undefined { - return this.excludeTools; - } - /** * Returns the merged allow-rules for PermissionManager. * * This merges all sources so that PermissionManager receives a single, * authoritative list: * - settings.permissions.allow (persistent rules from all scopes) - * - coreTools param (SDK / argv allowlist mode: only these tools run) * - allowedTools param (SDK / argv auto-approve list) * + * Note: coreTools is intentionally excluded here — it has whitelist semantics + * (only listed tools are registered), not auto-approve semantics. It is + * handled separately via PermissionManager.coreToolsAllowList. + * * CLI callers (loadCliConfig) already pre-merge argv into permissionsAllow * before constructing Config, so those fields will be empty for CLI usage. - * SDK callers construct Config directly and rely on coreTools/allowedTools. + * SDK callers construct Config directly and rely on allowedTools. */ getPermissionsAllow(): string[] | undefined { const base = this.permissionsAllow ?? []; - const sdkAllow = [...(this.coreTools ?? []), ...(this.allowedTools ?? [])]; + const sdkAllow = [...(this.allowedTools ?? [])]; if (sdkAllow.length === 0) return base.length > 0 ? base : undefined; const merged = [...base]; for (const t of sdkAllow) { diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts index d6a2cc173..5c21edca2 100644 --- a/packages/core/src/core/coreToolScheduler.test.ts +++ b/packages/core/src/core/coreToolScheduler.test.ts @@ -245,7 +245,7 @@ describe('CoreToolScheduler', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -322,7 +322,7 @@ describe('CoreToolScheduler', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -382,7 +382,7 @@ describe('CoreToolScheduler', () => { getToolRegistry: () => mockToolRegistry, getUseModelRouter: () => false, getGeminiClient: () => null, // No client needed for these tests - getExcludeTools: () => undefined, + getPermissionsDeny: () => undefined, isInteractive: () => true, } as unknown as Config; @@ -423,7 +423,7 @@ describe('CoreToolScheduler', () => { getToolRegistry: () => mockToolRegistry, getUseModelRouter: () => false, getGeminiClient: () => null, - getExcludeTools: () => ['write_file', 'edit', 'run_shell_command'], + getPermissionsDeny: () => ['write_file', 'edit', 'run_shell_command'], isInteractive: () => false, // Value doesn't matter, but included for completeness } as unknown as Config; @@ -453,7 +453,7 @@ describe('CoreToolScheduler', () => { getToolRegistry: () => mockToolRegistry, getUseModelRouter: () => false, getGeminiClient: () => null, - getExcludeTools: () => ['write_file', 'edit'], + getPermissionsDeny: () => ['write_file', 'edit'], isInteractive: () => false, // Value doesn't matter } as unknown as Config; @@ -494,7 +494,7 @@ describe('CoreToolScheduler', () => { getToolRegistry: () => mockToolRegistry, getUseModelRouter: () => false, getGeminiClient: () => null, - getExcludeTools: () => undefined, + getPermissionsDeny: () => undefined, isInteractive: () => true, } as unknown as Config; @@ -554,8 +554,8 @@ describe('CoreToolScheduler', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, - getAllowedTools: () => [], - getExcludeTools: () => ['write_file', 'edit', 'run_shell_command'], + getPermissionsAllow: () => [], + getPermissionsDeny: () => ['write_file', 'edit', 'run_shell_command'], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -640,8 +640,8 @@ describe('CoreToolScheduler', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, - getAllowedTools: () => [], - getExcludeTools: () => ['write_file', 'edit'], // Different excluded tools + getPermissionsAllow: () => [], + getPermissionsDeny: () => ['write_file', 'edit'], // Different excluded tools getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -730,7 +730,7 @@ describe('CoreToolScheduler with payload', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -1073,7 +1073,7 @@ describe('CoreToolScheduler edit cancellation', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -1180,7 +1180,7 @@ describe('CoreToolScheduler YOLO mode', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -1421,7 +1421,7 @@ describe('CoreToolScheduler request queueing', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -1543,7 +1543,7 @@ describe('CoreToolScheduler request queueing', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -1617,7 +1617,7 @@ describe('CoreToolScheduler request queueing', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => approvalMode, - getAllowedTools: () => [], + getPermissionsAllow: () => [], setApprovalMode: (mode: ApprovalMode) => { approvalMode = mode; }, @@ -1779,8 +1779,8 @@ describe('CoreToolScheduler truncated output protection', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.AUTO_EDIT, - getAllowedTools: () => [], - getExcludeTools: () => undefined, + getPermissionsAllow: () => [], + getPermissionsDeny: () => undefined, getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -1978,7 +1978,7 @@ describe('CoreToolScheduler Sequential Execution', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -2098,7 +2098,7 @@ describe('CoreToolScheduler Sequential Execution', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', @@ -2490,7 +2490,7 @@ describe('CoreToolScheduler plan mode with ask_user_question', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.PLAN, - getAllowedTools: () => [], + getPermissionsAllow: () => [], getContentGeneratorConfig: () => ({ model: 'test-model', authType: 'gemini', diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index 64ff9d8a6..ee046cb8f 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -761,9 +761,10 @@ export class CoreToolScheduler { }; } - // Legacy fallback: check getExcludeTools() when PM is not available + // Legacy fallback: check getPermissionsDeny() when PM is not available if (!pm) { - const excludeTools = this.config.getExcludeTools?.() ?? undefined; + const excludeTools = + this.config.getPermissionsDeny?.() ?? undefined; if (excludeTools && excludeTools.length > 0) { const normalizedToolName = reqInfo.name.toLowerCase().trim(); const excludedMatch = excludeTools.find( diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 5faa00f8f..552195f9d 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -51,7 +51,7 @@ describe('ShellTool', () => { mockConfig = { getCoreTools: vi.fn().mockReturnValue([]), - getExcludeTools: vi.fn().mockReturnValue([]), + getPermissionsDeny: vi.fn().mockReturnValue([]), getDebugMode: vi.fn().mockReturnValue(false), getTargetDir: vi.fn().mockReturnValue('/test/dir'), getSummarizeToolOutputConfig: vi.fn().mockReturnValue(undefined), @@ -93,7 +93,7 @@ describe('ShellTool', () => { describe('isCommandAllowed', () => { it('should allow a command if no restrictions are provided', () => { (mockConfig.getCoreTools as Mock).mockReturnValue(undefined); - (mockConfig.getExcludeTools as Mock).mockReturnValue(undefined); + (mockConfig.getPermissionsDeny as Mock).mockReturnValue(undefined); expect(isCommandAllowed('ls -l', mockConfig).allowed).toBe(true); }); diff --git a/packages/core/src/utils/shell-utils.test.ts b/packages/core/src/utils/shell-utils.test.ts index b974bfd5a..7a02ba4a7 100644 --- a/packages/core/src/utils/shell-utils.test.ts +++ b/packages/core/src/utils/shell-utils.test.ts @@ -44,8 +44,8 @@ beforeEach(() => { mockParse.mockImplementation((cmd: string) => cmd.split(' ')); config = { getCoreTools: () => [], - getExcludeTools: () => [], - getAllowedTools: () => [], + getPermissionsDeny: () => [], + getPermissionsAllow: () => [], } as unknown as Config; }); @@ -75,7 +75,7 @@ describe('isCommandAllowed', () => { }); it('should block a command if it is in the blocked list', () => { - config.getExcludeTools = () => ['ShellTool(rm -rf /)']; + config.getPermissionsDeny = () => ['ShellTool(rm -rf /)']; const result = isCommandAllowed('rm -rf /', config); expect(result.allowed).toBe(false); expect(result.reason).toBe( @@ -85,7 +85,7 @@ describe('isCommandAllowed', () => { it('should prioritize the blocklist over the allowlist', () => { config.getCoreTools = () => ['ShellTool(rm -rf /)']; - config.getExcludeTools = () => ['ShellTool(rm -rf /)']; + config.getPermissionsDeny = () => ['ShellTool(rm -rf /)']; const result = isCommandAllowed('rm -rf /', config); expect(result.allowed).toBe(false); expect(result.reason).toBe( @@ -100,7 +100,7 @@ describe('isCommandAllowed', () => { }); it('should block any command when a wildcard is in excludeTools', () => { - config.getExcludeTools = () => ['run_shell_command']; + config.getPermissionsDeny = () => ['run_shell_command']; const result = isCommandAllowed('any random command', config); expect(result.allowed).toBe(false); expect(result.reason).toBe( @@ -110,7 +110,7 @@ describe('isCommandAllowed', () => { it('should block a command on the blocklist even with a wildcard allow', () => { config.getCoreTools = () => ['ShellTool']; - config.getExcludeTools = () => ['ShellTool(rm -rf /)']; + config.getPermissionsDeny = () => ['ShellTool(rm -rf /)']; const result = isCommandAllowed('rm -rf /', config); expect(result.allowed).toBe(false); expect(result.reason).toBe( @@ -128,7 +128,7 @@ describe('isCommandAllowed', () => { }); it('should block a chained command if any part is blocked', () => { - config.getExcludeTools = () => ['run_shell_command(rm)']; + config.getPermissionsDeny = () => ['run_shell_command(rm)']; const result = isCommandAllowed('echo "hello" && rm -rf /', config); expect(result.allowed).toBe(false); expect(result.reason).toBe( @@ -298,7 +298,7 @@ describe('checkCommandPermissions', () => { }); it('should return a detailed failure object for a blocked command', () => { - config.getExcludeTools = () => ['ShellTool(rm)']; + config.getPermissionsDeny = () => ['ShellTool(rm)']; const result = checkCommandPermissions('rm -rf /', config); expect(result).toEqual({ allAllowed: false, @@ -364,7 +364,7 @@ describe('checkCommandPermissions', () => { }); it('should block a command on the sessionAllowlist if it is also globally blocked', () => { - config.getExcludeTools = () => ['run_shell_command(rm)']; + config.getPermissionsDeny = () => ['run_shell_command(rm)']; const result = checkCommandPermissions( 'rm -rf /', config, diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts index 200ab35c3..de80f6851 100644 --- a/packages/core/src/utils/shell-utils.ts +++ b/packages/core/src/utils/shell-utils.ts @@ -714,10 +714,10 @@ export function checkCommandPermissions( // ── Legacy fallback (no PermissionManager) ────────────────────────────── // Used by SDK consumers that have not yet migrated to the permissions system, - // or in unit tests that mock only getCoreTools/getExcludeTools. + // or in unit tests that mock only getCoreTools/getPermissionsDeny. // 1. Blocklist Check (Highest Priority) - const excludeTools = config.getExcludeTools() || []; + const excludeTools = config.getPermissionsDeny() || []; const isWildcardBlocked = SHELL_TOOL_NAMES.some((name) => excludeTools.includes(name), );