Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,16 @@ import { clamp } from '../../../../../../base/common/numbers.js';
import { IOutputAnalyzer } from './outputAnalyzer.js';
import { SandboxOutputAnalyzer } from './sandboxOutputAnalyzer.js';
import { IAgentSessionsService } from '../../../../chat/browser/agentSessions/agentSessionsService.js';
import { ITerminalSandboxService } from '../../common/terminalSandboxService.js';
import { ITerminalSandboxService, type ITerminalSandboxResolvedNetworkDomains } from '../../common/terminalSandboxService.js';

// #region Tool data

const TOOL_REFERENCE_NAME = 'runInTerminal';
const LEGACY_TOOL_REFERENCE_FULL_NAMES = ['runCommands/runInTerminal'];

function createPowerShellModelDescription(shell: string): string {
function createPowerShellModelDescription(shell: string, isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
const isWinPwsh = isWindowsPowerShell(shell);
return [
const parts = [
`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.`,
'',
'Command Execution:',
Expand All @@ -106,6 +106,13 @@ function createPowerShellModelDescription(shell: string): string {
'- For long-running tasks (e.g., servers), set isBackground=true',
'- Returns a terminal ID for checking status and runtime later',
'- Use Start-Job for background PowerShell jobs',
];

if (isSandboxEnabled) {
parts.push(...createSandboxLines(networkDomains));
}

parts.push(
'',
'Output Management:',
'- Output is automatically truncated if longer than 60KB to prevent context overflow',
Expand All @@ -121,10 +128,38 @@ function createPowerShellModelDescription(shell: string): string {
'- Use Test-Path to check file/directory existence',
'- Be specific with Select-Object properties to avoid excessive output',
'- Avoid printing credentials unless absolutely required',
].join('\n');
);

return parts.join('\n');
}

function createGenericDescription(isSandboxEnabled: boolean): string {
function createSandboxLines(networkDomains?: ITerminalSandboxResolvedNetworkDomains): string[] {
const lines = [
'',
'Sandboxing:',
'- ATTENTION: Terminal sandboxing is enabled, commands run in a sandbox by default',
'- 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',
'- Tools and scripts should respect the TMPDIR environment variable, which is automatically set to an appropriate path within the sandbox',
'- 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',
'- 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',
'- When setting requestUnsandboxedExecution=true, also provide requestUnsandboxedExecutionReason explaining why the command needs unsandboxed access',
];
if (networkDomains) {
const deniedSet = new Set(networkDomains.deniedDomains);
const effectiveAllowed = networkDomains.allowedDomains.filter(d => !deniedSet.has(d));
if (effectiveAllowed.length === 0) {
lines.push('- All network access is blocked in the sandbox');
} else {
lines.push(`- Only the following domains are accessible in the sandbox (all other network access is blocked): ${effectiveAllowed.join(', ')}`);
}
if (networkDomains.deniedDomains.length > 0) {
lines.push(`- The following domains are explicitly blocked in the sandbox: ${networkDomains.deniedDomains.join(', ')}`);
}
}
return lines;
}

function createGenericDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
const parts = [`
Command Execution:
- Use && to chain simple commands on one line
Expand All @@ -148,14 +183,7 @@ Background Processes:
- Returns a terminal ID for checking status and runtime later`];

if (isSandboxEnabled) {
parts.push(`
Sandboxing:
- ATTENTION: Terminal sandboxing is enabled, commands run in a sandbox by default
- 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
- Tools and scripts should respect the TMPDIR environment variable, which is automatically set to an appropriate path within the sandbox
- When a command fails due to sandbox restrictions, immediately re-run it with requestUnsandboxedExecution=true and prompt the user to bypass the sandbox
- 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
- When setting requestUnsandboxedExecution=true, also provide requestUnsandboxedExecutionReason; the user will be prompted before it runs unsandboxed`);
parts.push(createSandboxLines(networkDomains).join('\n'));
}

parts.push(`
Expand All @@ -175,20 +203,20 @@ Best Practices:
return parts.join('');
}

function createBashModelDescription(isSandboxEnabled: boolean): string {
function createBashModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
return [
'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.',
createGenericDescription(isSandboxEnabled),
createGenericDescription(isSandboxEnabled, networkDomains),
'- Use [[ ]] for conditional tests instead of [ ]',
'- Prefer $() over backticks for command substitution',
'- Use set -e at start of complex commands to exit on errors'
].join('\n');
}

function createZshModelDescription(isSandboxEnabled: boolean): string {
function createZshModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
return [
'This tool allows you to execute shell commands in a persistent zsh terminal session, preserving environment variables, working directory, and other context across multiple commands.',
createGenericDescription(isSandboxEnabled),
createGenericDescription(isSandboxEnabled, networkDomains),
'- Use type to check command type (builtin, function, alias)',
'- Use jobs, fg, bg for job control',
'- Use [[ ]] for conditional tests instead of [ ]',
Expand All @@ -198,10 +226,10 @@ function createZshModelDescription(isSandboxEnabled: boolean): string {
].join('\n');
}

function createFishModelDescription(isSandboxEnabled: boolean): string {
function createFishModelDescription(isSandboxEnabled: boolean, networkDomains?: ITerminalSandboxResolvedNetworkDomains): string {
return [
'This tool allows you to execute shell commands in a persistent fish terminal session, preserving environment variables, working directory, and other context across multiple commands.',
createGenericDescription(isSandboxEnabled),
createGenericDescription(isSandboxEnabled, networkDomains),
'- Use type to check command type (builtin, function, alias)',
'- Use jobs, fg, bg for job control',
'- Use test expressions for conditionals (no [[ ]] syntax)',
Expand All @@ -225,15 +253,17 @@ export async function createRunInTerminalToolData(
terminalSandboxService.isEnabled(),
]);

const networkDomains = isSandboxEnabled ? terminalSandboxService.getResolvedNetworkDomains() : undefined;

let modelDescription: string;
if (shell && os && isPowerShell(shell, os)) {
modelDescription = createPowerShellModelDescription(shell);
modelDescription = createPowerShellModelDescription(shell, isSandboxEnabled, networkDomains);
} else if (shell && os && isZsh(shell, os)) {
modelDescription = createZshModelDescription(isSandboxEnabled);
modelDescription = createZshModelDescription(isSandboxEnabled, networkDomains);
} else if (shell && os && isFish(shell, os)) {
modelDescription = createFishModelDescription(isSandboxEnabled);
modelDescription = createFishModelDescription(isSandboxEnabled, networkDomains);
} else {
modelDescription = createBashModelDescription(isSandboxEnabled);
modelDescription = createBashModelDescription(isSandboxEnabled, networkDomains);
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class SandboxOutputAnalyzer extends Disposable implements IOutputAnalyzer
: TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem;
return `Command failed while running in sandboxed mode. If the command failed due to sandboxing:
- If it would be reasonable to extend the sandbox rules, work with the user to update allowWrite for file system access problems in ${fileSystemSetting}, or to add required domains to ${TerminalChatAgentToolsSettingId.TerminalSandboxNetwork}.allowedDomains.
- You can also rerun requestUnsandboxedExecution=true and prompt the user to bypass the sandbox.
- Otherwise, immediately retry the command with requestUnsandboxedExecution=true. Do NOT ask the user — setting this flag automatically shows a confirmation prompt to the user.

Here is the output of the command:\n`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import { ILifecycleService, WillShutdownJoinerOrder } from '../../../../services

export const ITerminalSandboxService = createDecorator<ITerminalSandboxService>('terminalSandboxService');

export interface ITerminalSandboxResolvedNetworkDomains {
allowedDomains: string[];
deniedDomains: string[];
}

export interface ITerminalSandboxService {
readonly _serviceBrand: undefined;
isEnabled(): Promise<boolean>;
Expand All @@ -36,6 +41,7 @@ export interface ITerminalSandboxService {
getSandboxConfigPath(forceRefresh?: boolean): Promise<string | undefined>;
getTempDir(): URI | undefined;
setNeedsForceUpdateConfigFile(): void;
getResolvedNetworkDomains(): ITerminalSandboxResolvedNetworkDomains;
}

export class TerminalSandboxService extends Disposable implements ITerminalSandboxService {
Expand Down Expand Up @@ -264,6 +270,18 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
return undefined;
}

public getResolvedNetworkDomains(): ITerminalSandboxResolvedNetworkDomains {
const networkSetting = this._configurationService.getValue<ITerminalSandboxNetworkSettings>(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) ?? {};
let allowedDomains = networkSetting.allowedDomains ?? [];
if (networkSetting.allowTrustedDomains) {
allowedDomains = this._addTrustedDomainsToAllowedDomains(allowedDomains);
}
return {
allowedDomains,
deniedDomains: networkSetting.deniedDomains ?? []
};
}

private _addTrustedDomainsToAllowedDomains(allowedDomains: string[]): string[] {
const allowedDomainsSet = new Set(allowedDomains);
for (const domain of this._trustedDomainService.trustedDomains) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ suite('RunInTerminalTool', () => {
getTempDir: () => undefined,
setNeedsForceUpdateConfigFile: () => { },
getOS: async () => OperatingSystem.Linux,
getResolvedNetworkDomains: () => ({ allowedDomains: [], deniedDomains: [] }),
};
instantiationService.stub(ITerminalSandboxService, terminalSandboxService);

Expand Down Expand Up @@ -213,6 +214,32 @@ suite('RunInTerminalTool', () => {
ok(toolData.modelDescription?.includes('The /tmp directory is not guaranteed to be accessible or writable and must be avoided'), 'Expected sandboxed tool description to discourage /tmp usage');
});

test('should include allowed and denied network domains in model description', async () => {
sandboxEnabled = true;
terminalSandboxService.getResolvedNetworkDomains = () => ({
allowedDomains: ['github.com', 'npmjs.org'],
deniedDomains: ['evil.com'],
});

const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData);

ok(toolData.modelDescription?.includes('github.com, npmjs.org'), 'Expected allowed domains in description');
ok(toolData.modelDescription?.includes('evil.com'), 'Expected denied domains in description');
});

test('should exclude denied domains from effective allowed list', async () => {
sandboxEnabled = true;
terminalSandboxService.getResolvedNetworkDomains = () => ({
allowedDomains: ['github.com', 'evil.com', 'npmjs.org'],
deniedDomains: ['evil.com'],
});

const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData);

ok(toolData.modelDescription?.includes('github.com, npmjs.org'), 'Expected effective allowed list without denied domain');
ok(!toolData.modelDescription?.includes('accessible in the sandbox (all other network access is blocked): github.com, evil.com'), 'Expected denied domain removed from allowed list');
});

test('should use sandbox labels when command is sandbox wrapped', async () => {
terminalSandboxService.isEnabled = async () => true;
terminalSandboxService.getSandboxConfigPath = async () => '/tmp/vscode-sandbox-settings.json';
Expand Down
Loading