Reverting sandbox manager changes in main. (#302625)

* changes

* fixing merge issue
This commit is contained in:
dileepyavan
2026-03-17 18:51:54 -07:00
committed by GitHub
parent 9724cf9383
commit 8a3bfca4c6
20 changed files with 325 additions and 986 deletions

View File

@@ -1457,7 +1457,6 @@ export default tseslint.config(
'when': 'hasNode',
'allow': [
'@github/copilot-sdk',
'@anthropic-ai/sandbox-runtime',
'@parcel/watcher',
'@vscode/sqlite3',
'@vscode/vscode-languagedetection',

View File

@@ -135,9 +135,6 @@ import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeM
import { IMcpGatewayService, McpGatewayChannelName } from '../../platform/mcp/common/mcpGateway.js';
import { McpGatewayService } from '../../platform/mcp/node/mcpGatewayService.js';
import { McpGatewayChannel } from '../../platform/mcp/node/mcpGatewayChannel.js';
import { SandboxHelperChannelName } from '../../platform/sandbox/common/sandboxHelperIpc.js';
import { ISandboxHelperService } from '../../platform/sandbox/common/sandboxHelperService.js';
import { SandboxHelperService } from '../../platform/sandbox/node/sandboxHelperService.js';
import { IWebContentExtractorService } from '../../platform/webContentExtractor/common/webContentExtractor.js';
import { NativeWebContentExtractorService } from '../../platform/webContentExtractor/electron-main/webContentExtractorService.js';
import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js';
@@ -1163,9 +1160,6 @@ export class CodeApplication extends Disposable {
// Proxy Auth
services.set(IProxyAuthService, new SyncDescriptor(ProxyAuthService));
// Sandbox
services.set(ISandboxHelperService, new SyncDescriptor(SandboxHelperService));
// MCP
services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService));
services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService));
@@ -1298,10 +1292,6 @@ export class CodeApplication extends Disposable {
const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService), disposables);
mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel);
// Sandbox
const sandboxHelperChannel = ProxyChannel.fromService(accessor.get(ISandboxHelperService), disposables);
mainProcessElectronServer.registerChannel(SandboxHelperChannelName, sandboxHelperChannel);
// MCP
const mcpDiscoveryChannel = ProxyChannel.fromService(accessor.get(INativeMcpDiscoveryHelperService), disposables);
mainProcessElectronServer.registerChannel(NativeMcpDiscoveryHelperChannelName, mcpDiscoveryChannel);

View File

@@ -1,13 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const sandboxFilesystemErrorPattern = /(?:\b(?:EACCES|EPERM|ENOENT|EROFS|ENOTDIR|EISDIR|ELOOP|ENAMETOOLONG|EXDEV|ENODEV|ENOEXEC|EBUSY|ETXTBSY|EINVAL|ENOSYS)\b|permission denied|operation not permitted|not accessible|cannot access|failed to open|no such file|read[- ]only|deny file-|restricted|fatal:)/i;
/**
* Returns whether a sandboxed process output line looks like a filesystem access failure.
*/
export function isSandboxFilesystemError(value: string): boolean {
return sandboxFilesystemErrorPattern.test(value);
}

View File

@@ -1,45 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const SandboxHelperChannelName = 'SandboxHelper';
export interface ISandboxNetworkHostPattern {
readonly host: string;
readonly port: number | undefined;
}
export interface ISandboxNetworkConfig {
readonly allowedDomains?: string[];
readonly deniedDomains?: string[];
readonly allowUnixSockets?: string[];
readonly allowAllUnixSockets?: boolean;
readonly allowLocalBinding?: boolean;
readonly httpProxyPort?: number;
readonly socksProxyPort?: number;
}
export interface ISandboxFilesystemConfig {
readonly denyRead?: string[];
readonly allowWrite?: string[];
readonly denyWrite?: string[];
readonly allowGitConfig?: boolean;
}
export interface ISandboxRuntimeConfig {
readonly network?: ISandboxNetworkConfig;
readonly filesystem?: ISandboxFilesystemConfig;
readonly ignoreViolations?: Record<string, string[]>;
readonly enableWeakerNestedSandbox?: boolean;
readonly ripgrep?: {
readonly command: string;
readonly args?: string[];
};
readonly mandatoryDenySearchDepth?: number;
readonly allowPty?: boolean;
}
export interface ISandboxPermissionRequest extends ISandboxNetworkHostPattern {
readonly requestId: string;
}

View File

@@ -1,19 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../base/common/event.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { type ISandboxPermissionRequest, type ISandboxRuntimeConfig } from './sandboxHelperIpc.js';
export const ISandboxHelperService = createDecorator<ISandboxHelperService>('ISandboxHelperService');
export interface ISandboxHelperService {
readonly _serviceBrand: undefined;
readonly onDidRequestSandboxPermission: Event<ISandboxPermissionRequest>;
resetSandbox(): Promise<void>;
resolveSandboxPermissionRequest(requestId: string, allowed: boolean): Promise<void>;
wrapWithSandbox(runtimeConfig: ISandboxRuntimeConfig, command: string): Promise<string>;
}

View File

@@ -1,10 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerMainProcessRemoteService } from '../../ipc/electron-browser/services.js';
import { SandboxHelperChannelName } from '../common/sandboxHelperIpc.js';
import { ISandboxHelperService } from '../common/sandboxHelperService.js';
registerMainProcessRemoteService(ISandboxHelperService, SandboxHelperChannelName);

View File

@@ -1,44 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../base/common/event.js';
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { RemoteAgentConnectionContext } from '../../remote/common/remoteAgentEnvironment.js';
import { type ISandboxRuntimeConfig } from '../common/sandboxHelperIpc.js';
import { ISandboxHelperService } from '../common/sandboxHelperService.js';
export class SandboxHelperChannel implements IServerChannel<RemoteAgentConnectionContext> {
constructor(
@ISandboxHelperService private readonly sandboxHelperService: ISandboxHelperService
) { }
listen<T>(context: RemoteAgentConnectionContext, event: string): Event<T> {
switch (event) {
case 'onDidRequestSandboxPermission': {
return this.sandboxHelperService.onDidRequestSandboxPermission as Event<T>;
}
}
throw new Error('Invalid listen');
}
async call<T>(context: RemoteAgentConnectionContext, command: string, args?: unknown): Promise<T> {
const argsArray = Array.isArray(args) ? args : [];
switch (command) {
case 'resetSandbox': {
return this.sandboxHelperService.resetSandbox() as T;
}
case 'resolveSandboxPermissionRequest': {
return this.sandboxHelperService.resolveSandboxPermissionRequest(argsArray[0] as string, argsArray[1] as boolean) as T;
}
case 'wrapWithSandbox': {
return this.sandboxHelperService.wrapWithSandbox(argsArray[0] as ISandboxRuntimeConfig, argsArray[1] as string) as T;
}
}
throw new Error('Invalid call');
}
}

View File

