diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 571d81285..dbc4cd48b 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -729,14 +729,7 @@ export async function loadCliConfig( const includeDirectories = (settings.context?.includeDirectories || []) .map(resolvePath) - .concat((argv.includeDirectories || []).map(resolvePath)) - .concat( - ( - ((settings.permissions as Record | undefined)?.[ - 'additionalDirectories' - ] as string[] | undefined) ?? [] - ).map(resolvePath), - ); + .concat((argv.includeDirectories || []).map(resolvePath)); // LSP configuration: enabled only via --experimental-lsp flag const lspEnabled = argv.experimentalLsp === true; diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 3bb327424..cfbed07f8 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -835,18 +835,6 @@ const SETTINGS_SCHEMA = { showInDialog: false, mergeStrategy: MergeStrategy.UNION, }, - additionalDirectories: { - type: 'array', - label: 'Additional Directories', - category: 'Tools', - requiresRestart: false, - default: [] as string[], - description: - 'Additional directories to include in the workspace context. ' + - 'Alias for context.includeDirectories. Files in these directories are treated as workspace files.', - showInDialog: false, - mergeStrategy: MergeStrategy.CONCAT, - }, }, }, diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 552195f9d..1f6af0dec 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -957,6 +957,34 @@ describe('ShellTool', () => { expect(details.type).toBe('exec'); }); + it('should exclude read-only sub-commands from confirmation details in compound commands', async () => { + // "cd" is read-only, "npm run build" is not + const params = { + command: 'cd packages/core && npm run build', + is_background: false, + }; + const invocation = shellTool.build(params); + + const permission = await invocation.getDefaultPermission(); + expect(permission).toBe('ask'); + + const details = (await invocation.getConfirmationDetails( + new AbortController().signal, + )) as { rootCommand: string; permissionRules: string[] }; + + // rootCommand should only include 'npm', not 'cd' + expect(details.rootCommand).not.toContain('cd'); + expect(details.rootCommand).toContain('npm'); + + // permissionRules should not include Bash(cd *) + expect(details.permissionRules).not.toContainEqual( + expect.stringContaining('cd'), + ); + expect(details.permissionRules).toContainEqual( + expect.stringContaining('npm'), + ); + }); + it('should throw an error if validation fails', () => { expect(() => shellTool.build({ command: '', is_background: false }), diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 117f0b51a..af82103db 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -33,7 +33,9 @@ import { formatMemoryUsage } from '../utils/formatters.js'; import type { AnsiOutput } from '../utils/terminalSerializer.js'; import { isSubpath } from '../utils/paths.js'; import { + getCommandRoot, getCommandRoots, + splitCommands, stripShellWrapper, detectCommandSubstitution, } from '../utils/shell-utils.js'; @@ -117,20 +119,52 @@ export class ShellToolInvocation extends BaseToolInvocation< /** * Constructs confirmation dialog details for a shell command that needs - * user approval. + * user approval. For compound commands (e.g. `cd foo && npm run build`), + * sub-commands that are already allowed (read-only) are excluded from both + * the displayed root-command list and the suggested permission rules. */ override async getConfirmationDetails( _abortSignal: AbortSignal, ): Promise { const command = stripShellWrapper(this.params.command); - const rootCommands = [...new Set(getCommandRoots(command))]; - // Extract minimum-scope permission rules for this command. + // Split compound command and filter out already-allowed (read-only) sub-commands + const subCommands = splitCommands(command); + const nonReadOnlySubCommands: string[] = []; + for (const sub of subCommands) { + try { + const isReadOnly = await isShellCommandReadOnlyAST(sub); + if (!isReadOnly) { + nonReadOnlySubCommands.push(sub); + } + } catch { + nonReadOnlySubCommands.push(sub); // conservative: include if check fails + } + } + + // Fallback to all sub-commands if everything was filtered out (shouldn't + // normally happen since getDefaultPermission already returned 'ask'). + const effectiveSubCommands = + nonReadOnlySubCommands.length > 0 ? nonReadOnlySubCommands : subCommands; + + const rootCommands = [ + ...new Set( + effectiveSubCommands + .map((c) => getCommandRoot(c)) + .filter((c): c is string => !!c), + ), + ]; + + // Extract minimum-scope permission rules only for sub-commands that + // actually need confirmation. let permissionRules: string[] = []; try { - permissionRules = (await extractCommandRules(command)).map( - (rule) => `Bash(${rule})`, - ); + const allRules: string[] = []; + for (const sub of effectiveSubCommands) { + const rules = await extractCommandRules(sub); + allRules.push(...rules); + } + permissionRules = [...new Set(allRules)].map((rule) => `Bash(${rule})`); } catch (e) { debugLogger.warn('Failed to extract command rules:', e); } diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index fdf83d3ba..94d1e9fd2 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -390,13 +390,6 @@ "items": { "type": "string" } - }, - "additionalDirectories": { - "description": "Additional directories to include in the workspace context. Alias for context.includeDirectories. Files in these directories are treated as workspace files.", - "type": "array", - "items": { - "type": "string" - } } } },