diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 9f65f1b9..19ccb466 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -60,11 +60,11 @@ export const ToolConfirmationMessage: React.FC< question = `Apply this change?`; options.push( { - label: 'Yes', + label: 'Yes, allow once', value: ToolConfirmationOutcome.ProceedOnce, }, { - label: 'Yes (always allow)', + label: 'Yes, allow always', // TODO: this is extreme w/o being qualified by file or directory value: ToolConfirmationOutcome.ProceedAlways, }, { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel }, @@ -73,29 +73,22 @@ export const ToolConfirmationMessage: React.FC< const executionProps = confirmationDetails as ToolExecuteConfirmationDetails; - // For execution, we still need context display and description - const commandDisplay = ( - {executionProps.command} - ); - - // Combine command and description into bodyContent for layout consistency bodyContent = ( - {commandDisplay} + {executionProps.command} ); question = `Allow execution?`; - const alwaysLabel = `Yes (always allow '${executionProps.rootCommand}' commands)`; options.push( { - label: 'Yes', + label: 'Yes, allow once', value: ToolConfirmationOutcome.ProceedOnce, }, { - label: alwaysLabel, + label: `Yes, allow always for ${executionProps.rootCommand} ...`, value: ToolConfirmationOutcome.ProceedAlways, }, { label: 'No (esc)', value: ToolConfirmationOutcome.Cancel }, diff --git a/packages/server/src/config/config.ts b/packages/server/src/config/config.ts index 8a49380c..498c7257 100644 --- a/packages/server/src/config/config.ts +++ b/packages/server/src/config/config.ts @@ -147,7 +147,7 @@ function createToolRegistry(config: Config): ToolRegistry { // use ShellTool (next-gen TerminalTool) if environment variable is set if (process.env.SHELL_TOOL) { - tools.push(new ShellTool(targetDir, config)); + tools.push(new ShellTool(config)); } else { tools.push(new TerminalTool(targetDir, config)); } diff --git a/packages/server/src/tools/shell.ts b/packages/server/src/tools/shell.ts index 7af6e703..bf4cf810 100644 --- a/packages/server/src/tools/shell.ts +++ b/packages/server/src/tools/shell.ts @@ -4,10 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import path from 'path'; import fs from 'fs'; import { Config } from '../config/config.js'; -import { BaseTool, ToolResult } from './tools.js'; +import { + BaseTool, + ToolResult, + ToolCallConfirmationDetails, + ToolExecuteConfirmationDetails, + ToolConfirmationOutcome, +} from './tools.js'; import toolParameterSchema from './shell.json' with { type: 'json' }; export interface ShellToolParams { @@ -17,10 +22,11 @@ export interface ShellToolParams { export class ShellTool extends BaseTool { static Name: string = 'execute_bash_command'; - private readonly rootDirectory: string; private readonly config: Config; + private cwd: string; + private whitelist: Set = new Set(); - constructor(rootDirectory: string, config: Config) { + constructor(config: Config) { const toolDisplayName = 'Shell'; const descriptionUrl = new URL('shell.md', import.meta.url); const toolDescription = fs.readFileSync(descriptionUrl, 'utf-8'); @@ -31,7 +37,41 @@ export class ShellTool extends BaseTool { toolParameterSchema, ); this.config = config; - this.rootDirectory = path.resolve(rootDirectory); + this.cwd = config.getTargetDir(); + } + + getDescription(params: ShellToolParams): string { + return params.description || `Execute \`${params.command}\` in ${this.cwd}`; + } + + validateToolParams(_params: ShellToolParams): string | null { + // TODO: validate the command here + return null; + } + + async shouldConfirmExecute( + params: ShellToolParams, + ): Promise { + const rootCommand = + params.command + .trim() + .split(/[\s;&&|]+/)[0] + ?.split(/[/\\]/) + .pop() || 'unknown'; + if (this.whitelist.has(rootCommand)) { + return false; + } + const confirmationDetails: ToolExecuteConfirmationDetails = { + title: 'Confirm Shell Command', + command: params.command, + rootCommand, + onConfirm: async (outcome: ToolConfirmationOutcome) => { + if (outcome === ToolConfirmationOutcome.ProceedAlways) { + this.whitelist.add(rootCommand); + } + }, + }; + return confirmationDetails; } async execute(_params: ShellToolParams): Promise { diff --git a/packages/server/src/tools/terminal.ts b/packages/server/src/tools/terminal.ts index 0d5c3d96..64b8e652 100644 --- a/packages/server/src/tools/terminal.ts +++ b/packages/server/src/tools/terminal.ts @@ -253,12 +253,10 @@ Use this tool for running build steps (\`npm install\`, \`make\`), linters (\`es if (this.shouldAlwaysExecuteCommands.get(rootCommand)) { return false; } - const description = this.getDescription(params); const confirmationDetails: ToolExecuteConfirmationDetails = { title: 'Confirm Shell Command', command: params.command, rootCommand, - description: `Execute in '${this.currentCwd}':\n${description}`, onConfirm: async (outcome: ToolConfirmationOutcome) => { if (outcome === ToolConfirmationOutcome.ProceedAlways) { this.shouldAlwaysExecuteCommands.set(rootCommand, true); diff --git a/packages/server/src/tools/tools.ts b/packages/server/src/tools/tools.ts index ed7c017a..448ea206 100644 --- a/packages/server/src/tools/tools.ts +++ b/packages/server/src/tools/tools.ts @@ -181,7 +181,6 @@ export interface ToolExecuteConfirmationDetails extends ToolCallConfirmationDetails { command: string; rootCommand: string; - description: string; } export enum ToolConfirmationOutcome {