Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { Tool, ToolResultObject } from '@github/copilot-sdk';
7
import { generateUuid } from '../../../../base/common/uuid.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
10
import * as platform from '../../../../base/common/platform.js';
11
import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
12
import { Emitter, Event } from '../../../../base/common/event.js';
13
import { ILogService } from '../../../log/common/log.js';
14
import { TerminalClaimKind, type TerminalSessionClaim } from '../../common/state/protocol/state.js';
15
import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js';
16
17
/**
18
* Maximum scrollback content (in bytes) returned to the model in tool results.
19
*/
20
const MAX_OUTPUT_BYTES = 80_000;
21
22
/**
23
* Default command timeout in milliseconds (120 seconds).
24
*/
25
const DEFAULT_TIMEOUT_MS = 120_000;
26
27
/**
28
* The sentinel prefix used to detect command completion in terminal output.
29
* The full sentinel format is: `<<<COPILOT_SENTINEL_<uuid>_EXIT_<code>>>`.
30
*/
31
const SENTINEL_PREFIX = '<<<COPILOT_SENTINEL_';
32
33
/**
34
* Tracks a single persistent shell instance backed by a managed PTY terminal.
35
*/
36
interface IManagedShell {
37
readonly id: string;
38
readonly terminalUri: string;
39
readonly shellType: ShellType;
40
}
41
42
export type ShellType = 'bash' | 'powershell';
43
44
function getShellExecutable(shellType: ShellType): string {
45
if (shellType === 'powershell') {
46
return 'powershell.exe';
47
}
48
return process.env['SHELL'] || '/bin/bash';
49
}
50
51
// ---------------------------------------------------------------------------
52
// ShellManager
53
// ---------------------------------------------------------------------------
54
55
/**
56
* Per-session manager for persistent shell instances. Each shell is backed by
57
* a {@link IAgentHostTerminalManager} terminal and participates in AHP terminal
58
* claim semantics.
59
*
60
* Created via {@link IInstantiationService} once per session and disposed when
61
* the session ends.
62
*/
63
export class ShellManager {
64
65
private readonly _shells = new Map<string, IManagedShell>();
66
private readonly _toolCallShells = new Map<string, string>();
67
68
private readonly _onDidAssociateTerminal = new Emitter<{ toolCallId: string; terminalUri: string; displayName: string }>();
69
readonly onDidAssociateTerminal: Event<{ toolCallId: string; terminalUri: string; displayName: string }> = this._onDidAssociateTerminal.event;
70
71
constructor(
72
private readonly _sessionUri: URI,
73
private readonly _workingDirectory: URI | undefined,
74
@IAgentHostTerminalManager private readonly _terminalManager: IAgentHostTerminalManager,
75
@ILogService private readonly _logService: ILogService,
76
) { }
77
78
async getOrCreateShell(
79
shellType: ShellType,
80
turnId: string,
81
toolCallId: string,
82
cwd?: string,
83
): Promise<IManagedShell> {
84
for (const shell of this._shells.values()) {
85
if (shell.shellType === shellType && this._terminalManager.hasTerminal(shell.terminalUri)) {
86
const exitCode = this._terminalManager.getExitCode(shell.terminalUri);
87
if (exitCode === undefined) {
88
this._trackToolCall(toolCallId, shell.id);
89
return shell;
90
}
91
this._shells.delete(shell.id);
92
}
93
}
94
95
const id = generateUuid();
96
const terminalUri = `agenthost-terminal://shell/${id}`;
97
98
const claim: TerminalSessionClaim = {
99
kind: TerminalClaimKind.Session,
100
session: this._sessionUri.toString(),
101
turnId,
102
toolCallId,
103
};
104
105
const shellDisplayName = shellType === 'bash' ? 'Bash' : 'PowerShell';
106
107
await this._terminalManager.createTerminal({
108
terminal: terminalUri,
109
claim,
110
name: shellDisplayName,
111
cwd: cwd ?? this._workingDirectory?.fsPath,
112
}, { shell: getShellExecutable(shellType), preventShellHistory: true, nonInteractive: true });
113
114
const shell: IManagedShell = { id, terminalUri, shellType };
115
this._shells.set(id, shell);
116
this._trackToolCall(toolCallId, id);
117
this._logService.info(`[ShellManager] Created ${shellType} shell ${id} (terminal=${terminalUri})`);
118
return shell;
119
}
120
121
private _trackToolCall(toolCallId: string, shellId: string): void {
122
this._toolCallShells.set(toolCallId, shellId);
123
const shell = this._shells.get(shellId);
124
if (shell) {
125
const displayName = shell.shellType === 'bash' ? 'Bash' : 'PowerShell';
126
this._onDidAssociateTerminal.fire({ toolCallId, terminalUri: shell.terminalUri, displayName });
127
}
128
}
129
130
getTerminalUriForToolCall(toolCallId: string): string | undefined {
131
const shellId = this._toolCallShells.get(toolCallId);
132
if (!shellId) {
133
return undefined;
134
}
135
return this._shells.get(shellId)?.terminalUri;
136
}
137
138
getShell(id: string): IManagedShell | undefined {
139
return this._shells.get(id);
140
}
141
142
listShells(): IManagedShell[] {
143
const result: IManagedShell[] = [];
144
for (const shell of this._shells.values()) {
145
if (this._terminalManager.hasTerminal(shell.terminalUri)) {
146
result.push(shell);
147
}
148
}
149
return result;
150
}
151
152
shutdownShell(id: string): boolean {
153
const shell = this._shells.get(id);
154
if (!shell) {
155
return false;
156
}
157
this._terminalManager.disposeTerminal(shell.terminalUri);
158
this._shells.delete(id);
159
this._logService.info(`[ShellManager] Shut down shell ${id}`);
160
return true;
161
}
162
163
dispose(): void {
164
for (const shell of this._shells.values()) {
165
if (this._terminalManager.hasTerminal(shell.terminalUri)) {
166
this._terminalManager.disposeTerminal(shell.terminalUri);
167
}
168
}
169
this._shells.clear();
170
this._toolCallShells.clear();
171
}
172
}
173
174
// ---------------------------------------------------------------------------
175
// Sentinel helpers
176
// ---------------------------------------------------------------------------
177
178
function makeSentinelId(): string {
179
return generateUuid().replace(/-/g, '');
180
}
181
182
function buildSentinelCommand(sentinelId: string, shellType: ShellType): string {
183
if (shellType === 'powershell') {
184
return `Write-Output "${SENTINEL_PREFIX}${sentinelId}_EXIT_$LASTEXITCODE>>>"`;
185
}
186
return `echo "${SENTINEL_PREFIX}${sentinelId}_EXIT_$?>>>"`;
187
}
188
189
/**
190
* For POSIX shells (bash/zsh) that honor `HISTCONTROL=ignorespace` /
191
* `HIST_IGNORE_SPACE`, prepending a single space prevents the command from
192
* being recorded in shell history. The shell integration scripts opt these
193
* settings in via the `VSCODE_PREVENT_SHELL_HISTORY` env var (set when the
194
* terminal is created with `preventShellHistory: true`). PowerShell
195
* suppresses history through PSReadLine instead, so no prefix is needed.
196
*
197
* Exported for tests.
198
*/
199
export function prefixForHistorySuppression(shellType: ShellType): string {
200
return shellType === 'powershell' ? '' : ' ';
201
}
202
203
function parseSentinel(content: string, sentinelId: string): { found: boolean; exitCode: number; outputBeforeSentinel: string } {
204
const marker = `${SENTINEL_PREFIX}${sentinelId}_EXIT_`;
205
const idx = content.indexOf(marker);
206
if (idx === -1) {
207
return { found: false, exitCode: -1, outputBeforeSentinel: content };
208
}
209
210
const outputBeforeSentinel = content.substring(0, idx);
211
const afterMarker = content.substring(idx + marker.length);
212
const endIdx = afterMarker.indexOf('>>>');
213
const exitCodeStr = endIdx >= 0 ? afterMarker.substring(0, endIdx) : afterMarker.trim();
214
const exitCode = parseInt(exitCodeStr, 10);
215
return {
216
found: true,
217
exitCode: isNaN(exitCode) ? -1 : exitCode,
218
outputBeforeSentinel,
219
};
220
}
221
222
function prepareOutputForModel(rawOutput: string): string {
223
let text = removeAnsiEscapeCodes(rawOutput).trim();
224
if (text.length > MAX_OUTPUT_BYTES) {
225
text = text.substring(text.length - MAX_OUTPUT_BYTES);
226
}
227
return text;
228
}
229
230
// ---------------------------------------------------------------------------
231
// Tool implementations
232
// ---------------------------------------------------------------------------
233
234
function makeSuccessResult(text: string): ToolResultObject {
235
return { textResultForLlm: text, resultType: 'success' };
236
}
237
238
function makeFailureResult(text: string, error?: string): ToolResultObject {
239
return { textResultForLlm: text, resultType: 'failure', error };
240
}
241
242
async function executeCommandInShell(
243
shell: IManagedShell,
244
command: string,
245
timeoutMs: number,
246
terminalManager: IAgentHostTerminalManager,
247
logService: ILogService,
248
): Promise<ToolResultObject> {
249
const result = terminalManager.supportsCommandDetection(shell.terminalUri)
250
? await executeCommandWithShellIntegration(shell, command, timeoutMs, terminalManager, logService)
251
: await executeCommandWithSentinel(shell, command, timeoutMs, terminalManager, logService);
252
return {
253
...result,
254
textResultForLlm: `Shell ID: ${shell.id}\n${result.textResultForLlm}`,
255
};
256
}
257
258
/**
259
* Execute a command using shell integration (OSC 633) for completion detection.
260
* No sentinel echo is injected — the shell's own command-finished signal
261
* provides the exit code and cleanly delineated output.
262
*/
263
async function executeCommandWithShellIntegration(
264
shell: IManagedShell,
265
command: string,
266
timeoutMs: number,
267
terminalManager: IAgentHostTerminalManager,
268
logService: ILogService,
269
): Promise<ToolResultObject> {
270
const disposables = new DisposableStore();
271
272
terminalManager.writeInput(shell.terminalUri, `${prefixForHistorySuppression(shell.shellType)}${command}\r`);
273
274
return new Promise<ToolResultObject>(resolve => {
275
let resolved = false;
276
const finish = (result: ToolResultObject) => {
277
if (resolved) {
278
return;
279
}
280
resolved = true;
281
disposables.dispose();
282
resolve(result);
283
};
284
285
disposables.add(terminalManager.onCommandFinished(shell.terminalUri, event => {
286
const output = prepareOutputForModel(event.output);
287
const exitCode = event.exitCode ?? 0;
288
logService.info(`[ShellTool] Command completed (shell integration) with exit code ${exitCode}`);
289
if (exitCode === 0) {
290
finish(makeSuccessResult(`Exit code: ${exitCode}\n${output}`));
291
} else {
292
finish(makeFailureResult(`Exit code: ${exitCode}\n${output}`));
293
}
294
}));
295
296
disposables.add(terminalManager.onExit(shell.terminalUri, (exitCode: number) => {
297
logService.info(`[ShellTool] Shell exited unexpectedly with code ${exitCode}`);
298
const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
299
const output = prepareOutputForModel(fullContent);
300
finish(makeFailureResult(`Shell exited with code ${exitCode}\n${output}`));
301
}));
302
303
disposables.add(terminalManager.onClaimChanged(shell.terminalUri, (claim) => {
304
if (claim.kind === TerminalClaimKind.Session && !claim.toolCallId) {
305
logService.info(`[ShellTool] Continuing in background (claim narrowed)`);
306
finish(makeSuccessResult('The user chose to continue this command in the background. The terminal is still running.'));
307
}
308
}));
309
310
const timer = setTimeout(() => {
311
logService.warn(`[ShellTool] Command timed out after ${timeoutMs}ms`);
312
const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
313
const output = prepareOutputForModel(fullContent);
314
finish(makeFailureResult(
315
`Command timed out after ${Math.round(timeoutMs / 1000)}s. Partial output:\n${output}`,
316
'timeout',
317
));
318
}, timeoutMs);
319
disposables.add(toDisposable(() => clearTimeout(timer)));
320
});
321
}
322
323
/**
324
* Fallback: execute a command using a sentinel echo to detect completion.
325
* Used when shell integration is not available.
326
*/
327
async function executeCommandWithSentinel(
328
shell: IManagedShell,
329
command: string,
330
timeoutMs: number,
331
terminalManager: IAgentHostTerminalManager,
332
logService: ILogService,
333
): Promise<ToolResultObject> {
334
const sentinelId = makeSentinelId();
335
const sentinelCmd = buildSentinelCommand(sentinelId, shell.shellType);
336
const disposables = new DisposableStore();
337
338
const contentBefore = terminalManager.getContent(shell.terminalUri) ?? '';
339
const offsetBefore = contentBefore.length;
340
341
// PTY input uses \r for line endings — the PTY translates to \r\n
342
const input = `${prefixForHistorySuppression(shell.shellType)}${command}\r${sentinelCmd}\r`;
343
terminalManager.writeInput(shell.terminalUri, input);
344
345
return new Promise<ToolResultObject>(resolve => {
346
let resolved = false;
347
const finish = (result: ToolResultObject) => {
348
if (resolved) {
349
return;
350
}
351
resolved = true;
352
disposables.dispose();
353
resolve(result);
354
};
355
356
const checkForSentinel = () => {
357
const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
358
// Clamp offset: the terminal manager trims content when it exceeds
359
// 100k chars (slices to last 80k). If trimming happened after we
360
// captured offsetBefore, scan from the start of the current buffer.
361
const clampedOffset = Math.min(offsetBefore, fullContent.length);
362
const newContent = fullContent.substring(clampedOffset);
363
const parsed = parseSentinel(newContent, sentinelId);
364
if (parsed.found) {
365
const output = prepareOutputForModel(parsed.outputBeforeSentinel);
366
logService.info(`[ShellTool] Command completed with exit code ${parsed.exitCode}`);
367
if (parsed.exitCode === 0) {
368
finish(makeSuccessResult(`Exit code: ${parsed.exitCode}\n${output}`));
369
} else {
370
finish(makeFailureResult(`Exit code: ${parsed.exitCode}\n${output}`));
371
}
372
}
373
};
374
375
disposables.add(terminalManager.onData(shell.terminalUri, () => {
376
checkForSentinel();
377
}));
378
379
disposables.add(terminalManager.onExit(shell.terminalUri, (exitCode: number) => {
380
logService.info(`[ShellTool] Shell exited unexpectedly with code ${exitCode}`);
381
const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
382
const newContent = fullContent.substring(offsetBefore);
383
const output = prepareOutputForModel(newContent);
384
finish(makeFailureResult(`Shell exited with code ${exitCode}\n${output}`));
385
}));
386
387
disposables.add(terminalManager.onClaimChanged(shell.terminalUri, (claim) => {
388
if (claim.kind === TerminalClaimKind.Session && !claim.toolCallId) {
389
logService.info(`[ShellTool] Continuing in background (claim narrowed)`);
390
finish(makeSuccessResult('The user chose to continue this command in the background. The terminal is still running.'));
391
}
392
}));
393
394
const timer = setTimeout(() => {
395
logService.warn(`[ShellTool] Command timed out after ${timeoutMs}ms`);
396
const fullContent = terminalManager.getContent(shell.terminalUri) ?? '';
397
const newContent = fullContent.substring(offsetBefore);
398
const output = prepareOutputForModel(newContent);
399
finish(makeFailureResult(
400
`Command timed out after ${Math.round(timeoutMs / 1000)}s. Partial output:\n${output}`,
401
'timeout',
402
));
403
}, timeoutMs);
404
disposables.add(toDisposable(() => clearTimeout(timer)));
405
406
checkForSentinel();
407
});
408
}
409
410
// ---------------------------------------------------------------------------
411
// Public factory
412
// ---------------------------------------------------------------------------
413
414
interface IShellToolArgs {
415
command: string;
416
timeout?: number;
417
}
418
419
interface IWriteShellArgs {
420
command: string;
421
}
422
423
interface IReadShellArgs {
424
shell_id?: string;
425
}
426
427
interface IShutdownShellArgs {
428
shell_id?: string;
429
}
430
431
/**
432
* Creates the set of SDK {@link Tool} definitions that override the built-in
433
* Copilot CLI shell tools with PTY-backed implementations.
434
*
435
* Returns tools for the platform-appropriate shell (bash or powershell),
436
* including companion tools (read, write, shutdown, list).
437
*/
438
export function createShellTools(
439
shellManager: ShellManager,
440
terminalManager: IAgentHostTerminalManager,
441
logService: ILogService,
442
// eslint-disable-next-line @typescript-eslint/no-explicit-any
443
): Tool<any>[] {
444
const shellType: ShellType = platform.isWindows ? 'powershell' : 'bash';
445
446
const primaryTool: Tool<IShellToolArgs> = {
447
name: shellType,
448
description: shellType === 'bash' ? createBashModelDescription(false) : createPowerShellModelDescription(shellType, 'pwsh.exe', false),
449
parameters: {
450
type: 'object',
451
properties: {
452
command: { type: 'string', description: 'The command to execute' },
453
timeout: { type: 'number', description: 'Timeout in milliseconds (default 120000)' },
454
},
455
required: ['command'],
456
},
457
overridesBuiltInTool: true,
458
handler: async (args, invocation) => {
459
const shell = await shellManager.getOrCreateShell(
460
shellType,
461
invocation.toolCallId,
462
invocation.toolCallId,
463
);
464
const timeoutMs = args.timeout ?? DEFAULT_TIMEOUT_MS;
465
return executeCommandInShell(shell, args.command, timeoutMs, terminalManager, logService);
466
},
467
};
468
469
const readTool: Tool<IReadShellArgs> = {
470
name: `read_${shellType}`,
471
description: `Read the latest output from a running ${shellType} shell.`,
472
parameters: {
473
type: 'object',
474
properties: {
475
shell_id: { type: 'string', description: 'Shell ID to read from (optional; uses latest shell if omitted)' },
476
},
477
},
478
overridesBuiltInTool: true,
479
skipPermission: true,
480
handler: (args) => {
481
const shells = shellManager.listShells();
482
const shell = args.shell_id
483
? shellManager.getShell(args.shell_id)
484
: shells[shells.length - 1];
485
if (!shell) {
486
return makeFailureResult('No active shell found.', 'no_shell');
487
}
488
const content = terminalManager.getContent(shell.terminalUri);
489
if (!content) {
490
return makeSuccessResult('(no output)');
491
}
492
return makeSuccessResult(prepareOutputForModel(content));
493
},
494
};
495
496
const writeTool: Tool<IWriteShellArgs> = {
497
name: `write_${shellType}`,
498
description: `Send input to a running ${shellType} shell (e.g. answering a prompt, sending Ctrl+C).`,
499
parameters: {
500
type: 'object',
501
properties: {
502
command: { type: 'string', description: 'Text to write to the shell stdin' },
503
},
504
required: ['command'],
505
},
506
overridesBuiltInTool: true,
507
skipPermission: true,
508
handler: (args) => {
509
const shells = shellManager.listShells();
510
const shell = shells[shells.length - 1];
511
if (!shell) {
512
return makeFailureResult('No active shell found.', 'no_shell');
513
}
514
terminalManager.writeInput(shell.terminalUri, args.command);
515
return makeSuccessResult('Input sent to shell.');
516
},
517
};
518
519
const shutdownTool: Tool<IShutdownShellArgs> = {
520
name: shellType === 'bash' ? 'bash_shutdown' : `${shellType}_shutdown`,
521
description: `Stop a ${shellType} shell.`,
522
parameters: {
523
type: 'object',
524
properties: {
525
shell_id: { type: 'string', description: 'Shell ID to stop (optional; stops latest shell if omitted)' },
526
},
527
},
528
overridesBuiltInTool: true,
529
skipPermission: true,
530
handler: (args) => {
531
if (args.shell_id) {
532
const success = shellManager.shutdownShell(args.shell_id);
533
return success
534
? makeSuccessResult('Shell stopped.')
535
: makeFailureResult('Shell not found.', 'not_found');
536
}
537
const shells = shellManager.listShells();
538
const shell = shells[shells.length - 1];
539
if (!shell) {
540
return makeFailureResult('No active shell to stop.', 'no_shell');
541
}
542
shellManager.shutdownShell(shell.id);
543
return makeSuccessResult('Shell stopped.');
544
},
545
};
546
547
const listTool: Tool<Record<string, never>> = {
548
name: `list_${shellType}`,
549
description: `List active ${shellType} shell instances.`,
550
parameters: { type: 'object', properties: {} },
551
overridesBuiltInTool: true,
552
skipPermission: true,
553
handler: () => {
554
const shells = shellManager.listShells();
555
if (shells.length === 0) {
556
return makeSuccessResult('No active shells.');
557
}
558
const descriptions = shells.map(s => {
559
const exitCode = terminalManager.getExitCode(s.terminalUri);
560
const status = exitCode !== undefined ? `exited (${exitCode})` : 'running';
561
return `- ${s.id}: ${s.shellType} [${status}]`;
562
});
563
return makeSuccessResult(descriptions.join('\n'));
564
},
565
};
566
567
return [primaryTool, readTool, writeTool, shutdownTool, listTool];
568
}
569
interface ITerminalSandboxResolvedNetworkDomains {
570
allowedDomains: string[];
571
deniedDomains: string[];
572
}
573
574
function isWindowsPowerShell(envShell: string): boolean {
575
return envShell.endsWith('System32\\WindowsPowerShell\\v1.0\\powershell.exe');
576
}
577
578
function createPowerShellModelDescription(shellType: string, shellPath: string, isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
579
const isWinPwsh = isWindowsPowerShell(shellPath);
580
const parts = [
581
`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.`,
582
'',
583
'Command Execution:',
584
// IMPORTANT: PowerShell 5 does not support `&&` so always re-write them to `;`. Note that
585
// the behavior of `&&` differs a little from `;` but in general it's fine
586
isWinPwsh ? '- Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly' : '- Prefer ; when chaining commands on one line',
587
'- Prefer pipelines | for object-based data flow',
588
'- Never create a sub-shell (eg. powershell -c "command") unless explicitly asked',
589
'',
590
'Directory Management:',
591
'- Prefer relative paths when navigating directories, only use absolute when the path is far away or the current cwd is not expected',
592
'- By default (mode=sync), shell and cwd are reused by subsequent sync commands',
593
'- Use $PWD or Get-Location for current directory',
594
'- Use Push-Location/Pop-Location for directory stack',
595
'',
596
'Program Execution:',
597
'- Supports .NET, Python, Node.js, and other executables',
598
'- Install modules via Install-Module, Install-Package',
599
'- Use Get-Command to verify cmdlet/function availability',
600
'',
601
'Async Mode:',
602
'- For long-running tasks (e.g., servers), use mode=async',
603
'- Returns a terminal ID for checking status and runtime later',
604
'- Use Start-Job for background PowerShell jobs',
605
'',
606
`Use write_${shellType} to send commands or input to a terminal session.`,
607
];
608
609
if (isSandboxEnabled) {
610
parts.push(...createSandboxLines(networkDomains));
611
}
612
613
parts.push(
614
'',
615
'Output Management:',
616
'- Output is automatically truncated if longer than 60KB to prevent context overflow',
617
'- Use Select-Object, Where-Object, Format-Table to filter output',
618
'- Use -First/-Last parameters to limit results',
619
'- For pager commands, add | Out-String or | Format-List',
620
'',
621
'Best Practices:',
622
'- Use proper cmdlet names instead of aliases in scripts',
623
'- Quote paths with spaces: "C:\\Path With Spaces"',
624
'- Prefer PowerShell cmdlets over external commands when available',
625
'- Prefer idiomatic PowerShell like Get-ChildItem instead of dir or ls for file listings',
626
'- Use Test-Path to check file/directory existence',
627
'- Be specific with Select-Object properties to avoid excessive output',
628
'- Avoid printing credentials unless absolutely required',
629
'',
630
'Interactive Input Handling:',
631
'- 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.',
632
`- Send exactly one answer per prompt using write_${shellType}. Never send multiple answers in a single send.`,
633
`- After each send, call read_${shellType} to read the next prompt before sending the next answer.`,
634
'- Continue one prompt at a time until the command finishes.',
635
);
636
637
return parts.join('\n');
638
}
639
640
function createSandboxLines(networkDomains?: ITerminalSandboxResolvedNetworkDomains): string[] {
641
const lines = [
642
'',
643
'Sandboxing:',
644
'- ATTENTION: Terminal sandboxing is enabled, commands run in a sandbox by default',
645
'- 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',
646
'- Tools and scripts should respect the TMPDIR environment variable, which is automatically set to an appropriate path within the sandbox',
647
'- 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',
648
'- 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',
649
'- 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.',
650
'- When setting requestUnsandboxedExecution=true, also provide requestUnsandboxedExecutionReason explaining why the command needs unsandboxed access',
651
];
652
if (networkDomains) {
653
const deniedSet = new Set(networkDomains.deniedDomains);
654
const effectiveAllowed = networkDomains.allowedDomains.filter(d => !deniedSet.has(d));
655
if (effectiveAllowed.length === 0) {
656
lines.push('- All network access is blocked in the sandbox');
657
} else {
658
lines.push(`- Only the following domains are accessible in the sandbox (all other network access is blocked): ${effectiveAllowed.join(', ')}`);
659
}
660
if (networkDomains.deniedDomains.length > 0) {
661
lines.push(`- The following domains are explicitly blocked in the sandbox: ${networkDomains.deniedDomains.join(', ')}`);
662
}
663
}
664
return lines;
665
}
666
667
function createGenericDescription(shellType: string, isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
668
const parts = [`
669
Command Execution:
670
- Use && to chain simple commands on one line
671
- Prefer pipelines | over temporary files for data flow
672
- Never create a sub-shell (eg. bash -c "command") unless explicitly asked
673
674
Directory Management:
675
- Prefer relative paths when navigating directories, only use absolute when the path is far away or the current cwd is not expected
676
- By default (mode=sync), shell and cwd are reused by subsequent sync commands
677
- Use $PWD for current directory references
678
- Consider using pushd/popd for directory stack management
679
- Supports directory shortcuts like ~ and -
680
681
Program Execution:
682
- Supports Python, Node.js, and other executables
683
- Install packages via package managers (brew, apt, etc.)
684
- Use which or command -v to verify command availability
685
686
Async Mode:
687
- For long-running tasks (e.g., servers), use mode=async
688
- Returns a terminal ID for checking status and runtime later
689
690
Use write_${shellType} to send commands or input to a terminal session.`];
691
692
if (isSandboxEnabled) {
693
parts.push(createSandboxLines(networkDomains).join('\n'));
694
}
695
696
parts.push(`
697
698
Output Management:
699
- Output is automatically truncated if longer than 60KB to prevent context overflow
700
- Use head, tail, grep, awk to filter and limit output size
701
- For pager commands, disable paging: git --no-pager or add | cat
702
- Use wc -l to count lines before displaying large outputs
703
704
Best Practices:
705
- Quote variables: "$var" instead of $var to handle spaces
706
- Use find with -exec or xargs for file operations
707
- Be specific with commands to avoid excessive output
708
- Avoid printing credentials unless absolutely required
709
- 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.
710
711
Interactive Input Handling:
712
- 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.
713
- Send exactly one answer per prompt using write_${shellType}. Never send multiple answers in a single send.
714
- After each send, call read_${shellType} to read the next prompt before sending the next answer.
715
- Continue one prompt at a time until the command finishes.`);
716
717
return parts.join('');
718
}
719
720
function createBashModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
721
return [
722
'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.',
723
createGenericDescription('bash', isSandboxEnabled, networkDomains),
724
'- Use [[ ]] for conditional tests instead of [ ]',
725
'- Prefer $() over backticks for command substitution',
726
'- Use set -e at start of complex commands to exit on errors'
727
].join('\n');
728
}
729
730