2025-04-24 18:03:33 -07:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2026-01-27 13:17:40 -08:00
import fsPromises from 'node:fs/promises' ;
2025-08-25 22:11:27 +02:00
import path from 'node:path' ;
import os , { EOL } from 'node:os' ;
import crypto from 'node:crypto' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2026-01-04 00:19:00 -05:00
import { debugLogger } from '../index.js' ;
2025-08-26 15:26:16 -04:00
import { ToolErrorType } from './tool-error.js' ;
2025-08-26 00:04:53 +02:00
import type {
2025-08-13 12:27:09 -07:00
ToolInvocation ,
2025-04-25 14:05:58 -07:00
ToolResult ,
ToolCallConfirmationDetails ,
ToolExecuteConfirmationDetails ,
2025-08-26 00:04:53 +02:00
} from './tools.js' ;
import {
BaseDeclarativeTool ,
BaseToolInvocation ,
2025-04-25 14:05:58 -07:00
ToolConfirmationOutcome ,
2025-08-13 12:58:26 -03:00
Kind ,
2025-12-12 13:45:39 -08:00
type PolicyUpdateOptions ,
2025-04-25 14:05:58 -07:00
} from './tools.js' ;
2025-11-03 15:41:00 -08:00
2025-05-18 00:23:57 -07:00
import { getErrorMessage } from '../utils/errors.js' ;
2025-07-25 21:56:49 -04:00
import { summarizeToolOutput } from '../utils/summarizer.js' ;
2025-09-11 13:27:27 -07:00
import type {
ShellExecutionConfig ,
ShellOutputEvent ,
} from '../services/shellExecutionService.js' ;
2025-08-26 00:04:53 +02:00
import { ShellExecutionService } from '../services/shellExecutionService.js' ;
2026-01-27 17:21:53 +01:00
import { formatBytes } from '../utils/formatters.js' ;
2025-09-11 13:27:27 -07:00
import type { AnsiOutput } from '../utils/terminalSerializer.js' ;
2025-07-25 12:25:32 -07:00
import {
getCommandRoots ,
2025-10-16 17:25:30 -07:00
initializeShellParsers ,
2025-07-25 12:25:32 -07:00
stripShellWrapper ,
2026-01-19 20:07:28 -08:00
parseCommandDetails ,
hasRedirection ,
2025-07-25 12:25:32 -07:00
} from '../utils/shell-utils.js' ;
2025-10-17 21:07:26 -04:00
import { SHELL_TOOL_NAME } from './tool-names.js' ;
2025-10-24 13:04:40 -07:00
import type { MessageBus } from '../confirmation-bus/message-bus.js' ;
2025-06-02 14:50:12 -07:00
2025-07-25 21:56:49 -04:00
export const OUTPUT_UPDATE_INTERVAL_MS = 1000 ;
2026-01-30 09:53:09 -08:00
// Delay so user does not see the output of the process before the process is moved to the background.
const BACKGROUND_DELAY_MS = 200 ;
2025-04-24 18:03:33 -07:00
export interface ShellToolParams {
command : string ;
description? : string ;
2025-11-06 15:03:52 -08:00
dir_path? : string ;
2026-01-30 09:53:09 -08:00
is_background? : boolean ;
2025-04-24 18:03:33 -07:00
}
2025-05-30 01:58:09 -07:00
2025-09-11 13:27:27 -07:00
export class ShellToolInvocation extends BaseToolInvocation <
2025-08-13 12:27:09 -07:00
ShellToolParams ,
ToolResult
> {
constructor (
private readonly config : Config ,
params : ShellToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-28 09:20:57 -07:00
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-13 12:27:09 -07:00
) {
2025-10-28 09:20:57 -07:00
super ( params , messageBus , _toolName , _toolDisplayName ) ;
2025-04-25 14:05:58 -07:00
}
2025-08-13 12:27:09 -07:00
getDescription ( ) : string {
let description = ` ${ this . params . command } ` ;
2025-04-30 12:27:56 -07:00
// append optional [in directory]
2025-04-29 15:31:46 -07:00
// note description is needed even if validation fails due to absolute path
2025-11-06 15:03:52 -08:00
if ( this . params . dir_path ) {
description += ` [in ${ this . params . dir_path } ] ` ;
2025-11-06 08:51:07 -08:00
} else {
description += ` [current working directory ${ process . cwd ( ) } ] ` ;
2025-04-28 08:17:52 -07:00
}
// append optional (description), replacing any line breaks with spaces
2025-08-13 12:27:09 -07:00
if ( this . params . description ) {
description += ` ( ${ this . params . description . replace ( /\n/g , ' ' ) } ) ` ;
2025-04-27 18:57:10 -07:00
}
2026-01-30 09:53:09 -08:00
if ( this . params . is_background ) {
description += ' [background]' ;
}
2025-04-27 18:57:10 -07:00
return description ;
2025-04-25 14:05:58 -07:00
}
2025-12-12 13:45:39 -08:00
protected override getPolicyUpdateOptions (
outcome : ToolConfirmationOutcome ,
) : PolicyUpdateOptions | undefined {
2025-12-26 18:48:44 -05:00
if (
outcome === ToolConfirmationOutcome . ProceedAlwaysAndSave ||
outcome === ToolConfirmationOutcome . ProceedAlways
) {
const command = stripShellWrapper ( this . params . command ) ;
const rootCommands = [ . . . new Set ( getCommandRoots ( command ) ) ] ;
if ( rootCommands . length > 0 ) {
return { commandPrefix : rootCommands } ;
}
2025-12-12 13:45:39 -08:00
return { commandPrefix : this.params.command } ;
}
return undefined ;
}
2025-10-24 13:04:40 -07:00
protected override async getConfirmationDetails (
2025-05-27 23:40:25 -07:00
_abortSignal : AbortSignal ,
2025-04-25 14:05:58 -07:00
) : Promise < ToolCallConfirmationDetails | false > {
2025-08-13 12:27:09 -07:00
const command = stripShellWrapper ( this . params . command ) ;
2026-01-04 00:19:00 -05:00
2026-01-19 20:07:28 -08:00
const parsed = parseCommandDetails ( command ) ;
let rootCommandDisplay = '' ;
if ( ! parsed || parsed . hasError || parsed . details . length === 0 ) {
// Fallback if parser fails
2026-01-04 00:19:00 -05:00
const fallback = command . trim ( ) . split ( /\s+/ ) [ 0 ] ;
2026-01-19 20:07:28 -08:00
rootCommandDisplay = fallback || 'shell command' ;
if ( hasRedirection ( command ) ) {
rootCommandDisplay += ', redirection' ;
2025-10-06 12:15:21 -07:00
}
2026-01-19 20:07:28 -08:00
} else {
rootCommandDisplay = parsed . details
. map ( ( detail ) = > detail . name )
. join ( ', ' ) ;
2025-10-06 12:15:21 -07:00
}
2026-01-19 20:07:28 -08:00
const rootCommands = [ . . . new Set ( getCommandRoots ( command ) ) ] ;
2026-01-02 11:36:59 -08:00
// Rely entirely on PolicyEngine for interactive confirmation.
// If we are here, it means PolicyEngine returned ASK_USER (or no message bus),
// so we must provide confirmation details.
2025-04-25 14:05:58 -07:00
const confirmationDetails : ToolExecuteConfirmationDetails = {
2025-05-22 06:00:36 +00:00
type : 'exec' ,
2025-04-25 14:05:58 -07:00
title : 'Confirm Shell Command' ,
2025-08-13 12:27:09 -07:00
command : this.params.command ,
2026-01-19 20:07:28 -08:00
rootCommand : rootCommandDisplay ,
2026-01-14 13:50:28 -05:00
rootCommands ,
2025-04-25 14:05:58 -07:00
onConfirm : async ( outcome : ToolConfirmationOutcome ) = > {
2025-12-12 13:45:39 -08:00
await this . publishPolicyUpdate ( outcome ) ;
2025-04-25 14:05:58 -07:00
} ,
} ;
return confirmationDetails ;
2025-04-24 18:03:33 -07:00
}
2025-05-09 23:29:02 -07:00
async execute (
2025-07-25 12:25:32 -07:00
signal : AbortSignal ,
2025-09-11 13:27:27 -07:00
updateOutput ? : ( output : string | AnsiOutput ) = > void ,
shellExecutionConfig? : ShellExecutionConfig ,
setPidCallback ? : ( pid : number ) = > void ,
2025-05-09 23:29:02 -07:00
) : Promise < ToolResult > {
2025-08-13 12:27:09 -07:00
const strippedCommand = stripShellWrapper ( this . params . command ) ;
2025-04-27 18:57:10 -07:00
2025-07-25 12:25:32 -07:00
if ( signal . aborted ) {
2025-06-08 15:42:49 -07:00
return {
llmContent : 'Command was cancelled by user before it could start.' ,
returnDisplay : 'Command cancelled by user.' ,
} ;
}
2025-06-09 12:19:42 -07:00
const isWindows = os . platform ( ) === 'win32' ;
2025-06-15 02:19:19 -07:00
const tempFileName = ` shell_pgrep_ ${ crypto
. randomBytes ( 6 )
. toString ( 'hex' ) } .tmp ` ;
const tempFilePath = path . join ( os . tmpdir ( ) , tempFileName ) ;
2025-05-06 10:44:40 -07:00
2025-11-26 13:43:33 -08:00
const timeoutMs = this . config . getShellToolInactivityTimeout ( ) ;
const timeoutController = new AbortController ( ) ;
let timeoutTimer : NodeJS.Timeout | undefined ;
// Handle signal combination manually to avoid TS issues or runtime missing features
const combinedController = new AbortController ( ) ;
const onAbort = ( ) = > combinedController . abort ( ) ;
2025-07-25 21:56:49 -04:00
try {
// pgrep is not available on Windows, so we can't get background PIDs
const commandToExecute = isWindows
? strippedCommand
: ( ( ) = > {
// wrap command to append subprocess pids (via pgrep) to temporary file
let command = strippedCommand . trim ( ) ;
if ( ! command . endsWith ( '&' ) ) command += ';' ;
return ` { ${ command } }; __code= $ ?; pgrep -g 0 > ${ tempFilePath } 2>&1; exit $ __code; ` ;
} ) ( ) ;
2025-04-27 18:57:10 -07:00
2025-11-06 15:03:52 -08:00
const cwd = this . params . dir_path
? path . resolve ( this . config . getTargetDir ( ) , this . params . dir_path )
: this . config . getTargetDir ( ) ;
2025-05-30 01:58:09 -07:00
2026-01-27 13:17:40 -08:00
const validationError = this . config . validatePathAccess ( cwd ) ;
if ( validationError ) {
return {
llmContent : validationError ,
returnDisplay : 'Path not in workspace.' ,
error : {
message : validationError ,
type : ToolErrorType . PATH_NOT_IN_WORKSPACE ,
} ,
} ;
}
2025-09-11 13:27:27 -07:00
let cumulativeOutput : string | AnsiOutput = '' ;
2025-07-25 21:56:49 -04:00
let lastUpdateTime = Date . now ( ) ;
let isBinaryStream = false ;
2025-04-27 18:57:10 -07:00
2025-11-26 13:43:33 -08:00
const resetTimeout = ( ) = > {
if ( timeoutMs <= 0 ) {
return ;
}
if ( timeoutTimer ) clearTimeout ( timeoutTimer ) ;
timeoutTimer = setTimeout ( ( ) = > {
timeoutController . abort ( ) ;
} , timeoutMs ) ;
} ;
signal . addEventListener ( 'abort' , onAbort , { once : true } ) ;
timeoutController . signal . addEventListener ( 'abort' , onAbort , {
once : true ,
} ) ;
// Start timeout
resetTimeout ( ) ;
2025-09-11 13:27:27 -07:00
const { result : resultPromise , pid } =
await ShellExecutionService . execute (
commandToExecute ,
cwd ,
( event : ShellOutputEvent ) = > {
2025-11-26 13:43:33 -08:00
resetTimeout ( ) ; // Reset timeout on any event
2025-09-11 13:27:27 -07:00
if ( ! updateOutput ) {
return ;
}
2025-04-27 18:57:10 -07:00
2025-09-11 13:27:27 -07:00
let shouldUpdate = false ;
switch ( event . type ) {
case 'data' :
if ( isBinaryStream ) break ;
cumulativeOutput = event . chunk ;
2025-07-25 21:56:49 -04:00
shouldUpdate = true ;
2025-09-11 13:27:27 -07:00
break ;
case 'binary_detected' :
isBinaryStream = true ;
cumulativeOutput =
'[Binary output detected. Halting stream...]' ;
2025-07-25 21:56:49 -04:00
shouldUpdate = true ;
2025-09-11 13:27:27 -07:00
break ;
case 'binary_progress' :
isBinaryStream = true ;
2026-01-27 17:21:53 +01:00
cumulativeOutput = ` [Receiving binary output... ${ formatBytes (
2025-09-11 13:27:27 -07:00
event . bytesReceived ,
) } received] ` ;
if ( Date . now ( ) - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS ) {
shouldUpdate = true ;
}
break ;
2026-01-30 09:53:09 -08:00
case 'exit' :
break ;
2025-09-11 13:27:27 -07:00
default : {
throw new Error ( 'An unhandled ShellOutputEvent was found.' ) ;
2025-06-09 12:19:42 -07:00
}
}
2025-04-27 18:57:10 -07:00
2026-01-30 09:53:09 -08:00
if ( shouldUpdate && ! this . params . is_background ) {
2025-09-11 13:27:27 -07:00
updateOutput ( cumulativeOutput ) ;
lastUpdateTime = Date . now ( ) ;
}
} ,
2025-11-26 13:43:33 -08:00
combinedController . signal ,
2025-10-08 13:28:19 -07:00
this . config . getEnableInteractiveShell ( ) ,
2025-12-22 19:18:27 -08:00
{
. . . shellExecutionConfig ,
pager : 'cat' ,
sanitizationConfig :
shellExecutionConfig?.sanitizationConfig ? ?
this . config . sanitizationConfig ,
} ,
2025-09-11 13:27:27 -07:00
) ;
2026-01-30 09:53:09 -08:00
if ( pid ) {
if ( setPidCallback ) {
setPidCallback ( pid ) ;
}
// If the model requested to run in the background, do so after a short delay.
if ( this . params . is_background ) {
setTimeout ( ( ) = > {
ShellExecutionService . background ( pid ) ;
} , BACKGROUND_DELAY_MS ) ;
}
2025-09-11 13:27:27 -07:00
}
2025-05-09 23:29:02 -07:00
2025-07-25 21:56:49 -04:00
const result = await resultPromise ;
const backgroundPIDs : number [ ] = [ ] ;
if ( os . platform ( ) !== 'win32' ) {
2026-01-27 13:17:40 -08:00
let tempFileExists = false ;
try {
await fsPromises . access ( tempFilePath ) ;
tempFileExists = true ;
} catch {
tempFileExists = false ;
}
if ( tempFileExists ) {
const pgrepContent = await fsPromises . readFile ( tempFilePath , 'utf8' ) ;
const pgrepLines = pgrepContent . split ( EOL ) . filter ( Boolean ) ;
2025-07-25 21:56:49 -04:00
for ( const line of pgrepLines ) {
if ( ! /^\d+$/ . test ( line ) ) {
2025-10-29 13:20:11 -07:00
debugLogger . error ( ` pgrep: ${ line } ` ) ;
2025-07-25 21:56:49 -04:00
}
const pid = Number ( line ) ;
if ( pid !== result . pid ) {
backgroundPIDs . push ( pid ) ;
}
2025-06-09 12:19:42 -07:00
}
2025-07-25 21:56:49 -04:00
} else {
2026-01-30 09:53:09 -08:00
if ( ! signal . aborted && ! result . backgrounded ) {
2025-10-29 13:20:11 -07:00
debugLogger . error ( 'missing pgrep output' ) ;
2025-06-09 12:19:42 -07:00
}
2025-05-06 10:44:40 -07:00
}
}
2026-01-30 09:53:09 -08:00
let data : Record < string , unknown > | undefined ;
2025-07-25 21:56:49 -04:00
let llmContent = '' ;
2025-11-26 13:43:33 -08:00
let timeoutMessage = '' ;
2025-07-25 21:56:49 -04:00
if ( result . aborted ) {
2025-11-26 13:43:33 -08:00
if ( timeoutController . signal . aborted ) {
timeoutMessage = ` Command was automatically cancelled because it exceeded the timeout of ${ (
timeoutMs / 60000
) . toFixed ( 1 ) } minutes without output. ` ;
llmContent = timeoutMessage ;
} else {
llmContent =
'Command was cancelled by user before it could complete.' ;
}
2025-07-25 21:56:49 -04:00
if ( result . output . trim ( ) ) {
2025-08-19 16:03:51 -07:00
llmContent += ` Below is the output before it was cancelled: \ n ${ result . output } ` ;
2025-07-25 21:56:49 -04:00
} else {
llmContent += ' There was no output before it was cancelled.' ;
}
2026-01-30 09:53:09 -08:00
} else if ( this . params . is_background || result . backgrounded ) {
llmContent = ` Command moved to background (PID: ${ result . pid } ). Output hidden. Press Ctrl+B to view. ` ;
data = {
pid : result.pid ,
command : this.params.command ,
initialOutput : result.output ,
} ;
2025-05-27 13:47:40 -07:00
} else {
2025-07-25 21:56:49 -04:00
// Create a formatted error string for display, replacing the wrapper command
// with the user-facing command.
2026-01-26 10:12:21 -08:00
const llmContentParts = [ ` Output: ${ result . output || '(empty)' } ` ] ;
if ( result . error ) {
const finalError = result . error . message . replaceAll (
commandToExecute ,
this . params . command ,
) ;
llmContentParts . push ( ` Error: ${ finalError } ` ) ;
}
if ( result . exitCode !== null && result . exitCode !== 0 ) {
llmContentParts . push ( ` Exit Code: ${ result . exitCode } ` ) ;
}
if ( result . signal ) {
llmContentParts . push ( ` Signal: ${ result . signal } ` ) ;
}
if ( backgroundPIDs . length ) {
llmContentParts . push ( ` Background PIDs: ${ backgroundPIDs . join ( ', ' ) } ` ) ;
}
if ( result . pid ) {
llmContentParts . push ( ` Process Group PGID: ${ result . pid } ` ) ;
}
llmContent = llmContentParts . join ( '\n' ) ;
2025-05-27 13:47:40 -07:00
}
2025-04-28 15:05:36 -07:00
2025-07-25 21:56:49 -04:00
let returnDisplayMessage = '' ;
if ( this . config . getDebugMode ( ) ) {
returnDisplayMessage = llmContent ;
2025-05-18 00:23:57 -07:00
} else {
2026-01-30 09:53:09 -08:00
if ( this . params . is_background || result . backgrounded ) {
returnDisplayMessage = ` Command moved to background (PID: ${ result . pid } ). Output hidden. Press Ctrl+B to view. ` ;
} else if ( result . output . trim ( ) ) {
2025-07-25 21:56:49 -04:00
returnDisplayMessage = result . output ;
} else {
if ( result . aborted ) {
2025-11-26 13:43:33 -08:00
if ( timeoutMessage ) {
returnDisplayMessage = timeoutMessage ;
} else {
returnDisplayMessage = 'Command cancelled by user.' ;
}
2025-07-25 21:56:49 -04:00
} else if ( result . signal ) {
returnDisplayMessage = ` Command terminated by signal: ${ result . signal } ` ;
} else if ( result . error ) {
returnDisplayMessage = ` Command failed: ${ getErrorMessage (
result . error ,
) } ` ;
} else if ( result . exitCode !== null && result . exitCode !== 0 ) {
returnDisplayMessage = ` Command exited with code: ${ result . exitCode } ` ;
}
// If output is empty and command succeeded (code 0, no error/signal/abort),
// returnDisplayMessage will remain empty, which is fine.
2025-05-18 00:23:57 -07:00
}
}
2025-07-12 21:09:12 -07:00
2025-07-25 21:56:49 -04:00
const summarizeConfig = this . config . getSummarizeToolOutputConfig ( ) ;
2025-08-26 15:26:16 -04:00
const executionError = result . error
? {
error : {
message : result.error.message ,
type : ToolErrorType . SHELL_EXECUTE_ERROR ,
} ,
}
: { } ;
2025-10-17 21:07:26 -04:00
if ( summarizeConfig && summarizeConfig [ SHELL_TOOL_NAME ] ) {
2025-07-25 21:56:49 -04:00
const summary = await summarizeToolOutput (
2025-11-11 08:10:50 -08:00
this . config ,
{ model : 'summarizer-shell' } ,
2025-07-25 21:56:49 -04:00
llmContent ,
this . config . getGeminiClient ( ) ,
signal ,
) ;
return {
llmContent : summary ,
returnDisplay : returnDisplayMessage ,
2025-08-26 15:26:16 -04:00
. . . executionError ,
2025-07-25 21:56:49 -04:00
} ;
}
2025-07-15 10:22:31 -07:00
return {
2025-07-25 21:56:49 -04:00
llmContent ,
2025-07-15 10:22:31 -07:00
returnDisplay : returnDisplayMessage ,
2026-01-30 09:53:09 -08:00
data ,
2025-08-26 15:26:16 -04:00
. . . executionError ,
2025-07-15 10:22:31 -07:00
} ;
2025-07-25 21:56:49 -04:00
} finally {
2025-11-26 13:43:33 -08:00
if ( timeoutTimer ) clearTimeout ( timeoutTimer ) ;
signal . removeEventListener ( 'abort' , onAbort ) ;
timeoutController . signal . removeEventListener ( 'abort' , onAbort ) ;
2026-01-27 13:17:40 -08:00
try {
await fsPromises . unlink ( tempFilePath ) ;
} catch {
// Ignore errors during unlink
2025-07-25 21:56:49 -04:00
}
2025-07-15 10:22:31 -07:00
}
2025-04-24 18:03:33 -07:00
}
}
2025-08-13 12:27:09 -07:00
2026-01-30 09:53:09 -08:00
function getShellToolDescription ( enableInteractiveShell : boolean ) : string {
2025-08-15 12:08:29 -07:00
const returnedInfo = `
2025-08-13 12:27:09 -07:00
The following information is returned:
2026-01-26 10:12:21 -08:00
Output: Combined stdout/stderr. Can be \` (empty) \` or partial on error and for any unwaited background processes.
Exit Code: Only included if non-zero (command failed).
Error: Only included if a process-level error occurred (e.g., spawn failure).
Signal: Only included if process was terminated by a signal.
Background PIDs: Only included if background processes were started.
Process Group PGID: Only included if available. ` ;
2025-08-15 12:08:29 -07:00
if ( os . platform ( ) === 'win32' ) {
2026-01-30 09:53:09 -08:00
const backgroundInstructions = enableInteractiveShell
? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use PowerShell background constructs.'
: 'Command can start background processes using PowerShell constructs such as `Start-Process -NoNewWindow` or `Start-Job`.' ;
return ` This tool executes a given shell command as \` powershell.exe -NoProfile -Command <command> \` . ${ backgroundInstructions } ${ returnedInfo } ` ;
2025-08-15 12:08:29 -07:00
} else {
2026-01-30 09:53:09 -08:00
const backgroundInstructions = enableInteractiveShell
? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use `&` to background commands.'
: 'Command can start background processes using `&`.' ;
return ` This tool executes a given shell command as \` bash -c <command> \` . ${ backgroundInstructions } Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \` kill -- -PGID \` or signaled as \` kill -s SIGNAL -- -PGID \` . ${ returnedInfo } ` ;
2025-08-15 12:08:29 -07:00
}
}
function getCommandDescription ( ) : string {
if ( os . platform ( ) === 'win32' ) {
2025-10-16 17:25:30 -07:00
return 'Exact command to execute as `powershell.exe -NoProfile -Command <command>`' ;
2025-08-15 12:08:29 -07:00
} else {
2025-10-16 17:25:30 -07:00
return 'Exact bash command to execute as `bash -c <command>`' ;
2025-08-15 12:08:29 -07:00
}
}
export class ShellTool extends BaseDeclarativeTool <
ShellToolParams ,
ToolResult
> {
2025-10-20 22:35:35 -04:00
static readonly Name = SHELL_TOOL_NAME ;
2025-10-24 13:04:40 -07:00
constructor (
private readonly config : Config ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-24 13:04:40 -07:00
) {
2025-10-16 17:25:30 -07:00
void initializeShellParsers ( ) . catch ( ( ) = > {
// Errors are surfaced when parsing commands.
} ) ;
2025-08-15 12:08:29 -07:00
super (
2025-10-20 22:35:35 -04:00
ShellTool . Name ,
2025-08-15 12:08:29 -07:00
'Shell' ,
2026-01-30 09:53:09 -08:00
getShellToolDescription ( config . getEnableInteractiveShell ( ) ) ,
2025-08-13 12:27:09 -07:00
Kind . Execute ,
{
type : 'object' ,
properties : {
command : {
type : 'string' ,
2025-08-15 12:08:29 -07:00
description : getCommandDescription ( ) ,
2025-08-13 12:27:09 -07:00
} ,
description : {
type : 'string' ,
description :
'Brief description of the command for the user. Be specific and concise. Ideally a single sentence. Can be up to 3 sentences for clarity. No line breaks.' ,
} ,
2025-11-06 15:03:52 -08:00
dir_path : {
2025-08-13 12:27:09 -07:00
type : 'string' ,
description :
2025-11-06 15:03:52 -08:00
'(OPTIONAL) The path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist.' ,
2025-08-13 12:27:09 -07:00
} ,
2026-01-30 09:53:09 -08:00
is_background : {
type : 'boolean' ,
description :
'Set to true if this command should be run in the background (e.g. for long-running servers or watchers). The command will be started, allowed to run for a brief moment to check for immediate errors, and then moved to the background.' ,
} ,
2025-08-13 12:27:09 -07:00
} ,
required : [ 'command' ] ,
} ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-08-13 12:27:09 -07:00
false , // output is not markdown
true , // output can be updated
) ;
}
2025-08-19 13:55:06 -07:00
protected override validateToolParamValues (
2025-08-13 16:17:38 -04:00
params : ShellToolParams ,
) : string | null {
2025-10-16 17:25:30 -07:00
if ( ! params . command . trim ( ) ) {
return 'Command cannot be empty.' ;
}
2025-11-06 15:03:52 -08:00
if ( params . dir_path ) {
const resolvedPath = path . resolve (
this . config . getTargetDir ( ) ,
params . dir_path ,
2025-08-13 12:27:09 -07:00
) ;
2026-01-27 13:17:40 -08:00
return this . config . validatePathAccess ( resolvedPath ) ;
2025-08-13 12:27:09 -07:00
}
return null ;
}
protected createInvocation (
params : ShellToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-28 09:20:57 -07:00
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-13 12:27:09 -07:00
) : ToolInvocation < ShellToolParams , ToolResult > {
2025-10-24 13:04:40 -07:00
return new ShellToolInvocation (
this . config ,
params ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-10-28 09:20:57 -07:00
_toolName ,
_toolDisplayName ,
2025-10-24 13:04:40 -07:00
) ;
2025-08-13 12:27:09 -07:00
}
}