@@ -1,131 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SandboxManager, type NetworkHostPattern } from '@anthropic-ai/sandbox-runtime';
import { Emitter } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { dirname, posix, win32 } from '../../../base/common/path.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IEnvironmentService, INativeEnvironmentService } from '../../environment/common/environment.js';
import { ILogService } from '../../log/common/log.js';
import { type ISandboxPermissionRequest, type ISandboxRuntimeConfig } from '../common/sandboxHelperIpc.js';
import { ISandboxHelperService } from '../common/sandboxHelperService.js';
export class SandboxHelperService extends Disposable implements ISandboxHelperService {
declare readonly _serviceBrand: undefined;
private readonly _onDidRequestSandboxPermission = this._register(new Emitter<ISandboxPermissionRequest>());
readonly onDidRequestSandboxPermission = this._onDidRequestSandboxPermission.event;
private readonly _pendingPermissionRequests = new Map<string, (allowed: boolean) => void>();
private readonly _rgPath: string | undefined;
private readonly _tempDir: string | undefined;
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@ILogService logService: ILogService,
) {
super();
const nativeEnvironmentService = environmentService as IEnvironmentService & Partial<INativeEnvironmentService>;
this._rgPath = nativeEnvironmentService.appRoot
? this._pathJoin(nativeEnvironmentService.appRoot, 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg')
: undefined;
this._tempDir = nativeEnvironmentService.tmpDir?.path;
logService.debug('SandboxHelperService#constructor ripgrep path configured', !!this._rgPath);
}
async resolveSandboxPermissionRequest(requestId: string, allowed: boolean): Promise<void> {
const resolver = this._pendingPermissionRequests.get(requestId);
if (!resolver) {
throw new Error(`No pending sandbox permission request with id ${requestId}`);
}
this._pendingPermissionRequests.delete(requestId);
resolver(allowed);
}
async resetSandbox(): Promise<void> {
await SandboxManager.reset();
}
async wrapWithSandbox(runtimeConfig: ISandboxRuntimeConfig, command: string): Promise<string> {
const normalizedRuntimeConfig = {
network: {
// adding at least one domain or else the sandbox doesnt do any proxy setup.
allowedDomains: runtimeConfig.network?.allowedDomains?.length ? [...runtimeConfig.network.allowedDomains] : ['microsoft.com'],
deniedDomains: [...(runtimeConfig.network?.deniedDomains ?? [])],
allowUnixSockets: runtimeConfig.network?.allowUnixSockets ? [...runtimeConfig.network.allowUnixSockets] : undefined,
allowAllUnixSockets: runtimeConfig.network?.allowAllUnixSockets,
allowLocalBinding: runtimeConfig.network?.allowLocalBinding,
httpProxyPort: runtimeConfig.network?.httpProxyPort,
socksProxyPort: runtimeConfig.network?.socksProxyPort,
},
filesystem: {
denyRead: [...(runtimeConfig.filesystem?.denyRead ?? [])],
allowWrite: [
...(runtimeConfig.filesystem?.allowWrite ?? []),
...(this._tempDir ? [this._tempDir] : []),
],
denyWrite: [...(runtimeConfig.filesystem?.denyWrite ?? [])],
allowGitConfig: runtimeConfig.filesystem?.allowGitConfig,
},
ignoreViolations: runtimeConfig.ignoreViolations,
enableWeakerNestedSandbox: runtimeConfig.enableWeakerNestedSandbox,
ripgrep: runtimeConfig.ripgrep ? {
command: runtimeConfig.ripgrep.command,
args: runtimeConfig.ripgrep?.args ? [...runtimeConfig.ripgrep.args] : undefined,
} : undefined,
mandatoryDenySearchDepth: runtimeConfig.mandatoryDenySearchDepth,
allowPty: runtimeConfig.allowPty,
};
await SandboxManager.initialize(normalizedRuntimeConfig, request => this._requestSandboxPermission(request));
return SandboxManager.wrapWithSandbox(`${this._getSandboxEnvironmentPrefix()} ${command}`);
}
private _getSandboxEnvironmentPrefix(): string {
const env: string[] = ['NODE_USE_ENV_PROXY=1'];
if (this._tempDir) {
env.push(this._toEnvironmentAssignment('TMPDIR', this._tempDir));
}
const pathWithRipgrep = this._getPathWithRipgrepDir();
if (pathWithRipgrep) {
env.push(this._toEnvironmentAssignment('PATH', pathWithRipgrep));
}
return env.join(' ');
}
private _getPathWithRipgrepDir(): string | undefined {
if (!this._rgPath) {
return undefined;
}
const rgDir = dirname(this._rgPath);
const currentPath = process.env['PATH'];
const path = process.platform === 'win32' ? win32 : posix;
return currentPath ? `${currentPath}${path.delimiter}${rgDir}` : rgDir;
}
private _toEnvironmentAssignment(name: string, value: string): string {
return `${name}="${value}"`;
}
private _pathJoin(...segments: string[]): string {
const path = process.platform === 'win32' ? win32 : posix;
return path.join(...segments);
}
private _requestSandboxPermission(request: NetworkHostPattern): Promise<boolean> {
const requestId = generateUuid();
return new Promise<boolean>(resolve => {
this._pendingPermissionRequests.set(requestId, resolve);
this._onDidRequestSandboxPermission.fire({
requestId,
host: request.host,
port: request.port,
});
});
}
}

View File

@@ -89,10 +89,6 @@ import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeM
import { IMcpGatewayService, McpGatewayChannelName } from '../../platform/mcp/common/mcpGateway.js';
import { McpGatewayService } from '../../platform/mcp/node/mcpGatewayService.js';
import { McpGatewayChannel } from '../../platform/mcp/node/mcpGatewayChannel.js';
import { SandboxHelperChannelName } from '../../platform/sandbox/common/sandboxHelperIpc.js';
import { ISandboxHelperService } from '../../platform/sandbox/common/sandboxHelperService.js';
import { SandboxHelperChannel } from '../../platform/sandbox/node/sandboxHelperChannel.js';
import { SandboxHelperService } from '../../platform/sandbox/node/sandboxHelperService.js';
import { IExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { ExtensionGalleryManifestIPCService } from '../../platform/extensionManagement/common/extensionGalleryManifestServiceIpc.js';
import { IAllowedMcpServersService, IMcpGalleryService, IMcpManagementService } from '../../platform/mcp/common/mcpManagement.js';
@@ -218,7 +214,6 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService));
services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService));
services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
services.set(ISandboxHelperService, new SyncDescriptor(SandboxHelperService));
services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService));
services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService));
@@ -275,7 +270,6 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI, productService.extensionsForceVersionByQuality ?? [], logService), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService, extensionManagementService);
socketServer.registerChannel(RemoteExtensionsScannerChannelName, new RemoteExtensionsScannerChannel(remoteExtensionsScanner, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)));
socketServer.registerChannel(SandboxHelperChannelName, instantiationService.createInstance(SandboxHelperChannel));
socketServer.registerChannel(NativeMcpDiscoveryHelperChannelName, instantiationService.createInstance(NativeMcpDiscoveryHelperChannel, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)));
socketServer.registerChannel(McpGatewayChannelName, instantiationService.createInstance(McpGatewayChannel<RemoteAgentConnectionContext>, socketServer));

View File

