Path: blob/main/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type { Tool, ToolResultObject } from '@github/copilot-sdk';6import { generateUuid } from '../../../../base/common/uuid.js';7import { URI } from '../../../../base/common/uri.js';8import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';9import * as platform from '../../../../base/common/platform.js';10import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';11import { Emitter, Event } from '../../../../base/common/event.js';12import { ILogService } from '../../../log/common/log.js';13import { TerminalClaimKind, type TerminalSessionClaim } from '../../common/state/protocol/state.js';14import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';1516/**17* Maximum scrollback content (in bytes) returned to the model in tool results.18*/19const MAX_OUTPUT_BYTES = 80_000;2021/**22* Default command timeout in milliseconds (120 seconds).23*/24const DEFAULT_TIMEOUT_MS = 120_000;2526/**27* The sentinel prefix used to detect command completion in terminal output.28* The full sentinel format is: `<<<COPILOT_SENTINEL_<uuid>_EXIT_<code>>>`.29*/30const SENTINEL_PREFIX = '<<<COPILOT_SENTINEL_';3132/**33* Tracks a single persistent shell instance backed by a managed PTY terminal.34*/35interface IManagedShell {36readonly id: string;37readonly terminalUri: string;38readonly shellType: ShellType;39}4041export type ShellType = 'bash' | 'powershell';4243function getShellExecutable(shellType: ShellType): string {44if (shellType === 'powershell') {45return 'powershell.exe';46}47return process.env['SHELL'] || '/bin/bash';48}4950// ---------------------------------------------------------------------------51// ShellManager52// ---------------------------------------------------------------------------5354/**55* Per-session manager for persistent shell instances. Each shell is backed by56* a {@link IAgentHostTerminalManager} terminal and participates in AHP terminal57* claim semantics.58*59* Created via {@link IInstantiationService} once per session and disposed when60* the session ends.61*/62export class ShellManager {6364private readonly _shells = new Map<string, IManagedShell>();65private readonly _toolCallShells = new Map<string, string>();6667private readonly _onDidAssociateTerminal = new Emitter<{ toolCallId: string; terminalUri: string; displayName: string }>();68readonly onDidAssociateTerminal: Event<{ toolCallId: string; terminalUri: string; displayName: string }> = this._onDidAssociateTerminal.event;6970constructor(71private readonly _sessionUri: URI,72private readonly _workingDirectory: URI | undefined,73@IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager,74@ILogService private readonly _logService: ILogService,75) { }7677async getOrCreateShell(78shellType: ShellType,79turnId: string,80toolCallId: string,81cwd?: string,82): Promise<IManagedShell> {83for (const shell of this._shells.values()) {84if (shell.shellType === shellType && this._terminalManager.hasTerminal(shell.terminalUri)) {85const exitCode = this._terminalManager.getExitCode(shell.terminalUri);86if (exitCode === undefined) {87this._trackToolCall(toolCallId, shell.id);88return shell;89}90this._shells.delete(shell.id);91}92}9394const id = generateUuid();95const terminalUri = `agenthost-terminal://shell/${id}`;9697const claim: TerminalSessionClaim = {98kind: TerminalClaimKind.Session,99session: this._sessionUri.toString(),100turnId,101toolCallId,102};103104const shellDisplayName = shellType === 'bash' ? 'Bash' : 'PowerShell';105106await this._terminalManager.createTerminal({107terminal: terminalUri,108claim,109name: shellDisplayName,110cwd: cwd ?? this._workingDirectory?.fsPath,111}, { shell: getShellExecutable(shellType), preventShellHistory: true, nonInteractive: true });112113const shell: IManagedShell = { id, terminalUri, shellType };114this._shells.set(id, shell);115this._trackToolCall(toolCallId, id);116this._logService.info(`[ShellManager] Created ${shellType} shell ${id} (terminal=${terminalUri})`);117return shell;118}119120private _trackToolCall(toolCallId: string, shellId: string): void {121this._toolCallShells.set(toolCallId, shellId);122const shell = this._shells.get(shellId);123if (shell) {124const displayName = shell.shellType === 'bash' ? 'Bash' : 'PowerShell';125this._onDidAssociateTerminal.fire({ toolCallId, terminalUri: shell.terminalUri, displayName });126}127}128129getTerminalUriForToolCall(toolCallId: string): string | undefined {130const shellId = this._toolCallShells.get(toolCallId);131if (!shellId) {132return undefined;133}134return this._shells.get(shellId)?.terminalUri;135}136137getShell(id: string): IManagedShell | undefined {138return this._shells.get(id);139}140141listShells(): IManagedShell[] {142const result: IManagedShell[] = [];143for (const shell of this._shells.values()) {144if (this._terminalManager.hasTerminal(shell.terminalUri)) {145result.push(shell);146}147}148return result;149}150151shutdownShell(id: string): boolean {152const shell = this._shells.get(id);153if (!shell) {154return false;155}156this._terminalManager.disposeTerminal(shell.terminalUri);157this._shells.delete(id);158this._logService.info(`[ShellManager] Shut down shell ${id}`);159return true;160}161162dispose(): void {163for (const shell of this._shells.values()) {164if (this._terminalManager.hasTerminal(shell.terminalUri)) {165this._terminalManager.disposeTerminal(shell.terminalUri);166}167}168this._shells.clear();169this._toolCallShells.clear();170}171}172173// ---------------------------------------------------------------------------174// Sentinel helpers175// ---------------------------------------------------------------------------176177function makeSentinelId(): string {178return generateUuid().replace(/-/g, '');179}180181function buildSentinelCommand(sentinelId: string, shellType: ShellType): string {182if (shellType === 'powershell') {183return `Write-Output "${SENTINEL_PREFIX}${sentinelId}_EXIT_$LASTEXITCODE>>>"`;184}185return `echo "${SENTINEL_PREFIX}${sentinelId}_EXIT_$?>>>"`;186}187188/**189* For POSIX shells (bash/zsh) that honor `HISTCONTROL=ignorespace` /190* `HIST_IGNORE_SPACE`, prepending a single space prevents the command from191* being recorded in shell history. The shell integration scripts opt these192* settings in via the `VSCODE_PREVENT_SHELL_HISTORY` env var (set when the193* terminal is created with `preventShellHistory: true`). PowerShell194* suppresses history through PSReadLine instead, so no prefix is needed.195*196* Exported for tests.197*/198export function prefixForHistorySuppression(shellType: ShellType): string {199return shellType === 'powershell' ? '' : ' ';200}201202function parseSentinel(content: string, sentinelId: string): { found: boolean; exitCode: number; outputBeforeSentinel: string } {203const marker = `${SENTINEL_PREFIX}${sentinelId}_EXIT_`;204const idx = content.indexOf(marker);205if (idx === -1) {206return { found: false, exitCode: -1, outputBeforeSentinel: content };207}208209const outputBeforeSentinel = content.substring(0, idx);210const afterMarker = content.substring(idx + marker.length);211const endIdx = afterMarker.indexOf('>>>');212const exitCodeStr = endIdx >= 0 ? afterMarker.substring(0, endIdx) : afterMarker.trim();213const exitCode = parseInt(exitCodeStr, 10);214return {215found: true,216exitCode: isNaN(exitCode) ? -1 : exitCode,217outputBeforeSentinel,218};219}220221function prepareOutputForModel(rawOutput: string): string {222let text = removeAnsiEscapeCodes(rawOutput).trim();223if (text.length > MAX_OUTPUT_BYTES) {224text = text.substring(text.length - MAX_OUTPUT_BYTES);225}226return text;227}228229// ---------------------------------------------------------------------------230// Tool implementations231// ---------------------------------------------------------------------------232233function makeSuccessResult(text: string): ToolResultObject {234return { textResultForLlm: text, resultType: 'success' };235}236237function makeFailureResult(text: string, error?: string): ToolResultObject {238return { textResultForLlm: text, resultType: 'failure', error };239}240241async function executeCommandInShell(242shell: IManagedShell,243command: string,244timeoutMs: number,245terminalManager: IAgentHostTerminalManager,246logService: ILogService,247): Promise<ToolResultObject> {248const result = terminalManager.supportsCommandDetection(shell.terminalUri)249? await executeCommandWithShellIntegration(shell, command, timeoutMs, terminalManager, logService)250: await executeCommandWithSentinel(shell, command, timeoutMs, terminalManager, logService);251return {252...result,253textResultForLlm: `Shell ID: ${shell.id}\n${result.textResultForLlm}`,254};255}256257/**258* Execute a command using shell integration (OSC 633) for completion detection.259* No sentinel echo is injected — the shell's own command-finished signal260* provides the exit code and cleanly delineated output.261*/262async function executeCommandWithShellIntegration(263shell: IManagedShell,264command: string,265timeoutMs: number,266terminalManager: IAgentHostTerminalManager,267logService: ILogService,268): Promise<ToolResultObject> {269const disposables = new DisposableStore();270271terminalManager.writeInput(shell.terminalUri, `${prefixForHistorySuppression(shell.shellType)}${command}\r`);272273return new Promise<ToolResultObject>(resolve => {274let resolved = false;275const finish = (result: ToolResultObject) => {276if (resolved) {277return;278}279resolved = true;280disposables.dispose();281resolve(result);282};283284disposables.add(terminalManager.onCommandFinished(shell.terminalUri, event => {285const output = prepareOutputForModel(event.output);286const exitCode = event.exitCode ?? 0;287logService.info(`[ShellTool] Command completed (shell integration) with exit code ${exitCode}`);288if (exitCode === 0) {289finish(makeSuccessResult(`Exit code: ${exitCode}\n${output}`));290} else {291finish(makeFailureResult(`Exit code: ${exitCode}\n${output}`));292}293}));294295disposables.add(terminalManager.onExit(shell.terminalUri, (exitCode: number) => {296logService.info(`[ShellTool] Shell exited unexpectedly with code ${exitCode}`);297const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';298const output = prepareOutputForModel(fullContent);299finish(makeFailureResult(`Shell exited with code ${exitCode}\n${output}`));300}));301302disposables.add(terminalManager.onClaimChanged(shell.terminalUri, (claim) => {303if (claim.kind === TerminalClaimKind.Session && !claim.toolCallId) {304logService.info(`[ShellTool] Continuing in background (claim narrowed)`);305finish(makeSuccessResult('The user chose to continue this command in the background. The terminal is still running.'));306}307}));308309const timer = setTimeout(() => {310logService.warn(`[ShellTool] Command timed out after ${timeoutMs}ms`);311const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';312const output = prepareOutputForModel(fullContent);313finish(makeFailureResult(314`Command timed out after ${Math.round(timeoutMs / 1000)}s. Partial output:\n${output}`,315'timeout',316));317}, timeoutMs);318disposables.add(toDisposable(() => clearTimeout(timer)));319});320}321322/**323* Fallback: execute a command using a sentinel echo to detect completion.324* Used when shell integration is not available.325*/326async function executeCommandWithSentinel(327shell: IManagedShell,328command: string,329timeoutMs: number,330terminalManager: IAgentHostTerminalManager,331logService: ILogService,332): Promise<ToolResultObject> {333const sentinelId = makeSentinelId();334const sentinelCmd = buildSentinelCommand(sentinelId, shell.shellType);335const disposables = new DisposableStore();336337const contentBefore = terminalManager.getContent(shell.terminalUri) ?? '';338const offsetBefore = contentBefore.length;339340// PTY input uses \r for line endings — the PTY translates to \r\n341const input = `${prefixForHistorySuppression(shell.shellType)}${command}\r${sentinelCmd}\r`;342terminalManager.writeInput(shell.terminalUri, input);343344return new Promise<ToolResultObject>(resolve => {345let resolved = false;346const finish = (result: ToolResultObject) => {347if (resolved) {348return;349}350resolved = true;351disposables.dispose();352resolve(result);353};354355const checkForSentinel = () => {356const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';357// Clamp offset: the terminal manager trims content when it exceeds358// 100k chars (slices to last 80k). If trimming happened after we359// captured offsetBefore, scan from the start of the current buffer.360const clampedOffset = Math.min(offsetBefore, fullContent.length);361const newContent = fullContent.substring(clampedOffset);362const parsed = parseSentinel(newContent, sentinelId);363if (parsed.found) {364const output = prepareOutputForModel(parsed.outputBeforeSentinel);365logService.info(`[ShellTool] Command completed with exit code ${parsed.exitCode}`);366if (parsed.exitCode === 0) {367finish(makeSuccessResult(`Exit code: ${parsed.exitCode}\n${output}`));368} else {369finish(makeFailureResult(`Exit code: ${parsed.exitCode}\n${output}`));370}371}372};373374disposables.add(terminalManager.onData(shell.terminalUri, () => {375checkForSentinel();376}));377378disposables.add(terminalManager.onExit(shell.terminalUri, (exitCode: number) => {379logService.info(`[ShellTool] Shell exited unexpectedly with code ${exitCode}`);380const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';381const newContent = fullContent.substring(offsetBefore);382const output = prepareOutputForModel(newContent);383finish(makeFailureResult(`Shell exited with code ${exitCode}\n${output}`));384}));385386disposables.add(terminalManager.onClaimChanged(shell.terminalUri, (claim) => {387if (claim.kind === TerminalClaimKind.Session && !claim.toolCallId) {388logService.info(`[ShellTool] Continuing in background (claim narrowed)`);389finish(makeSuccessResult('The user chose to continue this command in the background. The terminal is still running.'));390}391}));392393const timer = setTimeout(() => {394logService.warn(`[ShellTool] Command timed out after ${timeoutMs}ms`);395const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';396const newContent = fullContent.substring(offsetBefore);397const output = prepareOutputForModel(newContent);398finish(makeFailureResult(399`Command timed out after ${Math.round(timeoutMs / 1000)}s. Partial output:\n${output}`,400'timeout',401));402}, timeoutMs);403disposables.add(toDisposable(() => clearTimeout(timer)));404405checkForSentinel();406});407}408409// ---------------------------------------------------------------------------410// Public factory411// ---------------------------------------------------------------------------412413interface IShellToolArgs {414command: string;415timeout?: number;416}417418interface IWriteShellArgs {419command: string;420}421422interface IReadShellArgs {423shell_id?: string;424}425426interface IShutdownShellArgs {427shell_id?: string;428}429430/**431* Creates the set of SDK {@link Tool} definitions that override the built-in432* Copilot CLI shell tools with PTY-backed implementations.433*434* Returns tools for the platform-appropriate shell (bash or powershell),435* including companion tools (read, write, shutdown, list).436*/437export function createShellTools(438shellManager: ShellManager,439terminalManager: IAgentHostTerminalManager,440logService: ILogService,441// eslint-disable-next-line @typescript-eslint/no-explicit-any442): Tool<any>[] {443const shellType: ShellType = platform.isWindows ? 'powershell' : 'bash';444445const primaryTool: Tool<IShellToolArgs> = {446name: shellType,447description: shellType === 'bash' ? createBashModelDescription(false) : createPowerShellModelDescription(shellType, 'pwsh.exe', false),448parameters: {449type: 'object',450properties: {451command: { type: 'string', description: 'The command to execute' },452timeout: { type: 'number', description: 'Timeout in milliseconds (default 120000)' },453},454required: ['command'],455},456overridesBuiltInTool: true,457handler: async (args, invocation) => {458const shell = await shellManager.getOrCreateShell(459shellType,460invocation.toolCallId,461invocation.toolCallId,462);463const timeoutMs = args.timeout ?? DEFAULT_TIMEOUT_MS;464return executeCommandInShell(shell, args.command, timeoutMs, terminalManager, logService);465},466};467468const readTool: Tool<IReadShellArgs> = {469name: `read_${shellType}`,470description: `Read the latest output from a running ${shellType} shell.`,471parameters: {472type: 'object',473properties: {474shell_id: { type: 'string', description: 'Shell ID to read from (optional; uses latest shell if omitted)' },475},476},477overridesBuiltInTool: true,478skipPermission: true,479handler: (args) => {480const shells = shellManager.listShells();481const shell = args.shell_id482? shellManager.getShell(args.shell_id)483: shells[shells.length - 1];484if (!shell) {485return makeFailureResult('No active shell found.', 'no_shell');486}487const content = terminalManager.getContent(shell.terminalUri);488if (!content) {489return makeSuccessResult('(no output)');490}491return makeSuccessResult(prepareOutputForModel(content));492},493};494495const writeTool: Tool<IWriteShellArgs> = {496name: `write_${shellType}`,497description: `Send input to a running ${shellType} shell (e.g. answering a prompt, sending Ctrl+C).`,498parameters: {499type: 'object',500properties: {501command: { type: 'string', description: 'Text to write to the shell stdin' },502},503required: ['command'],504},505overridesBuiltInTool: true,506skipPermission: true,507handler: (args) => {508const shells = shellManager.listShells();509const shell = shells[shells.length - 1];510if (!shell) {511return makeFailureResult('No active shell found.', 'no_shell');512}513terminalManager.writeInput(shell.terminalUri, args.command);514return makeSuccessResult('Input sent to shell.');515},516};517518const shutdownTool: Tool<IShutdownShellArgs> = {519name: shellType === 'bash' ? 'bash_shutdown' : `${shellType}_shutdown`,520description: `Stop a ${shellType} shell.`,521parameters: {522type: 'object',523properties: {524shell_id: { type: 'string', description: 'Shell ID to stop (optional; stops latest shell if omitted)' },525},526},527overridesBuiltInTool: true,528skipPermission: true,529handler: (args) => {530if (args.shell_id) {531const success = shellManager.shutdownShell(args.shell_id);532return success533? makeSuccessResult('Shell stopped.')534: makeFailureResult('Shell not found.', 'not_found');535}536const shells = shellManager.listShells();537const shell = shells[shells.length - 1];538if (!shell) {539return makeFailureResult('No active shell to stop.', 'no_shell');540}541shellManager.shutdownShell(shell.id);542return makeSuccessResult('Shell stopped.');543},544};545546const listTool: Tool<Record<string, never>> = {547name: `list_${shellType}`,548description: `List active ${shellType} shell instances.`,549parameters: { type: 'object', properties: {} },550overridesBuiltInTool: true,551skipPermission: true,552handler: () => {553const shells = shellManager.listShells();554if (shells.length === 0) {555return makeSuccessResult('No active shells.');556}557const descriptions = shells.map(s => {558const exitCode = terminalManager.getExitCode(s.terminalUri);559const status = exitCode !== undefined ? `exited (${exitCode})` : 'running';560return `- ${s.id}: ${s.shellType} [${status}]`;561});562return makeSuccessResult(descriptions.join('\n'));563},564};565566return [primaryTool, readTool, writeTool, shutdownTool, listTool];567}568interface ITerminalSandboxResolvedNetworkDomains {569allowedDomains: string[];570deniedDomains: string[];571}572573function isWindowsPowerShell(envShell: string): boolean {574return envShell.endsWith('System32\\WindowsPowerShell\\v1.0\\powershell.exe');575}576577function createPowerShellModelDescription(shellType: string, shellPath: string, isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {578const isWinPwsh = isWindowsPowerShell(shellPath);579const parts = [580`This tool allows you to execute ${isWinPwsh ? 'Windows PowerShell 5.1' : 'PowerShell'} commands in a persistent terminal session, preserving environment variables, working directory, and other context across multiple commands.`,581'',582'Command Execution:',583// IMPORTANT: PowerShell 5 does not support `&&` so always re-write them to `;`. Note that584// the behavior of `&&` differs a little from `;` but in general it's fine585isWinPwsh ? '- Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly' : '- Prefer ; when chaining commands on one line',586'- Prefer pipelines | for object-based data flow',587'- Never create a sub-shell (eg. powershell -c "command") unless explicitly asked',588'',589'Directory Management:',590'- Prefer relative paths when navigating directories, only use absolute when the path is far away or the current cwd is not expected',591'- By default (mode=sync), shell and cwd are reused by subsequent sync commands',592'- Use $PWD or Get-Location for current directory',593'- Use Push-Location/Pop-Location for directory stack',594'',595'Program Execution:',596'- Supports .NET, Python, Node.js, and other executables',597'- Install modules via Install-Module, Install-Package',598'- Use Get-Command to verify cmdlet/function availability',599'',600'Async Mode:',601'- For long-running tasks (e.g., servers), use mode=async',602'- Returns a terminal ID for checking status and runtime later',603'- Use Start-Job for background PowerShell jobs',604'',605`Use write_${shellType} to send commands or input to a terminal session.`,606];607608if (isSandboxEnabled) {609parts.push(...createSandboxLines(networkDomains));610}611612parts.push(613'',614'Output Management:',615'- Output is automatically truncated if longer than 60KB to prevent context overflow',616'- Use Select-Object, Where-Object, Format-Table to filter output',617'- Use -First/-Last parameters to limit results',618'- For pager commands, add | Out-String or | Format-List',619'',620'Best Practices:',621'- Use proper cmdlet names instead of aliases in scripts',622'- Quote paths with spaces: "C:\\Path With Spaces"',623'- Prefer PowerShell cmdlets over external commands when available',624'- Prefer idiomatic PowerShell like Get-ChildItem instead of dir or ls for file listings',625'- Use Test-Path to check file/directory existence',626'- Be specific with Select-Object properties to avoid excessive output',627'- Avoid printing credentials unless absolutely required',628'',629'Interactive Input Handling:',630'- When a terminal command is waiting for interactive input, do NOT suggest alternatives or ask the user whether to proceed. Instead, use the ask_user tool to collect the needed values from the user, then send them.',631`- Send exactly one answer per prompt using write_${shellType}. Never send multiple answers in a single send.`,632`- After each send, call read_${shellType} to read the next prompt before sending the next answer.`,633'- Continue one prompt at a time until the command finishes.',634);635636return parts.join('\n');637}638639function createSandboxLines(networkDomains?: ITerminalSandboxResolvedNetworkDomains): string[] {640const lines = [641'',642'Sandboxing:',643'- ATTENTION: Terminal sandboxing is enabled, commands run in a sandbox by default',644'- When executing commands within the sandboxed environment, all operations requiring a temporary directory must utilize the $TMPDIR environment variable. The /tmp directory is not guaranteed to be accessible or writable and must be avoided',645'- Tools and scripts should respect the TMPDIR environment variable, which is automatically set to an appropriate path within the sandbox',646'- When a command fails due to sandbox restrictions, immediately re-run it with requestUnsandboxedExecution=true. Do NOT ask the user for permission — setting this flag automatically shows a confirmation prompt to the user',647'- Only set requestUnsandboxedExecution=true when there is evidence of failures caused by the sandbox, e.g. \'Operation not permitted\' errors, network failures, or file access errors, etc',648'- Do NOT set requestUnsandboxedExecution=true without first executing the command in sandbox mode. Always try the command in the sandbox first, and only set requestUnsandboxedExecution=true when retrying after that sandboxed execution failed due to sandbox restrictions.',649'- When setting requestUnsandboxedExecution=true, also provide requestUnsandboxedExecutionReason explaining why the command needs unsandboxed access',650];651if (networkDomains) {652const deniedSet = new Set(networkDomains.deniedDomains);653const effectiveAllowed = networkDomains.allowedDomains.filter(d => !deniedSet.has(d));654if (effectiveAllowed.length === 0) {655lines.push('- All network access is blocked in the sandbox');656} else {657lines.push(`- Only the following domains are accessible in the sandbox (all other network access is blocked): ${effectiveAllowed.join(', ')}`);658}659if (networkDomains.deniedDomains.length > 0) {660lines.push(`- The following domains are explicitly blocked in the sandbox: ${networkDomains.deniedDomains.join(', ')}`);661}662}663return lines;664}665666function createGenericDescription(shellType: string, isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {667const parts = [`668Command Execution:669- Use && to chain simple commands on one line670- Prefer pipelines | over temporary files for data flow671- Never create a sub-shell (eg. bash -c "command") unless explicitly asked672673Directory Management:674- Prefer relative paths when navigating directories, only use absolute when the path is far away or the current cwd is not expected675- By default (mode=sync), shell and cwd are reused by subsequent sync commands676- Use $PWD for current directory references677- Consider using pushd/popd for directory stack management678- Supports directory shortcuts like ~ and -679680Program Execution:681- Supports Python, Node.js, and other executables682- Install packages via package managers (brew, apt, etc.)683- Use which or command -v to verify command availability684685Async Mode:686- For long-running tasks (e.g., servers), use mode=async687- Returns a terminal ID for checking status and runtime later688689Use write_${shellType} to send commands or input to a terminal session.`];690691if (isSandboxEnabled) {692parts.push(createSandboxLines(networkDomains).join('\n'));693}694695parts.push(`696697Output Management:698- Output is automatically truncated if longer than 60KB to prevent context overflow699- Use head, tail, grep, awk to filter and limit output size700- For pager commands, disable paging: git --no-pager or add | cat701- Use wc -l to count lines before displaying large outputs702703Best Practices:704- Quote variables: "$var" instead of $var to handle spaces705- Use find with -exec or xargs for file operations706- Be specific with commands to avoid excessive output707- Avoid printing credentials unless absolutely required708- NEVER run sleep or similar wait commands in a terminal. You will be automatically notified on your next turn when async terminal commands or timed-out sync commands complete or need input. Do NOT poll for completion.709710Interactive Input Handling:711- When a terminal command is waiting for interactive input, do NOT suggest alternatives or ask the user whether to proceed. Instead, use the ask_user tool to collect the needed values from the user, then send them.712- Send exactly one answer per prompt using write_${shellType}. Never send multiple answers in a single send.713- After each send, call read_${shellType} to read the next prompt before sending the next answer.714- Continue one prompt at a time until the command finishes.`);715716return parts.join('');717}718719function createBashModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {720return [721'This tool allows you to execute shell commands in a persistent bash terminal session, preserving environment variables, working directory, and other context across multiple commands.',722createGenericDescription('bash', isSandboxEnabled, networkDomains),723'- Use [[ ]] for conditional tests instead of [ ]',724'- Prefer $() over backticks for command substitution',725'- Use set -e at start of complex commands to exit on errors'726].join('\n');727}728729730