@@ -11,7 +11,6 @@ import { autorun, IObservable, observableValue } from '../../../../base/common/o
import { localize } from '../../../../nls.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogger, log, LogLevel } from '../../../../platform/log/common/log.js';
import { isSandboxFilesystemError } from '../../../../platform/sandbox/common/sandboxErrorAnalysis.js';
import { IMcpHostDelegate, IMcpMessageTransport } from './mcpRegistryTypes.js';
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
import { McpTaskManager } from './mcpTaskManager.js';
@@ -163,7 +162,7 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect
};
}
if (isSandboxFilesystemError(message)) {
if (/(?:\b(?:EACCES|EPERM|ENOENT|EROFS|fail(?:ed|ure)?)\b|not accessible|read[- ]only)/i.test(message)) {
return {
kind: 'filesystem',
message,

View File

@@ -6,7 +6,6 @@
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { registerMainProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import '../../../../platform/sandbox/electron-browser/sandboxHelperService.js';
import { ILocalPtyService, TerminalIpcChannels } from '../../../../platform/terminal/common/terminal.js';
import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from '../../../common/contributions.js';
import { ITerminalProfileResolverService } from '../common/terminal.js';

View File

@@ -19,11 +19,14 @@ export class CommandLineSandboxRewriter extends Disposable implements ICommandLi
return undefined;
}
const wrappedCommand = await this._sandboxService.wrapCommand(options.commandLine);
if (wrappedCommand === options.commandLine) {
// If the sandbox service returns the same command, it means it didn't actually wrap it for some reason. In that case, we should return undefined to allow other rewriters to run, instead of returning a result that claims the command was rewritten but doesn't actually change anything.
// Ensure sandbox config is initialized before wrapping
const sandboxConfigPath = await this._sandboxService.getSandboxConfigPath();
if (!sandboxConfigPath) {
// If no sandbox config is available, run without sandboxing
return undefined;
}
const wrappedCommand = this._sandboxService.wrapCommand(options.commandLine);
return {
rewritten: wrappedCommand,
reasoning: 'Wrapped command for sandbox execution',

View File

@@ -3,18 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { posix } from '../../../../../../base/common/path.js';
import { URI } from '../../../../../../base/common/uri.js';
import { localize } from '../../../../../../nls.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { IFileService } from '../../../../../../platform/files/common/files.js';
import { isSandboxFilesystemError } from '../../../../../../platform/sandbox/common/sandboxErrorAnalysis.js';
import { ITerminalSandboxService } from '../../common/terminalSandboxService.js';
import type { IOutputAnalyzer, IOutputAnalyzerOptions } from './outputAnalyzer.js';
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';
export class SandboxOutputAnalyzer extends Disposable implements IOutputAnalyzer {
constructor(
@IFileService private readonly _fileService: IFileService,
@ITerminalSandboxService private readonly _sandboxService: ITerminalSandboxService,
) {
super();
@@ -28,60 +24,12 @@ export class SandboxOutputAnalyzer extends Disposable implements IOutputAnalyzer
return undefined;
}
const filesystemPermissionLine = options.exitResult.split(/\r?\n/).find(line => isSandboxFilesystemError(line));
const sandboxPath = filesystemPermissionLine ? await this._getSandboxPathToAllow(filesystemPermissionLine) : undefined;
if (sandboxPath && await this._sandboxService.promptToAllowWritePath(sandboxPath)) {
return localize(
'runInTerminalTool.sandboxWritePathAllowed',
"Write access to {0} was added to the terminal sandbox allow list. Retry the command.",
sandboxPath
);
}
return localize(
'runInTerminalTool.sandboxCommandFailed',
"Command failed while running in sandboxed mode. Do not switch to other tools. Retry the command in sandboxed mode at most once, and only if the result suggests the failure was transient."
"Command failed while running in sandboxed mode. Use the command result to determine the scenario. If the issue is filesystem permissions, update allowWrite in {0} (Linux) or {1} (macOS). If the issue is domain/network related, add the required domains to {2}.allowedDomains.",
TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem,
TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem,
TerminalChatAgentToolsSettingId.TerminalSandboxNetwork
);
}
private _extractSandboxPath(line: string): string | undefined {
// Matches paths wrapped in square brackets, e.g. "[/tmp/file.txt]".
const bracketedPath = line.match(/\[(\/[^\]\r\n]+)\]/);
if (bracketedPath?.[1]) {
return bracketedPath[1].trim();
}
// Matches quoted absolute paths, e.g. "'/tmp/file.txt'" or '"/tmp/file.txt"'.
const quotedPath = line.match(/["'`](\/.+?)["'`]/);
if (quotedPath?.[1]) {
return quotedPath[1];
}
// Matches unquoted absolute paths followed by ": " message text, plain whitespace text,
// or end of line, e.g. "/home/user/file.txt: Warning..." or "/home/user/file.txt Warning...".
const inlinePath = line.match(/(\/[^\s:\r\n]+)(?=:\s|\s|$)/);
if (inlinePath?.[1]) {
return inlinePath[1].trim();
}
// Matches a trailing absolute path at the end of the line, e.g. "... open /tmp/file.txt".
const trailingPath = line.match(/(\/[\w.\-~/ ]+)$/);
return trailingPath?.[1]?.trim();
}
private async _getSandboxPathToAllow(line: string): Promise<string | undefined> {
const extractedPath = this._extractSandboxPath(line);
if (!extractedPath) {
return undefined;
}
if (await this._fileService.exists(URI.file(extractedPath))) {
return extractedPath;
}
const parentPath = posix.dirname(extractedPath);
return parentPath && parentPath !== extractedPath ? parentPath : extractedPath;
}
}

View File

@@ -542,13 +542,13 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
allowTrustedDomains: {
type: 'boolean',
description: localize('terminalSandbox.networkSetting.allowTrustedDomains', "When enabled, the Trusted Domains list is included in the allowed domains for network access."),
default: true
default: false
}
},
default: {
allowedDomains: [],
deniedDomains: [],
allowTrustedDomains: true
allowTrustedDomains: false
},
tags: ['preview'],
restricted: true,

View File

@@ -3,68 +3,83 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { Event } from '../../../../../base/common/event.js';
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { FileAccess } from '../../../../../base/common/network.js';
import { dirname, posix, win32 } from '../../../../../base/common/path.js';
import { OperatingSystem, OS } from '../../../../../base/common/platform.js';
import { ConfigurationTarget, IConfigurationChangeEvent, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { URI } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { ProxyChannel } from '../../../../../base/parts/ipc/common/ipc.js';
import { SandboxHelperChannelName, type ISandboxPermissionRequest, type ISandboxRuntimeConfig } from '../../../../../platform/sandbox/common/sandboxHelperIpc.js';
import { ISandboxHelperService } from '../../../../../platform/sandbox/common/sandboxHelperService.js';
import { ITerminalSandboxNetworkSettings } from './terminalSandbox.js';
import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js';
import { TerminalChatAgentToolsSettingId } from './terminalChatAgentToolsConfiguration.js';
import { IRemoteAgentEnvironment } from '../../../../../platform/remote/common/remoteAgentEnvironment.js';
import { ITrustedDomainService } from '../../../url/common/trustedDomainService.js';
import { localize } from '../../../../../nls.js';
type ISandboxHelperChannel = {
readonly onDidRequestSandboxPermission: Event<ISandboxPermissionRequest>;
resetSandbox(): Promise<void>;
resolveSandboxPermissionRequest(requestId: string, allowed: boolean): Promise<void>;
wrapWithSandbox(runtimeConfig: ISandboxRuntimeConfig, command: string): Promise<string>;
};
export const ITerminalSandboxService = createDecorator<ITerminalSandboxService>('terminalSandboxService');
export interface ITerminalSandboxService {
readonly _serviceBrand: undefined;
isEnabled(): Promise<boolean>;
promptToAllowWritePath(path: string): Promise<boolean>;
wrapWithSandbox(runtimeConfig: ISandboxRuntimeConfig, command: string): Promise<string>;
wrapCommand(command: string): Promise<string>;
wrapCommand(command: string): string;
getSandboxConfigPath(forceRefresh?: boolean): Promise<string | undefined>;
getTempDir(): URI | undefined;
setNeedsForceUpdateConfigFile(): void;
}
type ITerminalSandboxFilesystemSettings = {
denyRead?: string[];
allowWrite?: string[];
denyWrite?: string[];
};
export class TerminalSandboxService extends Disposable implements ITerminalSandboxService {
readonly _serviceBrand: undefined;
private _srtPath: string | undefined;
private _rgPath: string | undefined;
private _srtPathResolved = false;
private _execPath?: string;
private _sandboxConfigPath: string | undefined;
private _needsForceUpdateConfigFile = true;
private _tempDir: URI | undefined;
private _sandboxSettingsId: string | undefined;
private _remoteEnvDetailsPromise: Promise<IRemoteAgentEnvironment | null>;
private _remoteEnvDetails: IRemoteAgentEnvironment | null = null;
private _appRoot: string;
private _os: OperatingSystem = OS;
private _defaultWritePaths: string[] = ['~/.npm'];
private readonly _sandboxPermissionRequestListener = this._register(new MutableDisposable());
private _sandboxHelperSource: string | undefined;
constructor(
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IDialogService private readonly _dialogService: IDialogService,
@IFileService private readonly _fileService: IFileService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@ILogService private readonly _logService: ILogService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
@ISandboxHelperService private readonly _localSandboxHelperService: ISandboxHelperService,
@ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService,
) {
super();
this._appRoot = dirname(FileAccess.asFileUri('').path);
// Get the node executable path from native environment service if available (Electron's execPath with ELECTRON_RUN_AS_NODE)
const nativeEnv = this._environmentService as IEnvironmentService & { execPath?: string };
this._execPath = nativeEnv.execPath;
this._sandboxSettingsId = generateUuid();
this._remoteEnvDetailsPromise = this._remoteAgentService.getEnvironment();
this._register(this._configurationService.onDidChangeConfiguration(e => this._handleSandboxConfigurationChange(e)));
this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, (e: IConfigurationChangeEvent | undefined) => {
// If terminal sandbox settings changed, update sandbox config.
if (
e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled) ||
e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) ||
e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) ||
e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem)
) {
this.setNeedsForceUpdateConfigFile();
}
}));
this._register(this._trustedDomainService.onDidChangeTrustedDomains(() => {
this.setNeedsForceUpdateConfigFile();
}));
}
public async isEnabled(): Promise<boolean> {
@@ -76,127 +91,129 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
return this._configurationService.getValue<boolean>(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled);
}
public async wrapWithSandbox(runtimeConfig: ISandboxRuntimeConfig, command: string): Promise<string> {
try {
const service = this._getSandboxHelperService();
return service.wrapWithSandbox(runtimeConfig, command);
} catch (error) {
this._logService.error('TerminalSandboxService: Failed to wrap command with sandbox', error);
return command;
public wrapCommand(command: string): string {
if (!this._sandboxConfigPath || !this._tempDir) {
throw new Error('Sandbox config path or temp dir not initialized');
}
if (!this._execPath) {
throw new Error('Executable path not set to run sandbox commands');
}
if (!this._srtPath) {
throw new Error('Sandbox runtime path not resolved');
}
if (!this._rgPath) {
throw new Error('Ripgrep path not resolved');
}
// Use ELECTRON_RUN_AS_NODE=1 to make Electron executable behave as Node.js
// TMPDIR must be set as environment variable before the command
// Quote shell arguments so the wrapped command cannot break out of the outer shell.
const wrappedCommand = `PATH="$PATH:${dirname(this._rgPath)}" TMPDIR="${this._tempDir.path}" "${this._execPath}" "${this._srtPath}" --settings "${this._sandboxConfigPath}" -c ${this._quoteShellArgument(command)}`;
if (this._remoteEnvDetails) {
return `${wrappedCommand}`;
}
return `ELECTRON_RUN_AS_NODE=1 ${wrappedCommand}`;
}
public async promptToAllowWritePath(path: string): Promise<boolean> {
if (!(await this.isEnabled())) {
return false;
}
const sandboxPath = path.trim();
const settingsKey = this._getFileSystemSettingsKey();
if (!sandboxPath || !settingsKey) {
return false;
}
const target = this._getSandboxConfigurationTarget();
const inspectedValue = this._configurationService.inspect<ITerminalSandboxFilesystemSettings>(settingsKey);
const currentSettings = target === ConfigurationTarget.USER_REMOTE ? inspectedValue.userRemoteValue : inspectedValue.userValue;
const allowWrite = new Set(currentSettings?.allowWrite ?? []);
const denyWrite = currentSettings?.denyWrite ?? [];
if (allowWrite.has(sandboxPath) && !denyWrite.includes(sandboxPath)) {
return false;
}
const { confirmed } = await this._dialogService.confirm({
type: 'warning',
message: localize('terminalSandboxAllowWritePathMessage', "Allow Sandboxed File Write?"),
detail: localize('terminalSandboxAllowWritePathDetail', "The sandboxed terminal command was blocked from writing to {0}. Add this path to {1}.allowWrite?", sandboxPath, settingsKey),
primaryButton: localize('terminalSandboxAllowWritePathPrimary', "&&Allow"),
cancelButton: localize('terminalSandboxAllowWritePathCancel', "&&Deny")
});
if (!confirmed) {
return false;
}
allowWrite.add(sandboxPath);
await this._configurationService.updateValue(settingsKey, {
...currentSettings,
allowWrite: Array.from(allowWrite),
denyWrite: denyWrite.filter(value => value !== sandboxPath),
}, target);
return true;
public getTempDir(): URI | undefined {
return this._tempDir;
}
public async promptForSandboxPermission(request: ISandboxPermissionRequest): Promise<boolean> {
const target = request.port === undefined ? request.host : `${request.host}:${request.port}`;
const { confirmed } = await this._dialogService.confirm({
type: 'warning',
message: localize('terminalSandboxPermissionRequestMessage', "Allow Sandboxed Network Access?"),
detail: localize('terminalSandboxPermissionRequestDetail', "The sandboxed terminal command requested access to {0}.", target),
primaryButton: localize('terminalSandboxPermissionAllow', "&&Allow"),
cancelButton: localize('terminalSandboxPermissionDeny', "&&Deny")
});
return confirmed;
public setNeedsForceUpdateConfigFile(): void {
this._needsForceUpdateConfigFile = true;
}
public async wrapCommand(command: string): Promise<string> {
const sandboxSettings = await this._getSandboxSettings();
if (!sandboxSettings) {
throw new Error('Sandbox settings not initialized');
public async getSandboxConfigPath(forceRefresh: boolean = false): Promise<string | undefined> {
await this._resolveSrtPath();
if (!this._sandboxConfigPath || forceRefresh || this._needsForceUpdateConfigFile) {
this._sandboxConfigPath = await this._createSandboxConfig();
this._needsForceUpdateConfigFile = false;
}
return this.wrapWithSandbox(sandboxSettings, command);
return this._sandboxConfigPath;
}
private async _resetSandbox(): Promise<void> {
const service = this._getSandboxHelperService();
await service.resetSandbox();
private _quoteShellArgument(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
private _handleSandboxConfigurationChange(e: IConfigurationChangeEvent): void {
if (!this._affectsSandboxConfiguration(e)) {
private async _resolveSrtPath(): Promise<void> {
if (this._srtPathResolved) {
return;
}
this._srtPathResolved = true;
const remoteEnv = this._remoteEnvDetails || await this._remoteEnvDetailsPromise;
if (remoteEnv) {
this._resetSandbox().catch(error => {
this._logService.error('TerminalSandboxService: Failed to reset sandbox after configuration change', error);
});
}
private async _getSandboxSettings(): Promise<ISandboxRuntimeConfig | undefined> {
const networkSetting = this._configurationService.getValue<ITerminalSandboxNetworkSettings>(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) ?? {};
const linuxFileSystemSetting = this._os === OperatingSystem.Linux
? this._configurationService.getValue<ITerminalSandboxFilesystemSettings>(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) ?? {}
: {};
const macFileSystemSetting = this._os === OperatingSystem.Macintosh
? this._configurationService.getValue<ITerminalSandboxFilesystemSettings>(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) ?? {}
: {};
const linuxAllowWrite = this._resolveAllowWritePaths(linuxFileSystemSetting.allowWrite);
const macAllowWrite = this._resolveAllowWritePaths(macFileSystemSetting.allowWrite);
let allowedDomains = networkSetting.allowedDomains ?? [];
if (networkSetting.allowTrustedDomains) {
allowedDomains = this._addTrustedDomainsToAllowedDomains(allowedDomains);
this._appRoot = remoteEnv.appRoot.path;
this._execPath = this._pathJoin(this._appRoot, 'node');
}
return {
network: {
allowedDomains,
deniedDomains: networkSetting.deniedDomains ?? []
},
filesystem: {
denyRead: (this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyRead : linuxFileSystemSetting.denyRead) || [],
allowWrite: (this._os === OperatingSystem.Macintosh ? macAllowWrite : linuxAllowWrite) || [],
denyWrite: (this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyWrite : linuxFileSystemSetting.denyWrite) || [],
}
};
this._srtPath = this._pathJoin(this._appRoot, 'node_modules', '@anthropic-ai', 'sandbox-runtime', 'dist', 'cli.js');
this._rgPath = this._pathJoin(this._appRoot, 'node_modules', '@vscode', 'ripgrep', 'bin', 'rg');
}
private _resolveAllowWritePaths(configuredAllowWrite: string[] | undefined): string[] {
const workspaceFolderPaths = this._workspaceContextService.getWorkspace().folders.map(folder => folder.uri.path);
return [...new Set([...workspaceFolderPaths, ...this._defaultWritePaths, ...(configuredAllowWrite ?? [])])];
private async _createSandboxConfig(): Promise<string | undefined> {
if (await this.isEnabled() && !this._tempDir) {
await this._initTempDir();
}
if (this._tempDir) {
const networkSetting = this._configurationService.getValue<ITerminalSandboxNetworkSettings>(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) ?? {};
const linuxFileSystemSetting = this._os === OperatingSystem.Linux
? this._configurationService.getValue<{ denyRead?: string[]; allowWrite?: string[]; denyWrite?: string[] }>(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) ?? {}
: {};
const macFileSystemSetting = this._os === OperatingSystem.Macintosh
? this._configurationService.getValue<{ denyRead?: string[]; allowWrite?: string[]; denyWrite?: string[] }>(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) ?? {}
: {};
const configFileUri = URI.joinPath(this._tempDir, `vscode-sandbox-settings-${this._sandboxSettingsId}.json`);
const defaultAllowWrite = [...this._defaultWritePaths];
const linuxAllowWrite = [...new Set([...(linuxFileSystemSetting.allowWrite ?? []), ...defaultAllowWrite])];
const macAllowWrite = [...new Set([...(macFileSystemSetting.allowWrite ?? []), ...defaultAllowWrite])];
let allowedDomains = networkSetting.allowedDomains ?? [];
if (networkSetting.allowTrustedDomains) {
allowedDomains = this._addTrustedDomainsToAllowedDomains(allowedDomains);
}
const sandboxSettings = {
network: {
allowedDomains,
deniedDomains: networkSetting.deniedDomains ?? []
},
filesystem: {
denyRead: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyRead : linuxFileSystemSetting.denyRead,
allowWrite: this._os === OperatingSystem.Macintosh ? macAllowWrite : linuxAllowWrite,
denyWrite: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyWrite : linuxFileSystemSetting.denyWrite,
}
};
this._sandboxConfigPath = configFileUri.path;
await this._fileService.createFile(configFileUri, VSBuffer.fromString(JSON.stringify(sandboxSettings, null, '\t')), { overwrite: true });
return this._sandboxConfigPath;
}
return undefined;
}
// Joins path segments according to the current OS.
private _pathJoin = (...segments: string[]) => {
const path = this._os === OperatingSystem.Windows ? win32 : posix;
return path.join(...segments);
};
private async _initTempDir(): Promise<void> {
if (await this.isEnabled()) {
this._needsForceUpdateConfigFile = true;
const remoteEnv = this._remoteEnvDetails || await this._remoteEnvDetailsPromise;
if (remoteEnv) {
this._tempDir = remoteEnv.tmpDir;
} else {
const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI };
this._tempDir = environmentService.tmpDir;
}
if (this._tempDir) {
this._defaultWritePaths.push(this._tempDir.path);
}
if (!this._tempDir) {
this._logService.warn('TerminalSandboxService: Cannot create sandbox settings file because no tmpDir is available in this environment');
}
}
}
private _addTrustedDomainsToAllowedDomains(allowedDomains: string[]): string[] {
@@ -213,83 +230,4 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
}
return Array.from(allowedDomainsSet);
}
private _getSandboxConfigurationTarget(): ConfigurationTarget {
return this._remoteAgentService.getConnection() ? ConfigurationTarget.USER_REMOTE : ConfigurationTarget.USER;
}
private _affectsSandboxConfiguration(e: IConfigurationChangeEvent): boolean {
return e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled)
|| e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork)
|| e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem)
|| e.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem);
}
private _getFileSystemSettingsKey(): string | undefined {
if (this._os === OperatingSystem.Linux) {
return TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem;
}
if (this._os === OperatingSystem.Macintosh) {
return TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem;
}
return undefined;
}
private _getSandboxHelperService(): ISandboxHelperChannel {
const connection = this._remoteAgentService.getConnection();
const service = connection
? ProxyChannel.toService<ISandboxHelperChannel>(connection.getChannel(SandboxHelperChannelName))
: this._localSandboxHelperService;
const source = connection ? `remote:${connection.remoteAuthority}` : 'local';
if (this._sandboxHelperSource !== source) {
this._sandboxHelperSource = source;
this._sandboxPermissionRequestListener.value = service.onDidRequestSandboxPermission(request => {
void this._handleSandboxPermissionRequest(service, request);
});
}
return service;
}
private async _handleSandboxPermissionRequest(service: ISandboxHelperChannel, request: ISandboxPermissionRequest): Promise<void> {
let allowed = false;
try {
allowed = await this.promptForSandboxPermission(request);
if (allowed) {
await this._persistAllowedSandboxDomain(request.host);
}
} catch (error) {
this._logService.error('TerminalSandboxService: Failed to prompt for sandbox permission', error);
}
try {
await service.resolveSandboxPermissionRequest(request.requestId, allowed);
} catch (error) {
this._logService.error('TerminalSandboxService: Failed to resolve sandbox permission request', error);
}
}
private async _persistAllowedSandboxDomain(host: string): Promise<void> {
const target = this._getSandboxConfigurationTarget();
const inspectedValue = this._configurationService.inspect<ITerminalSandboxNetworkSettings>(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork);
const currentSettings = target === ConfigurationTarget.USER_REMOTE ? inspectedValue.userRemoteValue : inspectedValue.userValue;
const allowedDomains = new Set(currentSettings?.allowedDomains ?? []);
const deniedDomains = (currentSettings?.deniedDomains ?? []).filter(domain => domain !== host);
if (allowedDomains.has(host) && deniedDomains.length === (currentSettings?.deniedDomains ?? []).length) {
return;
}
allowedDomains.add(host);
await this._configurationService.updateValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, {
...currentSettings,
allowedDomains: Array.from(allowedDomains),
deniedDomains,
}, target);
}
}

View File

@@ -1,142 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ok, strictEqual } from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { IFileService } from '../../../../../../platform/files/common/files.js';
import type { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
import { SandboxOutputAnalyzer } from '../../browser/tools/sandboxOutputAnalyzer.js';
import { ITerminalSandboxService } from '../../common/terminalSandboxService.js';
suite('SandboxOutputAnalyzer', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
let instantiationService: TestInstantiationService;
function createAnalyzer(options: { sandboxService?: Partial<ITerminalSandboxService>; existingPaths?: string[] } = {}) {
instantiationService = workbenchInstantiationService({}, store);
const existingPaths = new Set(options.existingPaths ?? []);
instantiationService.stub(IFileService, {
_serviceBrand: undefined,
exists: async resource => existingPaths.has(resource.path),
});
instantiationService.stub(ITerminalSandboxService, {
_serviceBrand: undefined,
isEnabled: async () => true,
promptToAllowWritePath: async () => false,
wrapWithSandbox: async (_runtimeConfig, command) => command,
wrapCommand: async command => command,
...options.sandboxService,
});
return store.add(instantiationService.createInstance(SandboxOutputAnalyzer));
}
test('should prompt to allow a denied existing write path and ask for retry', async () => {
const requestedPaths: string[] = [];
const analyzer = createAnalyzer({
existingPaths: ['/tmp/blocked.txt'],
sandboxService: {
promptToAllowWritePath: async path => {
requestedPaths.push(path);
return true;
}
}
});
const result = await analyzer.analyze({
exitCode: 1,
// eslint-disable-next-line local/code-no-unexternalized-strings
exitResult: "Error: EPERM: operation not permitted, open '/tmp/blocked.txt'",
commandLine: 'touch /tmp/blocked.txt'
});
strictEqual(requestedPaths[0], '/tmp/blocked.txt');
ok(/Retry the command\./.test(result ?? ''));
ok(/\/tmp\/blocked\.txt/.test(result ?? ''));
});
test('should fall back to the parent path when the extracted path does not exist', async () => {
const requestedPaths: string[] = [];
const analyzer = createAnalyzer({
sandboxService: {
promptToAllowWritePath: async path => {
requestedPaths.push(path);
return true;
}
}
});
const result = await analyzer.analyze({
exitCode: 1,
// eslint-disable-next-line local/code-no-unexternalized-strings
exitResult: "Error: EPERM: operation not permitted, open '/tmp/new-folder/file.txt'",
commandLine: 'mkdir -p /tmp/new-folder && touch /tmp/new-folder/file.txt'
});
strictEqual(requestedPaths[0], '/tmp/new-folder');
ok(/\/tmp\/new-folder/.test(result ?? ''));
});
test('should return generic sandbox guidance when write path approval is declined', async () => {
const analyzer = createAnalyzer({
sandboxService: {
promptToAllowWritePath: async () => false,
}
});
const result = await analyzer.analyze({
exitCode: 1,
// eslint-disable-next-line local/code-no-unexternalized-strings
exitResult: "Error: EPERM: operation not permitted, open '/tmp/blocked.txt'",
commandLine: 'touch /tmp/blocked.txt'
});
ok(/Command failed while running in sandboxed mode\./.test(result ?? ''));
ok(/Do not switch to other tools\./.test(result ?? ''));
ok(/at most once/.test(result ?? ''));
});
test('should extract an inline path followed by warning text', async () => {
const requestedPaths: string[] = [];
const analyzer = createAnalyzer({
sandboxService: {
promptToAllowWritePath: async path => {
requestedPaths.push(path);
return true;
}
}
});
const result = await analyzer.analyze({
exitCode: 1,
exitResult: 'Warning: Failed to create the file /home/testing/openai-api-docs.html: Warning: Read-only file system',
commandLine: 'touch /home/testing/openai-api-docs.html'
});
strictEqual(requestedPaths[0], '/home/testing');
ok(/\/home\/testing/.test(result ?? ''));
});
test('should extract an inline path followed by plain text without a colon', async () => {
const requestedPaths: string[] = [];
const analyzer = createAnalyzer({
sandboxService: {
promptToAllowWritePath: async path => {
requestedPaths.push(path);
return true;
}
}
});
const result = await analyzer.analyze({
exitCode: 1,
exitResult: 'Warning: Failed to create the file /home/testing/openai-api-docs.html Warning Read-only file system',
commandLine: 'touch /home/testing/openai-api-docs.html'
});
strictEqual(requestedPaths[0], '/home/testing');
ok(/\/home\/testing/.test(result ?? ''));
});
});

View File

@@ -8,7 +8,6 @@ import { SandboxedCommandLinePresenter } from '../../browser/tools/commandLinePr
import { OperatingSystem } from '../../../../../../base/common/platform.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import type { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import type { ISandboxRuntimeConfig } from '../../../../../../platform/sandbox/common/sandboxHelperIpc.js';
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
import { ITerminalSandboxService } from '../../common/terminalSandboxService.js';
@@ -16,31 +15,16 @@ suite('SandboxedCommandLinePresenter', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
let instantiationService: TestInstantiationService;
class TestTerminalSandboxService implements ITerminalSandboxService {
readonly _serviceBrand: undefined;
constructor(private readonly _enabled: boolean) { }
async isEnabled(): Promise<boolean> {
return this._enabled;
}
async promptToAllowWritePath(_path: string): Promise<boolean> {
return false;
}
async wrapCommand(command: string): Promise<string> {
return command;
}
async wrapWithSandbox(_runtimeConfig: ISandboxRuntimeConfig, command: string): Promise<string> {
return command;
}
}
const createPresenter = (enabled: boolean = true) => {
instantiationService = workbenchInstantiationService({}, store);
instantiationService.stub(ITerminalSandboxService, new TestTerminalSandboxService(enabled));
instantiationService.stub(ITerminalSandboxService, {
_serviceBrand: undefined,
isEnabled: async () => enabled,
wrapCommand: command => command,
getSandboxConfigPath: async () => '/tmp/sandbox.json',
getTempDir: () => undefined,
setNeedsForceUpdateConfigFile: () => { },
});
return instantiationService.createInstance(SandboxedCommandLinePresenter);
};
@@ -58,17 +42,6 @@ suite('SandboxedCommandLinePresenter', () => {
strictEqual(result.languageDisplayName, undefined);
});
test('should prefer the original command line when provided', async () => {
const presenter = createPresenter();
const result = await presenter.present({
commandLine: { forDisplay: 'wrapped', original: 'echo hello' },
shell: 'bash',
os: OperatingSystem.Linux
});
ok(result);
strictEqual(result.commandLine, 'echo hello');
});
test('should return command line for non-sandboxed command when enabled', async () => {
const presenter = createPresenter();
const commandLine = 'echo hello';

View File

@@ -9,32 +9,18 @@ import { TestInstantiationService } from '../../../../../../platform/instantiati
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
import { TerminalSandboxService } from '../../common/terminalSandboxService.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { IFileService } from '../../../../../../platform/files/common/files.js';
import { IEnvironmentService } from '../../../../../../platform/environment/common/environment.js';
import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js';
import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js';
import { ITrustedDomainService } from '../../../../url/common/trustedDomainService.js';
import { URI } from '../../../../../../base/common/uri.js';
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';
import { Event, Emitter } from '../../../../../../base/common/event.js';
import { mock } from '../../../../../../base/test/common/mock.js';
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
import { VSBuffer } from '../../../../../../base/common/buffer.js';
import { OperatingSystem } from '../../../../../../base/common/platform.js';
import { IRemoteAgentEnvironment } from '../../../../../../platform/remote/common/remoteAgentEnvironment.js';
import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js';
import { ISandboxPermissionRequest, ISandboxRuntimeConfig } from '../../../../../../platform/sandbox/common/sandboxHelperIpc.js';
import { ISandboxHelperService } from '../../../../../../platform/sandbox/common/sandboxHelperService.js';
import { IWorkspaceContextService, IWorkspaceFolder, toWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js';
type CapturedSandboxRuntimeConfig = ISandboxRuntimeConfig & {
network: {
allowedDomains: string[];
deniedDomains: string[];
};
filesystem: {
denyRead: string[];
allowWrite: string[];
denyWrite: string[];
};
};
suite('TerminalSandboxService - allowTrustedDomains', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
@@ -42,8 +28,8 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
let instantiationService: TestInstantiationService;
let configurationService: TestConfigurationService;
let trustedDomainService: MockTrustedDomainService;
let workspaceContextService: MockWorkspaceContextService;
let sandboxHelperService: MockSandboxHelperService;
let fileService: MockFileService;
let createdFiles: Map<string, string>;
class MockTrustedDomainService implements ITrustedDomainService {
_serviceBrand: undefined;
@@ -55,6 +41,14 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
}
}
class MockFileService {
async createFile(uri: URI, content: VSBuffer): Promise<any> {
const contentString = content.toString();
createdFiles.set(uri.path, contentString);
return {};
}
}
class MockRemoteAgentService {
async getEnvironment(): Promise<IRemoteAgentEnvironment> {
// Return a Linux environment to ensure tests pass on Windows
@@ -83,113 +77,35 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
isUnsupportedGlibc: false
};
}
getConnection() {
return null;
}
}
class PersistingTestConfigurationService extends TestConfigurationService {
override updateValue(key: string, value: unknown, ..._rest: unknown[]): Promise<void> {
return this.setUserConfiguration(key, value);
}
}
class MockWorkspaceContextService extends mock<IWorkspaceContextService>() {
override _serviceBrand: undefined;
private readonly _onDidChangeWorkspaceFolders = new Emitter<any>();
override readonly onDidChangeWorkspaceFolders = this._onDidChangeWorkspaceFolders.event;
folders: IWorkspaceFolder[] = [];
override getWorkspace() {
return {
id: 'test-workspace',
folders: this.folders,
};
}
}
class MockSandboxHelperService implements ISandboxHelperService {
_serviceBrand: undefined;
private readonly _onDidRequestSandboxPermission = new Emitter<ISandboxPermissionRequest>();
readonly onDidRequestSandboxPermission = this._onDidRequestSandboxPermission.event;
private readonly _pendingPermissionResponses = new Map<string, (allowed: boolean) => void>();
resetSandboxCallCount = 0;
lastWrapRuntimeConfig: ISandboxRuntimeConfig | undefined;
async resetSandbox(): Promise<void> {
this.resetSandboxCallCount++;
}
async resolveSandboxPermissionRequest(requestId: string, allowed: boolean): Promise<void> {
this._pendingPermissionResponses.get(requestId)?.(allowed);
this._pendingPermissionResponses.delete(requestId);
}
async wrapWithSandbox(runtimeConfig: ISandboxRuntimeConfig, command: string): Promise<string> {
this.lastWrapRuntimeConfig = runtimeConfig;
return `wrapped:${command}`;
}
fireSandboxPermissionRequest(request: ISandboxPermissionRequest): void {
this._onDidRequestSandboxPermission.fire(request);
}
waitForSandboxPermissionResponse(requestId: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
this._pendingPermissionResponses.set(requestId, resolve);
});
}
}
class TestTerminalSandboxService extends TerminalSandboxService {
readonly permissionRequests: ISandboxPermissionRequest[] = [];
override async promptForSandboxPermission(request: ISandboxPermissionRequest): Promise<boolean> {
this.permissionRequests.push(request);
return true;
}
}
setup(() => {
createdFiles = new Map();
instantiationService = workbenchInstantiationService({}, store);
configurationService = new PersistingTestConfigurationService();
configurationService = new TestConfigurationService();
trustedDomainService = new MockTrustedDomainService();
workspaceContextService = new MockWorkspaceContextService();
sandboxHelperService = new MockSandboxHelperService();
fileService = new MockFileService();
// Setup default configuration
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled, true);
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, {
allowedDomains: [],
deniedDomains: [],
allowTrustedDomains: true
allowTrustedDomains: false
});
instantiationService.stub(IConfigurationService, configurationService);
instantiationService.stub(IFileService, fileService);
instantiationService.stub(IEnvironmentService, <IEnvironmentService & { tmpDir?: URI; execPath?: string }>{
_serviceBrand: undefined,
tmpDir: URI.file('/tmp'),
execPath: '/usr/bin/node'
});
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IRemoteAgentService, new MockRemoteAgentService());
instantiationService.stub(ITrustedDomainService, trustedDomainService);
instantiationService.stub(IWorkspaceContextService, workspaceContextService);
instantiationService.stub(ISandboxHelperService, sandboxHelperService);
instantiationService.stub(IDialogService, new class extends mock<IDialogService>() {
override async confirm() {
return { confirmed: true };
}
});
});
async function getWrappedRuntimeConfig(sandboxService: TerminalSandboxService): Promise<CapturedSandboxRuntimeConfig> {
await sandboxService.isEnabled();
await sandboxService.wrapCommand('echo test');
ok(sandboxHelperService.lastWrapRuntimeConfig, 'Sandbox helper should receive a runtime config');
ok(sandboxHelperService.lastWrapRuntimeConfig.network, 'Sandbox helper config should include network settings');
ok(Array.isArray(sandboxHelperService.lastWrapRuntimeConfig.network.allowedDomains), 'Sandbox helper config should include allowed domains');
ok(sandboxHelperService.lastWrapRuntimeConfig.filesystem, 'Sandbox helper config should include filesystem settings');
ok(Array.isArray(sandboxHelperService.lastWrapRuntimeConfig.filesystem.allowWrite), 'Sandbox helper config should include writable paths');
return sandboxHelperService.lastWrapRuntimeConfig as CapturedSandboxRuntimeConfig;
}
test('should filter out sole wildcard (*) from trusted domains', async () => {
// Setup: Enable allowTrustedDomains and add * to trusted domains
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, {
@@ -200,7 +116,13 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
trustedDomainService.trustedDomains = ['*'];
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
const configPath = await sandboxService.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const configContent = createdFiles.get(configPath);
ok(configContent, 'Config file should be created');
const config = JSON.parse(configContent);
strictEqual(config.network.allowedDomains.length, 0, 'Sole wildcard * should be filtered out');
});
@@ -214,7 +136,13 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
trustedDomainService.trustedDomains = ['*.github.com'];
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
const configPath = await sandboxService.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const configContent = createdFiles.get(configPath);
ok(configContent, 'Config file should be created');
const config = JSON.parse(configContent);
strictEqual(config.network.allowedDomains.length, 1, 'Wildcard domain should be included');
strictEqual(config.network.allowedDomains[0], '*.github.com', 'Wildcard domain should match');
});
@@ -229,7 +157,13 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
trustedDomainService.trustedDomains = ['*', '*.github.com', 'microsoft.com'];
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
const configPath = await sandboxService.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const configContent = createdFiles.get(configPath);
ok(configContent, 'Config file should be created');
const config = JSON.parse(configContent);
strictEqual(config.network.allowedDomains.length, 3, 'Should have 3 domains (excluding *)');
ok(config.network.allowedDomains.includes('example.com'), 'Should include configured domain');
ok(config.network.allowedDomains.includes('*.github.com'), 'Should include wildcard domain');
@@ -247,7 +181,13 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
trustedDomainService.trustedDomains = ['*', '*.github.com'];
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
const configPath = await sandboxService.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const configContent = createdFiles.get(configPath);
ok(configContent, 'Config file should be created');
const config = JSON.parse(configContent);
strictEqual(config.network.allowedDomains.length, 1, 'Should only have configured domain');
strictEqual(config.network.allowedDomains[0], 'example.com', 'Should only include example.com');
});
@@ -262,7 +202,13 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
trustedDomainService.trustedDomains = ['*.github.com', 'github.com'];
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
const configPath = await sandboxService.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const configContent = createdFiles.get(configPath);
ok(configContent, 'Config file should be created');
const config = JSON.parse(configContent);
strictEqual(config.network.allowedDomains.length, 2, 'Should have 2 unique domains');
ok(config.network.allowedDomains.includes('github.com'), 'Should include github.com');
ok(config.network.allowedDomains.includes('*.github.com'), 'Should include *.github.com');
@@ -278,7 +224,13 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
trustedDomainService.trustedDomains = [];
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
const configPath = await sandboxService.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const configContent = createdFiles.get(configPath);
ok(configContent, 'Config file should be created');
const config = JSON.parse(configContent);
strictEqual(config.network.allowedDomains.length, 1, 'Should have only configured domain');
strictEqual(config.network.allowedDomains[0], 'example.com', 'Should only include example.com');
});
@@ -293,159 +245,89 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
trustedDomainService.trustedDomains = ['*'];
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
const configPath = await sandboxService.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const configContent = createdFiles.get(configPath);
ok(configContent, 'Config file should be created');
const config = JSON.parse(configContent);
strictEqual(config.network.allowedDomains.length, 0, 'Should have no domains (* filtered out)');
});
test('should expand workspace write access defaults for multi-root workspaces', async () => {
workspaceContextService.folders = [
toWorkspaceFolder(URI.file('/workspace-one')),
toWorkspaceFolder(URI.file('/workspace-two')),
];
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem, {
denyRead: [],
allowWrite: ['.'],
denyWrite: []
});
test('should add ripgrep bin directory to PATH when wrapping command', async () => {
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const config = await getWrappedRuntimeConfig(sandboxService);
ok(config.filesystem.allowWrite.includes('/workspace-one'), 'Should include the first workspace folder path');
ok(config.filesystem.allowWrite.includes('/workspace-two'), 'Should include the second workspace folder path');
ok(config.filesystem.allowWrite.includes('~/.npm'), 'Should include the default npm write path');
await sandboxService.getSandboxConfigPath();
const wrappedCommand = sandboxService.wrapCommand('echo test');
ok(
wrappedCommand.includes('PATH') && wrappedCommand.includes('ripgrep'),
'Wrapped command should include PATH modification with ripgrep'
);
});
test('should delegate wrapping to sandbox helper', async () => {
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
const wrappedCommand = await sandboxService.wrapCommand('echo test');
strictEqual(wrappedCommand, 'wrapped:echo test');
});
test('should preserve the full command when delegating wrapping', async () => {
test('should pass wrapped command as a single quoted argument', async () => {
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
await sandboxService.getSandboxConfigPath();
const command = '";echo SANDBOX_ESCAPE_REPRO; # $(uname) `id`';
const wrappedCommand = await sandboxService.wrapCommand(command);
strictEqual(wrappedCommand, `wrapped:${command}`);
const wrappedCommand = sandboxService.wrapCommand(command);
ok(
wrappedCommand.includes(`-c '";echo SANDBOX_ESCAPE_REPRO; # $(uname) \`id\`'`),
'Wrapped command should shell-quote the command argument using single quotes'
);
ok(
!wrappedCommand.includes(`-c "${command}"`),
'Wrapped command should not embed the command in double quotes'
);
});
test('should preserve variable and command substitution payloads', async () => {
test('should keep variable and command substitution payloads literal', async () => {
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
await sandboxService.getSandboxConfigPath();
const command = 'echo $HOME $(curl eth0.me) `id`';
const wrappedCommand = await sandboxService.wrapCommand(command);
strictEqual(wrappedCommand, `wrapped:${command}`);
const wrappedCommand = sandboxService.wrapCommand(command);
ok(
wrappedCommand.includes(`-c 'echo $HOME $(curl eth0.me) \`id\`'`),
'Wrapped command should keep variable and command substitutions inside the quoted argument'
);
ok(
!wrappedCommand.includes(`-c ${command}`),
'Wrapped command should not pass substitution payloads to -c without quoting'
);
});
test('should preserve single-quote breakout payloads', async () => {
test('should escape single-quote breakout payloads in wrapped command argument', async () => {
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
await sandboxService.getSandboxConfigPath();
const command = `';curl eth0.me; #'`;
const wrappedCommand = await sandboxService.wrapCommand(command);
strictEqual(wrappedCommand, `wrapped:${command}`);
const wrappedCommand = sandboxService.wrapCommand(command);
ok(
wrappedCommand.includes(`-c '`),
'Wrapped command should continue to use a single-quoted -c argument'
);
ok(
wrappedCommand.includes('curl eth0.me'),
'Wrapped command should preserve the payload text literally'
);
ok(
!wrappedCommand.includes(`-c '${command}'`),
'Wrapped command should not embed attacker-controlled single quotes without escaping'
);
strictEqual((wrappedCommand.match(/\\''/g) ?? []).length, 2, 'Single quote breakout payload should escape each embedded single quote');
});
test('should preserve embedded single quotes', async () => {
test('should escape embedded single quotes in wrapped command argument', async () => {
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
await sandboxService.getSandboxConfigPath();
const wrappedCommand = await sandboxService.wrapCommand(`echo 'hello'`);
strictEqual(wrappedCommand, `wrapped:echo 'hello'`);
});
test('should route sandbox permission requests through terminal sandbox service', async () => {
const sandboxHelperService = new MockSandboxHelperService();
instantiationService.stub(ISandboxHelperService, sandboxHelperService);
const sandboxService = store.add(instantiationService.createInstance(TestTerminalSandboxService));
await sandboxService.wrapWithSandbox({
network: {
allowedDomains: [],
deniedDomains: []
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: []
}
}, 'echo test');
const responsePromise = sandboxHelperService.waitForSandboxPermissionResponse('request-1');
sandboxHelperService.fireSandboxPermissionRequest({
requestId: 'request-1',
host: 'example.com',
port: 443,
});
strictEqual(await responsePromise, true);
strictEqual(sandboxService.permissionRequests.length, 1);
strictEqual(sandboxService.permissionRequests[0].host, 'example.com');
strictEqual(sandboxService.permissionRequests[0].port, 443);
});
test('should persist approved sandbox hosts to settings', async () => {
const sandboxHelperService = new MockSandboxHelperService();
instantiationService.stub(ISandboxHelperService, sandboxHelperService);
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork, {
allowedDomains: ['existing.com'],
deniedDomains: ['example.com'],
allowTrustedDomains: false
});
const sandboxService = store.add(instantiationService.createInstance(TestTerminalSandboxService));
await sandboxService.wrapWithSandbox({
network: {
allowedDomains: [],
deniedDomains: []
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: []
}
}, 'echo test');
const responsePromise = sandboxHelperService.waitForSandboxPermissionResponse('request-2');
sandboxHelperService.fireSandboxPermissionRequest({
requestId: 'request-2',
host: 'example.com',
port: 443,
});
strictEqual(await responsePromise, true);
const updatedSettings = configurationService.getValue<{
allowedDomains?: string[];
deniedDomains?: string[];
}>(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork);
ok(updatedSettings?.allowedDomains?.includes('existing.com'));
ok(updatedSettings?.allowedDomains?.includes('example.com'));
ok(!updatedSettings?.deniedDomains?.includes('example.com'));
});
test('should persist approved sandbox write paths to settings', async () => {
configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem, {
denyRead: [],
allowWrite: ['/existing/path'],
denyWrite: ['/tmp/blocked.txt']
});
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
strictEqual(await sandboxService.promptToAllowWritePath('/tmp/blocked.txt'), true);
const updatedSettings = configurationService.getValue<{
allowWrite?: string[];
denyWrite?: string[];
}>(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem);
ok(updatedSettings?.allowWrite?.includes('/existing/path'));
ok(updatedSettings?.allowWrite?.includes('/tmp/blocked.txt'));
ok(!updatedSettings?.denyWrite?.includes('/tmp/blocked.txt'));
});
test('should not reset sandbox when unrelated settings change', async () => {
store.add(instantiationService.createInstance(TerminalSandboxService));
strictEqual(sandboxHelperService.resetSandboxCallCount, 0);
configurationService.setUserConfiguration('window.zoomLevel', 1);
strictEqual(sandboxHelperService.resetSandboxCallCount, 0);
const wrappedCommand = sandboxService.wrapCommand(`echo 'hello'`);
strictEqual((wrappedCommand.match(/\\''/g) ?? []).length, 2, 'Single quote escapes should be inserted for each embedded single quote');
});
});

View File

@@ -22,9 +22,10 @@ suite('CommandLineSandboxRewriter', () => {
instantiationService.stub(ITerminalSandboxService, {
_serviceBrand: undefined,
isEnabled: async () => false,
promptToAllowWritePath: async () => false,
wrapWithSandbox: async (_runtimeConfig, command) => command,
wrapCommand: command => Promise.resolve(command),
wrapCommand: command => command,
getSandboxConfigPath: async () => '/tmp/sandbox.json',
getTempDir: () => undefined,
setNeedsForceUpdateConfigFile: () => { },
...overrides
});
};
@@ -45,13 +46,29 @@ suite('CommandLineSandboxRewriter', () => {
strictEqual(result, undefined);
});
test('wraps command when sandbox is enabled', async () => {
test('returns undefined when sandbox config is unavailable', async () => {
stubSandboxService({
isEnabled: async () => true,
wrapCommand: command => `wrapped:${command}`,
getSandboxConfigPath: async () => undefined,
});
const rewriter = store.add(instantiationService.createInstance(CommandLineSandboxRewriter));
const result = await rewriter.rewrite(createRewriteOptions('echo hello'));
strictEqual(result, undefined);
});
test('wraps command when sandbox is enabled and config exists', async () => {
const calls: string[] = [];
stubSandboxService({
isEnabled: async () => true,
wrapCommand: command => {
calls.push('wrapCommand');
return Promise.resolve(`wrapped:${command}`);
return `wrapped:${command}`;
},
getSandboxConfigPath: async () => {
calls.push('getSandboxConfigPath');
return '/tmp/sandbox.json';
},
});
@@ -59,6 +76,6 @@ suite('CommandLineSandboxRewriter', () => {
const result = await rewriter.rewrite(createRewriteOptions('echo hello'));
strictEqual(result?.rewritten, 'wrapped:echo hello');
strictEqual(result?.reasoning, 'Wrapped command for sandbox execution');
deepStrictEqual(calls, ['wrapCommand']);
deepStrictEqual(calls, ['getSandboxConfigPath', 'wrapCommand']);
});
});

View File

@@ -108,9 +108,10 @@ suite('RunInTerminalTool', () => {
instantiationService.stub(ITerminalSandboxService, {
_serviceBrand: undefined,
isEnabled: async () => false,
promptToAllowWritePath: async () => false,
wrapWithSandbox: async (_runtimeConfig, command) => command,
wrapCommand: command => Promise.resolve(command),
wrapCommand: command => command,
getSandboxConfigPath: async () => undefined,
getTempDir: () => undefined,
setNeedsForceUpdateConfigFile: () => { }
});
const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService));