mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
Agent sandboxing: detect missing dependencies before execution and offer installation (#305898)
* updating tests * sandbox dependencies check for linux * sandbox dependencies check for linux * review comment * Injecting sandboxhelperservice for web
This commit is contained in:
@@ -49,6 +49,8 @@ import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from '../..
|
||||
import { ExtensionHostStarter } from '../../platform/extensions/electron-main/extensionHostStarter.js';
|
||||
import { IExternalTerminalMainService } from '../../platform/externalTerminal/electron-main/externalTerminal.js';
|
||||
import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from '../../platform/externalTerminal/node/externalTerminalService.js';
|
||||
import { ISandboxHelperMainService } from '../../platform/sandbox/electron-main/sandboxHelperService.js';
|
||||
import { SandboxHelperService } from '../../platform/sandbox/node/sandboxHelper.js';
|
||||
import { LOCAL_FILE_SYSTEM_CHANNEL_NAME } from '../../platform/files/common/diskFileSystemProviderClient.js';
|
||||
import { IFileService } from '../../platform/files/common/files.js';
|
||||
import { DiskFileSystemProviderChannel } from '../../platform/files/electron-main/diskFileSystemProviderServer.js';
|
||||
@@ -1150,6 +1152,7 @@ export class CodeApplication extends Disposable {
|
||||
} else if (isLinux) {
|
||||
services.set(IExternalTerminalMainService, new SyncDescriptor(LinuxExternalTerminalService));
|
||||
}
|
||||
services.set(ISandboxHelperMainService, new SyncDescriptor(SandboxHelperService));
|
||||
|
||||
// Backups
|
||||
const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService, this.stateService);
|
||||
@@ -1320,6 +1323,10 @@ export class CodeApplication extends Disposable {
|
||||
const externalTerminalChannel = ProxyChannel.fromService(accessor.get(IExternalTerminalMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel('externalTerminal', externalTerminalChannel);
|
||||
|
||||
// Sandbox Helper
|
||||
const sandboxHelperChannel = ProxyChannel.fromService(accessor.get(ISandboxHelperMainService), disposables);
|
||||
mainProcessElectronServer.registerChannel('sandboxHelper', sandboxHelperChannel);
|
||||
|
||||
// MCP
|
||||
const mcpDiscoveryChannel = ProxyChannel.fromService(accessor.get(INativeMcpDiscoveryHelperService), disposables);
|
||||
mainProcessElectronServer.registerChannel(NativeMcpDiscoveryHelperChannelName, mcpDiscoveryChannel);
|
||||
|
||||
23
src/vs/platform/sandbox/browser/sandboxHelperService.ts
Normal file
23
src/vs/platform/sandbox/browser/sandboxHelperService.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { ISandboxDependencyStatus, ISandboxHelperService } from '../common/sandboxHelperService.js';
|
||||
|
||||
class NullSandboxHelperService implements ISandboxHelperService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
async checkSandboxDependencies(): Promise<ISandboxDependencyStatus> {
|
||||
// Web targets cannot inspect host sandbox dependencies directly.
|
||||
// Treat them as satisfied so browser workbench targets do not fail DI
|
||||
// or block sandbox flows on an unavailable host-side capability.
|
||||
return {
|
||||
bubblewrapInstalled: true,
|
||||
socatInstalled: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ISandboxHelperService, NullSandboxHelperService, InstantiationType.Delayed);
|
||||
39
src/vs/platform/sandbox/common/sandboxHelperIpc.ts
Normal file
39
src/vs/platform/sandbox/common/sandboxHelperIpc.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { ISandboxDependencyStatus, ISandboxHelperService } from './sandboxHelperService.js';
|
||||
|
||||
export const SANDBOX_HELPER_CHANNEL_NAME = 'sandboxHelper';
|
||||
|
||||
export class SandboxHelperChannel implements IServerChannel {
|
||||
|
||||
constructor(private readonly service: ISandboxHelperService) { }
|
||||
|
||||
listen<T>(_context: unknown, _event: string): Event<T> {
|
||||
throw new Error('Invalid listen');
|
||||
}
|
||||
|
||||
call<T>(_context: unknown, command: string, _arg?: unknown, _cancellationToken?: CancellationToken): Promise<T> {
|
||||
switch (command) {
|
||||
case 'checkSandboxDependencies':
|
||||
return this.service.checkSandboxDependencies() as Promise<T>;
|
||||
}
|
||||
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
export class SandboxHelperChannelClient implements ISandboxHelperService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(private readonly channel: IChannel) { }
|
||||
|
||||
checkSandboxDependencies(): Promise<ISandboxDependencyStatus | undefined> {
|
||||
return this.channel.call<ISandboxDependencyStatus | undefined>('checkSandboxDependencies');
|
||||
}
|
||||
}
|
||||
18
src/vs/platform/sandbox/common/sandboxHelperService.ts
Normal file
18
src/vs/platform/sandbox/common/sandboxHelperService.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
|
||||
export const ISandboxHelperService = createDecorator<ISandboxHelperService>('sandboxHelperService');
|
||||
|
||||
export interface ISandboxDependencyStatus {
|
||||
readonly bubblewrapInstalled: boolean;
|
||||
readonly socatInstalled: boolean;
|
||||
}
|
||||
|
||||
export interface ISandboxHelperService {
|
||||
readonly _serviceBrand: undefined;
|
||||
checkSandboxDependencies(): Promise<ISandboxDependencyStatus | undefined>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ISandboxHelperService } from '../common/sandboxHelperService.js';
|
||||
|
||||
registerMainProcessRemoteService(ISandboxHelperService, 'sandboxHelper');
|
||||
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ISandboxHelperService } from '../common/sandboxHelperService.js';
|
||||
|
||||
export const ISandboxHelperMainService = createDecorator<ISandboxHelperMainService>('sandboxHelper');
|
||||
|
||||
export interface ISandboxHelperMainService extends ISandboxHelperService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
34
src/vs/platform/sandbox/node/sandboxHelper.ts
Normal file
34
src/vs/platform/sandbox/node/sandboxHelper.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isLinux } from '../../../base/common/platform.js';
|
||||
import { findExecutable } from '../../../base/node/processes.js';
|
||||
import { ISandboxDependencyStatus, ISandboxHelperService } from '../common/sandboxHelperService.js';
|
||||
|
||||
type FindCommand = (command: string) => Promise<string | undefined>;
|
||||
|
||||
export class SandboxHelperService implements ISandboxHelperService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
static async checkSandboxDependenciesWith(findCommand: FindCommand, linux: boolean = isLinux): Promise<ISandboxDependencyStatus | undefined> {
|
||||
if (!linux) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [bubblewrapPath, socatPath] = await Promise.all([
|
||||
findCommand('bwrap'),
|
||||
findCommand('socat'),
|
||||
]);
|
||||
|
||||
return {
|
||||
bubblewrapInstalled: !!bubblewrapPath,
|
||||
socatInstalled: !!socatPath,
|
||||
};
|
||||
}
|
||||
|
||||
checkSandboxDependencies(): Promise<ISandboxDependencyStatus | undefined> {
|
||||
return SandboxHelperService.checkSandboxDependenciesWith(findExecutable);
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,8 @@ import { McpManagementChannel } from '../../platform/mcp/common/mcpManagementIpc
|
||||
import { AllowedMcpServersService } from '../../platform/mcp/common/allowedMcpServersService.js';
|
||||
import { IMcpGalleryManifestService } from '../../platform/mcp/common/mcpGalleryManifest.js';
|
||||
import { McpGalleryManifestIPCService } from '../../platform/mcp/common/mcpGalleryManifestServiceIpc.js';
|
||||
import { SANDBOX_HELPER_CHANNEL_NAME, SandboxHelperChannel } from '../../platform/sandbox/common/sandboxHelperIpc.js';
|
||||
import { SandboxHelperService } from '../../platform/sandbox/node/sandboxHelper.js';
|
||||
|
||||
const eventPrefix = 'monacoworkbench';
|
||||
|
||||
@@ -265,6 +267,8 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
|
||||
const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), oneDsAppender);
|
||||
socketServer.registerChannel('telemetry', telemetryChannel);
|
||||
|
||||
socketServer.registerChannel(SANDBOX_HELPER_CHANNEL_NAME, new SandboxHelperChannel(new SandboxHelperService()));
|
||||
|
||||
socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyHostService, productService, extensionManagementService, configurationService));
|
||||
|
||||
const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI, productService.extensionsForceVersionByQuality ?? [], logService), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService, extensionManagementService);
|
||||
|
||||
@@ -89,6 +89,7 @@ import '../workbench/services/extensions/electron-browser/nativeExtensionService
|
||||
import '../platform/userDataProfile/electron-browser/userDataProfileStorageService.js';
|
||||
import '../workbench/services/auxiliaryWindow/electron-browser/auxiliaryWindowService.js';
|
||||
import '../platform/extensionManagement/electron-browser/extensionsProfileScannerService.js';
|
||||
import '../platform/sandbox/electron-browser/sandboxHelperService.js';
|
||||
import '../platform/webContentExtractor/electron-browser/webContentExtractorService.js';
|
||||
import '../workbench/services/browserView/electron-browser/playwrightWorkbenchService.js';
|
||||
import '../workbench/services/process/electron-browser/processService.js';
|
||||
|
||||
@@ -71,6 +71,7 @@ import '../platform/extensionResourceLoader/browser/extensionResourceLoaderServi
|
||||
import '../workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.js';
|
||||
import '../workbench/services/browserElements/browser/webBrowserElementsService.js';
|
||||
import '../workbench/services/power/browser/powerService.js';
|
||||
import '../platform/sandbox/browser/sandboxHelperService.js';
|
||||
|
||||
import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js';
|
||||
import { IAccessibilityService } from '../platform/accessibility/common/accessibility.js';
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from '../../../../../../../base/browser/dom.js';
|
||||
import { MarkdownString } from '../../../../../../../base/common/htmlContent.js';
|
||||
import { localize } from '../../../../../../../nls.js';
|
||||
import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { IChatToolInvocation, type IChatTerminalToolInvocationData } from '../../../../common/chatService/chatService.js';
|
||||
import { ILanguageModelToolsService } from '../../../../common/tools/languageModelToolsService.js';
|
||||
import { AcceptToolConfirmationActionId, SkipToolConfirmationActionId } from '../../../actions/chatToolActions.js';
|
||||
import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js';
|
||||
import { IChatContentPartRenderContext } from '../chatContentParts.js';
|
||||
import { AbstractToolConfirmationSubPart } from './abstractToolConfirmationSubPart.js';
|
||||
|
||||
export class ChatMissingSandboxDepsConfirmationSubPart extends AbstractToolConfirmationSubPart {
|
||||
public readonly codeblocks: IChatCodeBlockInfo[] = [];
|
||||
|
||||
constructor(
|
||||
toolInvocation: IChatToolInvocation,
|
||||
_terminalData: IChatTerminalToolInvocationData,
|
||||
context: IChatContentPartRenderContext,
|
||||
private readonly renderer: IMarkdownRenderer,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IChatWidgetService chatWidgetService: IChatWidgetService,
|
||||
@ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService,
|
||||
) {
|
||||
super(toolInvocation, context, instantiationService, keybindingService, contextKeyService, chatWidgetService, languageModelToolsService);
|
||||
|
||||
this.render({
|
||||
allowActionId: AcceptToolConfirmationActionId,
|
||||
skipActionId: SkipToolConfirmationActionId,
|
||||
allowLabel: localize('missingDeps.install', "Install"),
|
||||
skipLabel: localize('missingDeps.cancel', "Cancel"),
|
||||
partType: 'chatMissingSandboxDepsConfirmation',
|
||||
});
|
||||
}
|
||||
|
||||
protected override createContentElement(): HTMLElement {
|
||||
const state = this.toolInvocation.state.get();
|
||||
const message = state.type === IChatToolInvocation.StateKind.WaitingForConfirmation
|
||||
? state.confirmationMessages?.message
|
||||
: undefined;
|
||||
|
||||
const container = dom.$('.chat-missing-sandbox-deps-confirmation');
|
||||
if (message) {
|
||||
const mdMessage = typeof message === 'string' ? new MarkdownString(message) : message;
|
||||
const rendered = this.renderer.render(mdMessage);
|
||||
this._register(rendered);
|
||||
container.appendChild(rendered.element);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
protected override getTitle(): string {
|
||||
const state = this.toolInvocation.state.get();
|
||||
if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) {
|
||||
return typeof state.confirmationMessages.title === 'string'
|
||||
? state.confirmationMessages.title
|
||||
: state.confirmationMessages.title.value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { Disposable, DisposableStore, IDisposable } from '../../../../../../../b
|
||||
import { autorun, derived } from '../../../../../../../base/common/observable.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js';
|
||||
import { IChatToolInvocation, IChatToolInvocationSerialized, isLegacyChatTerminalToolInvocationData, ToolConfirmKind } from '../../../../common/chatService/chatService.js';
|
||||
import { IChatRendererContent } from '../../../../common/model/chatViewModel.js';
|
||||
import { IChatTodoListService } from '../../../../common/tools/chatTodoListService.js';
|
||||
import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js';
|
||||
@@ -23,6 +23,7 @@ import { ChatInputOutputMarkdownProgressPart } from './chatInputOutputMarkdownPr
|
||||
import { ChatMcpAppSubPart, IMcpAppRenderData } from './chatMcpAppSubPart.js';
|
||||
import { ChatResultListSubPart } from './chatResultListSubPart.js';
|
||||
import { ChatSimpleToolProgressPart } from './chatSimpleToolProgressPart.js';
|
||||
import { ChatMissingSandboxDepsConfirmationSubPart } from './chatMissingSandboxDepsConfirmationSubPart.js';
|
||||
import { ChatModifiedFilesConfirmationSubPart } from './chatModifiedFilesConfirmationSubPart.js';
|
||||
import { ChatTerminalToolConfirmationSubPart } from './chatTerminalToolConfirmationSubPart.js';
|
||||
import { ChatTerminalToolProgressPart } from './chatTerminalToolProgressPart.js';
|
||||
@@ -122,6 +123,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
|
||||
const isConfirmation = this.subPart instanceof ToolConfirmationSubPart ||
|
||||
this.subPart instanceof ChatTerminalToolConfirmationSubPart ||
|
||||
this.subPart instanceof ChatModifiedFilesConfirmationSubPart ||
|
||||
this.subPart instanceof ChatMissingSandboxDepsConfirmationSubPart ||
|
||||
this.subPart instanceof ExtensionsInstallConfirmationWidgetSubPart ||
|
||||
this.subPart instanceof ChatToolPostExecuteConfirmationPart;
|
||||
this.domNode.classList.toggle('has-confirmation', isConfirmation);
|
||||
@@ -173,7 +175,9 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
|
||||
}
|
||||
|
||||
if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {
|
||||
if (this.toolInvocation.toolSpecificData?.kind === 'terminal') {
|
||||
if (this.toolInvocation.toolSpecificData?.kind === 'terminal' && !isLegacyChatTerminalToolInvocationData(this.toolInvocation.toolSpecificData) && this.toolInvocation.toolSpecificData.missingSandboxDependencies?.length) {
|
||||
return this.instantiationService.createInstance(ChatMissingSandboxDepsConfirmationSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer);
|
||||
} else if (this.toolInvocation.toolSpecificData?.kind === 'terminal') {
|
||||
return this.instantiationService.createInstance(ChatTerminalToolConfirmationSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockModelCollection, this.codeBlockStartIndex);
|
||||
} else if (this.toolInvocation.toolSpecificData?.kind === 'modifiedFilesConfirmation') {
|
||||
return this.instantiationService.createInstance(ChatModifiedFilesConfirmationSubPart, this.toolInvocation, this.context, this.listPool);
|
||||
|
||||
@@ -559,6 +559,8 @@ export interface IChatTerminalToolInvocationData {
|
||||
/** Whether the user chose to continue in background for this tool invocation */
|
||||
didContinueInBackground?: boolean;
|
||||
autoApproveInfo?: IMarkdownString;
|
||||
/** Names of missing sandbox dependencies that the user may choose to install */
|
||||
missingSandboxDependencies?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ITerminalSandboxService } from '../../../common/terminalSandboxService.js';
|
||||
import { ITerminalSandboxService, TerminalSandboxPrerequisiteCheck } from '../../../common/terminalSandboxService.js';
|
||||
import type { ICommandLineRewriter, ICommandLineRewriterOptions, ICommandLineRewriterResult } from './commandLineRewriter.js';
|
||||
|
||||
export class CommandLineSandboxRewriter extends Disposable implements ICommandLineRewriter {
|
||||
@@ -15,15 +15,8 @@ export class CommandLineSandboxRewriter extends Disposable implements ICommandLi
|
||||
}
|
||||
|
||||
async rewrite(options: ICommandLineRewriterOptions): Promise<ICommandLineRewriterResult | undefined> {
|
||||
|
||||
if (!(await this._sandboxService.isEnabled())) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Ensure sandbox config is initialized before wrapping
|
||||
const sandboxConfigPath = await this._sandboxService.getSandboxConfigPath();
|
||||
if (!sandboxConfigPath) {
|
||||
// If no sandbox config is available, run without sandboxing
|
||||
const sandboxPrereqs = await this._sandboxService.checkForSandboxingPrereqs();
|
||||
if (!sandboxPrereqs.enabled || sandboxPrereqs.failedCheck === TerminalSandboxPrerequisiteCheck.Config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import { ITerminalLogService, ITerminalProfile } from '../../../../../../platfor
|
||||
import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js';
|
||||
import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js';
|
||||
import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService/chatService.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IStreamedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js';
|
||||
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolConfirmationMessages, IStreamedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js';
|
||||
import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js';
|
||||
import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js';
|
||||
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';
|
||||
@@ -72,7 +72,7 @@ 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, type ITerminalSandboxResolvedNetworkDomains } from '../../common/terminalSandboxService.js';
|
||||
import { ITerminalSandboxService, TerminalSandboxPrerequisiteCheck, type ITerminalSandboxResolvedNetworkDomains } from '../../common/terminalSandboxService.js';
|
||||
import { LanguageModelPartAudience } from '../../../../chat/common/languageModels.js';
|
||||
|
||||
// #region Tool data
|
||||
@@ -546,7 +546,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
instance = toolTerminal.instance;
|
||||
}
|
||||
}
|
||||
const [os, shell, cwd, isTerminalSandboxEnabled] = await Promise.all([
|
||||
const [os, shell, cwd, sandboxPrereqs] = await Promise.all([
|
||||
this._osBackend,
|
||||
this._profileFetcher.getCopilotShell(),
|
||||
(async () => {
|
||||
@@ -558,11 +558,16 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
}
|
||||
return cwd;
|
||||
})(),
|
||||
this._terminalSandboxService.isEnabled()
|
||||
this._terminalSandboxService.checkForSandboxingPrereqs()
|
||||
]);
|
||||
const language = os === OperatingSystem.Windows ? 'pwsh' : 'sh';
|
||||
const isTerminalSandboxEnabled = sandboxPrereqs.enabled;
|
||||
const requiresUnsandboxConfirmation = isTerminalSandboxEnabled && args.requestUnsandboxedExecution === true;
|
||||
|
||||
const missingDependencies = sandboxPrereqs.failedCheck === TerminalSandboxPrerequisiteCheck.Dependencies && sandboxPrereqs.missingDependencies?.length
|
||||
? sandboxPrereqs.missingDependencies
|
||||
: undefined;
|
||||
|
||||
const terminalToolSessionId = generateUuid();
|
||||
// Generate a custom command ID to link the command between renderer and pty host
|
||||
const terminalCommandId = `tool-${generateUuid()}`;
|
||||
@@ -603,8 +608,28 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
isBackground: args.isBackground,
|
||||
requestUnsandboxedExecution: requiresUnsandboxConfirmation,
|
||||
requestUnsandboxedExecutionReason: args.requestUnsandboxedExecutionReason,
|
||||
missingSandboxDependencies: missingDependencies,
|
||||
};
|
||||
|
||||
let sandboxConfirmationMessageForMissingDeps: IToolConfirmationMessages | undefined = undefined;
|
||||
// If sandbox dependencies are missing, show a confirmation asking the user to install them.
|
||||
// This is handled before the tool is invoked so the model never sees the dependency error.
|
||||
if (missingDependencies) {
|
||||
const depsList = missingDependencies.join(', ');
|
||||
sandboxConfirmationMessageForMissingDeps = {
|
||||
title: localize('runInTerminal.missingDeps.title', "Missing Sandbox Dependencies"),
|
||||
message: new MarkdownString(localize(
|
||||
'runInTerminal.missingDeps.message',
|
||||
"The following dependencies required for sandboxed execution are not installed: {0}. Would you like to install them?",
|
||||
depsList
|
||||
)),
|
||||
customButtons: [
|
||||
localize('runInTerminal.missingDeps.install', "Install"),
|
||||
localize('runInTerminal.missingDeps.cancel', "Cancel"),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// HACK: Exit early if there's an alternative recommendation, this is a little hacky but
|
||||
// it's the current mechanism for re-routing terminal tool calls to something else.
|
||||
const alternativeRecommendation = getRecommendedToolsOverRunInTerminal(args.command, this._languageModelToolsService);
|
||||
@@ -818,7 +843,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
return {
|
||||
invocationMessage,
|
||||
icon: toolSpecificData.commandLine.isSandboxWrapped ? Codicon.terminalSecure : Codicon.terminal,
|
||||
confirmationMessages,
|
||||
confirmationMessages: sandboxConfirmationMessageForMissingDeps ?? confirmationMessages,
|
||||
toolSpecificData,
|
||||
};
|
||||
}
|
||||
@@ -866,6 +891,61 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
};
|
||||
}
|
||||
|
||||
// Handle missing sandbox dependencies install flow.
|
||||
// The user was shown a confirmation window in prepareToolInvocation.
|
||||
if (toolSpecificData.missingSandboxDependencies?.length) {
|
||||
const installButton = localize('runInTerminal.missingDeps.install', "Install");
|
||||
if (invocation.selectedCustomButton === installButton) {
|
||||
// Install dependencies, focus terminal for sudo password, wait for completion
|
||||
const sessionResource = invocation.context.sessionResource;
|
||||
const { exitCode } = await this._terminalSandboxService.installMissingSandboxDependencies(toolSpecificData.missingSandboxDependencies, sessionResource, token, {
|
||||
createTerminal: async () => this._terminalService.createTerminal({}),
|
||||
focusTerminal: async (terminal) => {
|
||||
this._terminalService.setActiveInstance(terminal as ITerminalInstance);
|
||||
await this._terminalService.revealTerminal(terminal as ITerminalInstance, true);
|
||||
terminal.focus();
|
||||
},
|
||||
});
|
||||
if (exitCode !== undefined && exitCode !== 0) {
|
||||
return {
|
||||
content: [{
|
||||
kind: 'text',
|
||||
value: localize(
|
||||
'runInTerminal.missingDeps.failed',
|
||||
"Sandbox dependency installation failed (exit code {0}). The command was not executed.",
|
||||
exitCode
|
||||
),
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (exitCode === undefined) {
|
||||
return {
|
||||
content: [{
|
||||
kind: 'text',
|
||||
value: localize(
|
||||
'runInTerminal.missingDeps.unknown',
|
||||
"Could not determine whether sandbox dependency installation succeeded. The command was not executed."
|
||||
),
|
||||
}],
|
||||
};
|
||||
}
|
||||
// Installation succeeded — fall through to execute the original command
|
||||
this._logService.info('RunInTerminalTool: Sandbox dependency installation succeeded, proceeding with command execution');
|
||||
} else {
|
||||
// User chose to cancel — do not run the command
|
||||
this._logService.info('RunInTerminalTool: User cancelled sandbox dependency installation');
|
||||
return {
|
||||
content: [{
|
||||
kind: 'text',
|
||||
value: localize(
|
||||
'runInTerminal.missingDeps.cancelled',
|
||||
"Sandbox dependency installation was cancelled by the user."
|
||||
),
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const args = invocation.parameters as IRunInTerminalInputParams;
|
||||
this._logService.debug(`RunInTerminalTool: Invoking with options ${JSON.stringify(args)}`);
|
||||
let toolResultMessage: string | IMarkdownString | undefined;
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from '../../../../../base/common/async.js';
|
||||
import { VSBuffer } from '../../../../../base/common/buffer.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { Disposable, DisposableStore } 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';
|
||||
@@ -24,6 +26,13 @@ import { ITrustedDomainService } from '../../../url/common/trustedDomainService.
|
||||
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { ILifecycleService, WillShutdownJoinerOrder } from '../../../../services/lifecycle/common/lifecycle.js';
|
||||
import { ISandboxDependencyStatus, ISandboxHelperService } from '../../../../../platform/sandbox/common/sandboxHelperService.js';
|
||||
import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
import { ChatElicitationRequestPart } from '../../../chat/common/model/chatProgressTypes/chatElicitationRequestPart.js';
|
||||
import { ChatModel } from '../../../chat/common/model/chatModel.js';
|
||||
import { ElicitationState, IChatService } from '../../../chat/common/chatService/chatService.js';
|
||||
import { SANDBOX_HELPER_CHANNEL_NAME, SandboxHelperChannelClient } from '../../../../../platform/sandbox/common/sandboxHelperIpc.js';
|
||||
|
||||
export const ITerminalSandboxService = createDecorator<ITerminalSandboxService>('terminalSandboxService');
|
||||
|
||||
@@ -32,15 +41,71 @@ export interface ITerminalSandboxResolvedNetworkDomains {
|
||||
deniedDomains: string[];
|
||||
}
|
||||
|
||||
export const enum TerminalSandboxPrerequisiteCheck {
|
||||
Config = 'config',
|
||||
Dependencies = 'dependencies',
|
||||
}
|
||||
|
||||
export interface ITerminalSandboxPrerequisiteCheckResult {
|
||||
enabled: boolean;
|
||||
sandboxConfigPath: string | undefined;
|
||||
failedCheck: TerminalSandboxPrerequisiteCheck | undefined;
|
||||
missingDependencies?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstraction over terminal operations needed by the install flow.
|
||||
* Provided by the browser-layer caller so the common-layer service
|
||||
* does not import browser types directly.
|
||||
*/
|
||||
export interface ISandboxDependencyInstallTerminal {
|
||||
sendText(text: string, addNewLine?: boolean): Promise<void>;
|
||||
focus(): void;
|
||||
capabilities: {
|
||||
get(id: TerminalCapability.CommandDetection): { onCommandFinished: Event<{ exitCode: number | undefined }> } | undefined;
|
||||
onDidAddCapability: Event<{ id: TerminalCapability }>;
|
||||
};
|
||||
onDidInputData: Event<string>;
|
||||
onDisposed: Event<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to the password prompt during dependency installation.
|
||||
*/
|
||||
interface ISandboxDependencyInstallTerminalContext {
|
||||
focusTerminal(): Promise<void>;
|
||||
onDidInputData: Event<string>;
|
||||
onDisposed: Event<unknown>;
|
||||
didSendInstallCommand(): boolean;
|
||||
}
|
||||
|
||||
export interface ISandboxDependencyInstallOptions {
|
||||
/**
|
||||
* Creates or obtains a terminal for running the install command.
|
||||
*/
|
||||
createTerminal(): Promise<ISandboxDependencyInstallTerminal>;
|
||||
/**
|
||||
* Focuses the terminal for password entry.
|
||||
*/
|
||||
focusTerminal(terminal: ISandboxDependencyInstallTerminal): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ISandboxDependencyInstallResult {
|
||||
exitCode: number | undefined;
|
||||
}
|
||||
|
||||
export interface ITerminalSandboxService {
|
||||
readonly _serviceBrand: undefined;
|
||||
isEnabled(): Promise<boolean>;
|
||||
getOS(): Promise<OperatingSystem>;
|
||||
checkForSandboxingPrereqs(forceRefresh?: boolean): Promise<ITerminalSandboxPrerequisiteCheckResult>;
|
||||
wrapCommand(command: string, requestUnsandboxedExecution?: boolean): string;
|
||||
getSandboxConfigPath(forceRefresh?: boolean): Promise<string | undefined>;
|
||||
getTempDir(): URI | undefined;
|
||||
setNeedsForceUpdateConfigFile(): void;
|
||||
getResolvedNetworkDomains(): ITerminalSandboxResolvedNetworkDomains;
|
||||
getMissingSandboxDependencies(): Promise<string[]>;
|
||||
installMissingSandboxDependencies(missingDependencies: string[], sessionResource: URI | undefined, token: CancellationToken, options: ISandboxDependencyInstallOptions): Promise<ISandboxDependencyInstallResult>;
|
||||
}
|
||||
|
||||
export class TerminalSandboxService extends Disposable implements ITerminalSandboxService {
|
||||
@@ -50,6 +115,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
private _srtPathResolved = false;
|
||||
private _execPath?: string;
|
||||
private _sandboxConfigPath: string | undefined;
|
||||
private _sandboxDependencyStatus: ISandboxDependencyStatus | undefined;
|
||||
private _needsForceUpdateConfigFile = true;
|
||||
private _tempDir: URI | undefined;
|
||||
private _sandboxSettingsId: string | undefined;
|
||||
@@ -70,6 +136,8 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@ISandboxHelperService private readonly _sandboxHelperService: ISandboxHelperService,
|
||||
@IChatService private readonly _chatService: IChatService,
|
||||
) {
|
||||
super();
|
||||
this._appRoot = dirname(FileAccess.asFileUri('').path);
|
||||
@@ -114,11 +182,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
}
|
||||
|
||||
public async isEnabled(): Promise<boolean> {
|
||||
const os = await this.getOS();
|
||||
if (os === OperatingSystem.Windows) {
|
||||
return false;
|
||||
}
|
||||
return this._configurationService.getValue<boolean>(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled);
|
||||
return await this._isSandboxConfiguredEnabled();
|
||||
}
|
||||
|
||||
public async getOS(): Promise<OperatingSystem> {
|
||||
@@ -163,7 +227,44 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
this._needsForceUpdateConfigFile = true;
|
||||
}
|
||||
|
||||
public async checkForSandboxingPrereqs(forceRefresh: boolean = false): Promise<ITerminalSandboxPrerequisiteCheckResult> {
|
||||
if (!(await this._isSandboxConfiguredEnabled())) {
|
||||
return {
|
||||
enabled: false,
|
||||
sandboxConfigPath: undefined,
|
||||
failedCheck: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const sandboxConfigPath = await this.getSandboxConfigPath(forceRefresh);
|
||||
if (!sandboxConfigPath) {
|
||||
return {
|
||||
enabled: true,
|
||||
sandboxConfigPath,
|
||||
failedCheck: TerminalSandboxPrerequisiteCheck.Config,
|
||||
};
|
||||
}
|
||||
|
||||
if (!(await this._checkSandboxDependencies(forceRefresh))) {
|
||||
return {
|
||||
enabled: true,
|
||||
sandboxConfigPath,
|
||||
failedCheck: TerminalSandboxPrerequisiteCheck.Dependencies,
|
||||
missingDependencies: await this.getMissingSandboxDependencies(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
sandboxConfigPath,
|
||||
failedCheck: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
public async getSandboxConfigPath(forceRefresh: boolean = false): Promise<string | undefined> {
|
||||
if (!(await this._isSandboxConfiguredEnabled())) {
|
||||
return undefined;
|
||||
}
|
||||
await this._resolveSrtPath();
|
||||
if (!this._sandboxConfigPath || forceRefresh || this._needsForceUpdateConfigFile) {
|
||||
this._sandboxConfigPath = await this._createSandboxConfig();
|
||||
@@ -172,10 +273,164 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
return this._sandboxConfigPath;
|
||||
}
|
||||
|
||||
private async _checkSandboxDependencies(forceRefresh = false): Promise<boolean> {
|
||||
const os = await this.getOS();
|
||||
if (os === OperatingSystem.Windows) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sandboxDependencyStatus = await this._resolveSandboxDependencyStatus(forceRefresh);
|
||||
this._sandboxDependencyStatus = sandboxDependencyStatus;
|
||||
|
||||
if (sandboxDependencyStatus && !sandboxDependencyStatus.bubblewrapInstalled) {
|
||||
this._logService.warn('TerminalSandboxService: bubblewrap (bwrap) is not installed');
|
||||
}
|
||||
if (sandboxDependencyStatus && !sandboxDependencyStatus.socatInstalled) {
|
||||
this._logService.warn('TerminalSandboxService: socat is not installed');
|
||||
}
|
||||
|
||||
return sandboxDependencyStatus ? sandboxDependencyStatus.bubblewrapInstalled && sandboxDependencyStatus.socatInstalled : true;
|
||||
}
|
||||
|
||||
public async getMissingSandboxDependencies(): Promise<string[]> {
|
||||
const os = await this.getOS();
|
||||
if (os === OperatingSystem.Windows) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this._sandboxDependencyStatus || !this._sandboxDependencyStatus.bubblewrapInstalled || !this._sandboxDependencyStatus.socatInstalled) {
|
||||
this._sandboxDependencyStatus = await this._resolveSandboxDependencyStatus(true);
|
||||
}
|
||||
|
||||
const missing: string[] = [];
|
||||
if (this._sandboxDependencyStatus && !this._sandboxDependencyStatus.bubblewrapInstalled) {
|
||||
missing.push('bubblewrap');
|
||||
}
|
||||
if (this._sandboxDependencyStatus && !this._sandboxDependencyStatus.socatInstalled) {
|
||||
missing.push('socat');
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
public async installMissingSandboxDependencies(missingDependencies: string[], sessionResource: URI | undefined, token: CancellationToken, options: ISandboxDependencyInstallOptions): Promise<ISandboxDependencyInstallResult> {
|
||||
const depsList = missingDependencies.join(' ');
|
||||
const installCommand = `sudo apt install -y ${depsList}`;
|
||||
const instance = await options.createTerminal();
|
||||
|
||||
// Wait for the install command to finish so the chat can proceed automatically.
|
||||
let installCommandSent = false;
|
||||
const completionPromise = new Promise<number | undefined>(resolve => {
|
||||
const store = new DisposableStore();
|
||||
let resolved = false;
|
||||
const resolveOnce = (code: number | undefined) => {
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
resolved = true;
|
||||
store.dispose();
|
||||
resolve(code);
|
||||
};
|
||||
|
||||
const attachListener = () => {
|
||||
const detection = instance.capabilities.get(TerminalCapability.CommandDetection);
|
||||
if (detection) {
|
||||
store.add(detection.onCommandFinished(cmd => resolveOnce(cmd.exitCode)));
|
||||
}
|
||||
};
|
||||
|
||||
attachListener();
|
||||
store.add(instance.capabilities.onDidAddCapability(e => {
|
||||
if (e.id === TerminalCapability.CommandDetection) {
|
||||
attachListener();
|
||||
}
|
||||
}));
|
||||
|
||||
// Handle terminal disposal
|
||||
store.add(instance.onDisposed(() => resolveOnce(undefined)));
|
||||
|
||||
// Handle cancellation
|
||||
store.add(token.onCancellationRequested(() => resolveOnce(undefined)));
|
||||
|
||||
// Safety timeout — 5 minutes should be more than enough for apt install
|
||||
const safetyTimeout = timeout(5 * 60 * 1000);
|
||||
store.add({ dispose: () => safetyTimeout.cancel() });
|
||||
safetyTimeout.then(() => resolveOnce(undefined));
|
||||
|
||||
const passwordPrompt = this._createMissingDependencyPasswordPrompt(sessionResource, {
|
||||
focusTerminal: () => options.focusTerminal(instance),
|
||||
onDidInputData: instance.onDidInputData,
|
||||
onDisposed: instance.onDisposed,
|
||||
didSendInstallCommand: () => installCommandSent,
|
||||
}, token);
|
||||
store.add(passwordPrompt);
|
||||
});
|
||||
|
||||
// Send the command after listeners are attached so we never miss the event.
|
||||
// Set installCommandSent only after sendText completes because sendText
|
||||
// fires onDidInputData internally, and the password-prompt listener would
|
||||
// dismiss the elicitation prematurely if the flag were already true.
|
||||
await instance.sendText(installCommand, true);
|
||||
installCommandSent = true;
|
||||
|
||||
return { exitCode: await completionPromise };
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a chat elicitation that keeps the "Install" flow grounded in chat while
|
||||
* the user focuses the terminal and types a sudo password.
|
||||
*/
|
||||
private _createMissingDependencyPasswordPrompt(sessionResource: URI | undefined, promptContext: ISandboxDependencyInstallTerminalContext, token: CancellationToken): DisposableStore {
|
||||
const chatModel = sessionResource && this._chatService.getSession(sessionResource);
|
||||
if (!(chatModel instanceof ChatModel)) {
|
||||
return new DisposableStore();
|
||||
}
|
||||
|
||||
const request = chatModel.getRequests().at(-1);
|
||||
if (!request) {
|
||||
return new DisposableStore();
|
||||
}
|
||||
|
||||
const part = new ChatElicitationRequestPart(
|
||||
localize('runInTerminal.missingDeps.passwordPromptTitle', "The terminal is awaiting input."),
|
||||
new MarkdownString(localize(
|
||||
'runInTerminal.missingDeps.passwordPromptMessage',
|
||||
"Installing missing sandbox dependencies may prompt for your sudo password. Select Focus Terminal to type it in the terminal."
|
||||
)),
|
||||
'',
|
||||
localize('runInTerminal.missingDeps.focusTerminal', 'Focus Terminal'),
|
||||
undefined,
|
||||
async () => {
|
||||
await promptContext.focusTerminal();
|
||||
return ElicitationState.Pending;
|
||||
}
|
||||
);
|
||||
chatModel.acceptResponseProgress(request, part);
|
||||
|
||||
const store = new DisposableStore();
|
||||
const disposePrompt = () => store.dispose();
|
||||
store.add({ dispose: () => part.hide() });
|
||||
store.add(token.onCancellationRequested(disposePrompt));
|
||||
store.add(promptContext.onDisposed(disposePrompt));
|
||||
store.add(promptContext.onDidInputData(data => {
|
||||
if (promptContext.didSendInstallCommand() && data.length > 0) {
|
||||
disposePrompt();
|
||||
}
|
||||
}));
|
||||
return store;
|
||||
}
|
||||
|
||||
private _quoteShellArgument(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
private async _isSandboxConfiguredEnabled(): Promise<boolean> {
|
||||
const os = await this.getOS();
|
||||
if (os === OperatingSystem.Windows) {
|
||||
return false;
|
||||
}
|
||||
return this._configurationService.getValue<boolean>(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled);
|
||||
}
|
||||
|
||||
private async _resolveSrtPath(): Promise<void> {
|
||||
if (this._srtPathResolved) {
|
||||
return;
|
||||
@@ -318,4 +573,21 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
const workspaceFolderPaths = this._workspaceContextService.getWorkspace().folders.map(folder => folder.uri.path);
|
||||
return [...new Set([...workspaceFolderPaths, ...this._defaultWritePaths, ...(configuredAllowWrite ?? [])])];
|
||||
}
|
||||
|
||||
private async _resolveSandboxDependencyStatus(forceRefresh = false): Promise<ISandboxDependencyStatus | undefined> {
|
||||
if (!forceRefresh && this._sandboxDependencyStatus) {
|
||||
return this._sandboxDependencyStatus;
|
||||
}
|
||||
|
||||
const connection = this._remoteAgentService.getConnection();
|
||||
if (connection) {
|
||||
return connection.withChannel(SANDBOX_HELPER_CHANNEL_NAME, channel => {
|
||||
const sandboxHelper = new SandboxHelperChannelClient(channel);
|
||||
return sandboxHelper.checkSandboxDependencies();
|
||||
});
|
||||
}
|
||||
|
||||
return this._sandboxHelperService.checkSandboxDependencies();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/
|
||||
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { TestLifecycleService, workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
|
||||
import { TestProductService } from '../../../../../test/common/workbenchTestServices.js';
|
||||
import { TerminalSandboxService } from '../../common/terminalSandboxService.js';
|
||||
import { TerminalSandboxPrerequisiteCheck, 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';
|
||||
@@ -26,6 +26,7 @@ import { IRemoteAgentEnvironment } from '../../../../../../platform/remote/commo
|
||||
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, WorkbenchState } from '../../../../../../platform/workspace/common/workspace.js';
|
||||
import { testWorkspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js';
|
||||
import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js';
|
||||
import { ISandboxDependencyStatus, ISandboxHelperService } from '../../../../../../platform/sandbox/common/sandboxHelperService.js';
|
||||
|
||||
suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -37,6 +38,7 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
let lifecycleService: TestLifecycleService;
|
||||
let workspaceContextService: MockWorkspaceContextService;
|
||||
let productService: IProductService;
|
||||
let sandboxHelperService: MockSandboxHelperService;
|
||||
let createdFiles: Map<string, string>;
|
||||
let createdFolders: string[];
|
||||
let deletedFolders: string[];
|
||||
@@ -70,6 +72,10 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
}
|
||||
|
||||
class MockRemoteAgentService {
|
||||
getConnection() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getEnvironment(): Promise<IRemoteAgentEnvironment> {
|
||||
// Return a Linux environment to ensure tests pass on Windows
|
||||
// (sandbox is not supported on Windows)
|
||||
@@ -148,6 +154,20 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
}
|
||||
}
|
||||
|
||||
class MockSandboxHelperService implements ISandboxHelperService {
|
||||
_serviceBrand: undefined;
|
||||
callCount = 0;
|
||||
status: ISandboxDependencyStatus = {
|
||||
bubblewrapInstalled: true,
|
||||
socatInstalled: true,
|
||||
};
|
||||
|
||||
checkSandboxDependencies(): Promise<ISandboxDependencyStatus> {
|
||||
this.callCount++;
|
||||
return Promise.resolve(this.status);
|
||||
}
|
||||
}
|
||||
|
||||
setup(() => {
|
||||
createdFiles = new Map();
|
||||
createdFolders = [];
|
||||
@@ -158,6 +178,7 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
fileService = new MockFileService();
|
||||
lifecycleService = store.add(new TestLifecycleService());
|
||||
workspaceContextService = new MockWorkspaceContextService();
|
||||
sandboxHelperService = new MockSandboxHelperService();
|
||||
productService = {
|
||||
...TestProductService,
|
||||
dataFolderName: '.test-data',
|
||||
@@ -185,6 +206,41 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
instantiationService.stub(ITrustedDomainService, trustedDomainService);
|
||||
instantiationService.stub(IWorkspaceContextService, workspaceContextService);
|
||||
instantiationService.stub(ILifecycleService, lifecycleService);
|
||||
instantiationService.stub(ISandboxHelperService, sandboxHelperService);
|
||||
});
|
||||
|
||||
test('dependency checks should not be called for isEnabled', async () => {
|
||||
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
|
||||
|
||||
strictEqual(await sandboxService.isEnabled(), true, 'Sandbox should be enabled when dependencies are present');
|
||||
strictEqual(await sandboxService.isEnabled(), true, 'Sandbox should stay enabled on subsequent checks');
|
||||
strictEqual(sandboxHelperService.callCount, 0, 'Dependency checks should not be called for isEnabled');
|
||||
});
|
||||
|
||||
test('should report dependency prereq failures', async () => {
|
||||
sandboxHelperService.status = {
|
||||
bubblewrapInstalled: false,
|
||||
socatInstalled: true,
|
||||
};
|
||||
|
||||
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
|
||||
const result = await sandboxService.checkForSandboxingPrereqs();
|
||||
|
||||
strictEqual(result.enabled, true, 'Sandbox should be enabled even when dependencies are missing');
|
||||
strictEqual(result.failedCheck, TerminalSandboxPrerequisiteCheck.Dependencies, 'Missing dependencies should be reported as the failed prereq');
|
||||
strictEqual(result.missingDependencies?.length, 1, 'Missing dependency list should be included');
|
||||
strictEqual(result.missingDependencies?.[0], 'bubblewrap', 'The missing dependency should be reported');
|
||||
ok(result.sandboxConfigPath, 'Sandbox config path should still be returned when config creation succeeds');
|
||||
});
|
||||
|
||||
test('should report successful sandbox prereq checks', async () => {
|
||||
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
|
||||
const result = await sandboxService.checkForSandboxingPrereqs();
|
||||
|
||||
strictEqual(result.enabled, true, 'Sandbox should be enabled when prereqs pass');
|
||||
strictEqual(result.failedCheck, undefined, 'No failed check should be reported when prereqs pass');
|
||||
strictEqual(result.missingDependencies, undefined, 'Missing dependencies should be omitted when prereqs pass');
|
||||
ok(result.sandboxConfigPath, 'Sandbox config path should be returned when prereqs pass');
|
||||
});
|
||||
|
||||
test('should filter out sole wildcard (*) from trusted domains', async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { TestInstantiationService } from '../../../../../../platform/instan
|
||||
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
|
||||
import { CommandLineSandboxRewriter } from '../../browser/tools/commandLineRewriter/commandLineSandboxRewriter.js';
|
||||
import type { ICommandLineRewriterOptions } from '../../browser/tools/commandLineRewriter/commandLineRewriter.js';
|
||||
import { ITerminalSandboxService } from '../../common/terminalSandboxService.js';
|
||||
import { ITerminalSandboxService, TerminalSandboxPrerequisiteCheck } from '../../common/terminalSandboxService.js';
|
||||
|
||||
suite('CommandLineSandboxRewriter', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -24,6 +24,7 @@ suite('CommandLineSandboxRewriter', () => {
|
||||
isEnabled: async () => false,
|
||||
wrapCommand: (command, _requestUnsandboxedExecution) => command,
|
||||
getSandboxConfigPath: async () => '/tmp/sandbox.json',
|
||||
checkForSandboxingPrereqs: async () => ({ enabled: false, sandboxConfigPath: undefined, failedCheck: TerminalSandboxPrerequisiteCheck.Config }),
|
||||
getTempDir: () => undefined,
|
||||
setNeedsForceUpdateConfigFile: () => { },
|
||||
...overrides
|
||||
@@ -48,9 +49,23 @@ suite('CommandLineSandboxRewriter', () => {
|
||||
|
||||
test('returns undefined when sandbox config is unavailable', async () => {
|
||||
stubSandboxService({
|
||||
isEnabled: async () => true,
|
||||
wrapCommand: command => `wrapped:${command}`,
|
||||
getSandboxConfigPath: async () => undefined,
|
||||
checkForSandboxingPrereqs: async () => ({ enabled: false, sandboxConfigPath: undefined, failedCheck: TerminalSandboxPrerequisiteCheck.Config }),
|
||||
});
|
||||
|
||||
const rewriter = store.add(instantiationService.createInstance(CommandLineSandboxRewriter));
|
||||
const result = await rewriter.rewrite(createRewriteOptions('echo hello'));
|
||||
strictEqual(result, undefined);
|
||||
});
|
||||
|
||||
test('returns undefined when sandbox dependencies are unavailable', async () => {
|
||||
stubSandboxService({
|
||||
checkForSandboxingPrereqs: async () => ({
|
||||
enabled: false,
|
||||
sandboxConfigPath: '/tmp/sandbox.json',
|
||||
failedCheck: TerminalSandboxPrerequisiteCheck.Dependencies,
|
||||
missingDependencies: ['bubblewrap'],
|
||||
}),
|
||||
});
|
||||
|
||||
const rewriter = store.add(instantiationService.createInstance(CommandLineSandboxRewriter));
|
||||
@@ -61,14 +76,13 @@ suite('CommandLineSandboxRewriter', () => {
|
||||
test('wraps command when sandbox is enabled and config exists', async () => {
|
||||
const calls: string[] = [];
|
||||
stubSandboxService({
|
||||
isEnabled: async () => true,
|
||||
wrapCommand: (command, _requestUnsandboxedExecution) => {
|
||||
calls.push('wrapCommand');
|
||||
return `wrapped:${command}`;
|
||||
},
|
||||
getSandboxConfigPath: async () => {
|
||||
calls.push('getSandboxConfigPath');
|
||||
return '/tmp/sandbox.json';
|
||||
checkForSandboxingPrereqs: async () => {
|
||||
calls.push('checkForSandboxingPrereqs');
|
||||
return { enabled: true, sandboxConfigPath: '/tmp/sandbox.json', failedCheck: undefined };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -76,20 +90,19 @@ 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, ['getSandboxConfigPath', 'wrapCommand']);
|
||||
deepStrictEqual(calls, ['checkForSandboxingPrereqs', 'wrapCommand']);
|
||||
});
|
||||
|
||||
test('wraps command and forwards sandbox bypass flag when explicitly requested', async () => {
|
||||
const calls: string[] = [];
|
||||
stubSandboxService({
|
||||
isEnabled: async () => true,
|
||||
wrapCommand: (command, requestUnsandboxedExecution) => {
|
||||
calls.push(`wrap:${command}:${String(requestUnsandboxedExecution)}`);
|
||||
return `wrapped:${command}`;
|
||||
},
|
||||
getSandboxConfigPath: async () => {
|
||||
calls.push('config');
|
||||
return '/tmp/sandbox.json';
|
||||
checkForSandboxingPrereqs: async () => {
|
||||
calls.push('prereqs');
|
||||
return { enabled: true, sandboxConfigPath: '/tmp/sandbox.json', failedCheck: undefined };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -101,6 +114,6 @@ suite('CommandLineSandboxRewriter', () => {
|
||||
|
||||
strictEqual(result?.rewritten, 'wrapped:echo hello');
|
||||
strictEqual(result?.reasoning, 'Wrapped command for sandbox execution');
|
||||
deepStrictEqual(calls, ['config', 'wrap:echo hello:true']);
|
||||
deepStrictEqual(calls, ['prereqs', 'wrap:echo hello:true']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { FileService } from '../../../../../../platform/files/common/fileService
|
||||
import type { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { NullLogService } from '../../../../../../platform/log/common/log.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
|
||||
import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js';
|
||||
import { IWorkspaceContextService, toWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js';
|
||||
import { Workspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js';
|
||||
@@ -34,7 +35,7 @@ import { IChatService, type IChatTerminalToolInvocationData } from '../../../../
|
||||
import { IChatWidgetService } from '../../../../chat/browser/chat.js';
|
||||
import { ChatPermissionLevel } from '../../../../chat/common/constants.js';
|
||||
import { LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js';
|
||||
import { ITerminalSandboxService } from '../../common/terminalSandboxService.js';
|
||||
import { ITerminalSandboxService, TerminalSandboxPrerequisiteCheck, type ITerminalSandboxPrerequisiteCheckResult } from '../../common/terminalSandboxService.js';
|
||||
import { ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocationPreparationContext, ToolDataSource, ToolSet, type ToolConfirmationAction } from '../../../../chat/common/tools/languageModelToolsService.js';
|
||||
import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js';
|
||||
import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js';
|
||||
@@ -78,7 +79,9 @@ suite('RunInTerminalTool', () => {
|
||||
let chatServiceDisposeEmitter: Emitter<{ sessionResources: URI[]; reason: 'cleared' }>;
|
||||
let chatSessionArchivedEmitter: Emitter<IAgentSession>;
|
||||
let sandboxEnabled: boolean;
|
||||
let sandboxPrereqResult: ITerminalSandboxPrerequisiteCheckResult;
|
||||
let terminalSandboxService: ITerminalSandboxService;
|
||||
let createdTerminalInstance: ITerminalInstance;
|
||||
|
||||
let runInTerminalTool: TestRunInTerminalTool;
|
||||
|
||||
@@ -94,6 +97,36 @@ suite('RunInTerminalTool', () => {
|
||||
setConfig(TerminalChatAgentToolsSettingId.EnableAutoApprove, true);
|
||||
setConfig(TerminalChatAgentToolsSettingId.BlockDetectedFileWrites, 'outsideWorkspace');
|
||||
sandboxEnabled = false;
|
||||
sandboxPrereqResult = {
|
||||
enabled: false,
|
||||
sandboxConfigPath: undefined,
|
||||
failedCheck: undefined,
|
||||
};
|
||||
|
||||
const commandFinishedEmitter = new Emitter<{ exitCode: number | undefined }>();
|
||||
const onDisposedEmitter = new Emitter<ITerminalInstance>();
|
||||
const onDidAddCapabilityEmitter = new Emitter<{ id: TerminalCapability }>();
|
||||
const onDidInputDataEmitter = new Emitter<string>();
|
||||
createdTerminalInstance = {
|
||||
sendText: async (_text: string) => {
|
||||
// Simulate successful command completion after sendText
|
||||
queueMicrotask(() => commandFinishedEmitter.fire({ exitCode: 0 }));
|
||||
},
|
||||
focus: () => { },
|
||||
capabilities: {
|
||||
get: (cap: TerminalCapability) => {
|
||||
if (cap === TerminalCapability.CommandDetection) {
|
||||
return {
|
||||
onCommandFinished: commandFinishedEmitter.event,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
onDidAddCapability: onDidAddCapabilityEmitter.event,
|
||||
},
|
||||
onDidInputData: onDidInputDataEmitter.event,
|
||||
onDisposed: onDisposedEmitter.event,
|
||||
} as unknown as ITerminalInstance;
|
||||
terminalServiceDisposeEmitter = new Emitter<ITerminalInstance>();
|
||||
chatServiceDisposeEmitter = new Emitter<{ sessionResources: URI[]; reason: 'cleared' }>();
|
||||
chatSessionArchivedEmitter = new Emitter<IAgentSession>();
|
||||
@@ -123,10 +156,18 @@ suite('RunInTerminalTool', () => {
|
||||
isEnabled: async () => sandboxEnabled,
|
||||
wrapCommand: (command: string, requestUnsandboxedExecution?: boolean) => requestUnsandboxedExecution ? `unsandboxed:${command}` : `sandbox:${command}`,
|
||||
getSandboxConfigPath: async () => sandboxEnabled ? '/tmp/sandbox.json' : undefined,
|
||||
checkForSandboxingPrereqs: async () => sandboxPrereqResult,
|
||||
getTempDir: () => undefined,
|
||||
setNeedsForceUpdateConfigFile: () => { },
|
||||
getOS: async () => OperatingSystem.Linux,
|
||||
getResolvedNetworkDomains: () => ({ allowedDomains: [], deniedDomains: [] }),
|
||||
getMissingSandboxDependencies: async () => [],
|
||||
installMissingSandboxDependencies: async (missingDependencies, _sessionResource, _token, options) => {
|
||||
const terminal = await options.createTerminal();
|
||||
await options.focusTerminal(terminal);
|
||||
await terminal.sendText(`sudo apt install -y ${missingDependencies.join(' ')}`, true);
|
||||
return { exitCode: 0 };
|
||||
},
|
||||
};
|
||||
instantiationService.stub(ITerminalSandboxService, terminalSandboxService);
|
||||
|
||||
@@ -140,7 +181,10 @@ suite('RunInTerminalTool', () => {
|
||||
},
|
||||
});
|
||||
instantiationService.stub(ITerminalService, {
|
||||
createTerminal: async () => createdTerminalInstance,
|
||||
onDidDisposeInstance: terminalServiceDisposeEmitter.event,
|
||||
revealTerminal: async () => { },
|
||||
setActiveInstance: () => { },
|
||||
setNextCommandId: async () => { }
|
||||
});
|
||||
instantiationService.stub(ITerminalProfileResolverService, {
|
||||
@@ -252,6 +296,11 @@ suite('RunInTerminalTool', () => {
|
||||
ok(!propertiesBefore?.['requestUnsandboxedExecution'], 'Expected no requestUnsandboxedExecution before enabling sandbox');
|
||||
|
||||
sandboxEnabled = true;
|
||||
sandboxPrereqResult = {
|
||||
enabled: true,
|
||||
sandboxConfigPath: '/tmp/sandbox.json',
|
||||
failedCheck: undefined,
|
||||
};
|
||||
|
||||
const toolDataAfter = await instantiationService.invokeFunction(createRunInTerminalToolData);
|
||||
const propertiesAfter = toolDataAfter.inputSchema?.properties as Record<string, object> | undefined;
|
||||
@@ -259,6 +308,29 @@ suite('RunInTerminalTool', () => {
|
||||
ok(toolDataAfter.modelDescription?.includes('Sandboxing:'), 'Expected sandbox instructions in description after enabling sandbox');
|
||||
});
|
||||
|
||||
test('should show confirmation to install missing sandbox dependencies when prereq check fails', async () => {
|
||||
sandboxEnabled = false;
|
||||
sandboxPrereqResult = {
|
||||
enabled: false,
|
||||
sandboxConfigPath: '/tmp/sandbox.json',
|
||||
failedCheck: TerminalSandboxPrerequisiteCheck.Dependencies,
|
||||
missingDependencies: ['bubblewrap'],
|
||||
};
|
||||
|
||||
const result = await executeToolTest({
|
||||
command: 'echo hello',
|
||||
explanation: 'Print hello',
|
||||
goal: 'Print hello'
|
||||
});
|
||||
|
||||
// The tool should return confirmation messages for the user
|
||||
ok(result, 'Expected prepared invocation to be defined');
|
||||
ok(result?.confirmationMessages, 'Expected confirmationMessages when deps are missing');
|
||||
ok(result?.confirmationMessages?.customButtons?.length === 2, 'Expected two custom buttons');
|
||||
// missingDependencies should be in toolSpecificData so invoke can handle it
|
||||
strictEqual((result?.toolSpecificData as IChatTerminalToolInvocationData | undefined)?.missingSandboxDependencies?.length, 1);
|
||||
});
|
||||
|
||||
test('should include allowed and denied network domains in model description', async () => {
|
||||
sandboxEnabled = true;
|
||||
terminalSandboxService.getResolvedNetworkDomains = () => ({
|
||||
@@ -286,8 +358,12 @@ suite('RunInTerminalTool', () => {
|
||||
});
|
||||
|
||||
test('should use sandbox labels when command is sandbox wrapped', async () => {
|
||||
terminalSandboxService.isEnabled = async () => true;
|
||||
terminalSandboxService.getSandboxConfigPath = async () => '/tmp/vscode-sandbox-settings.json';
|
||||
sandboxEnabled = true;
|
||||
sandboxPrereqResult = {
|
||||
enabled: true,
|
||||
sandboxConfigPath: '/tmp/vscode-sandbox-settings.json',
|
||||
failedCheck: undefined,
|
||||
};
|
||||
terminalSandboxService.wrapCommand = (command: string) => `sandbox-runtime ${command}`;
|
||||
|
||||
const preparedInvocation = await executeToolTest({ command: 'echo hello' });
|
||||
@@ -540,6 +616,11 @@ suite('RunInTerminalTool', () => {
|
||||
suite('sandbox bypass requests', () => {
|
||||
test('should force confirmation for explicit unsandboxed execution requests', async () => {
|
||||
sandboxEnabled = true;
|
||||
sandboxPrereqResult = {
|
||||
enabled: true,
|
||||
sandboxConfigPath: '/tmp/sandbox.json',
|
||||
failedCheck: undefined,
|
||||
};
|
||||
runInTerminalTool.setBackendOs(OperatingSystem.Linux);
|
||||
|
||||
const result = await executeToolTest({
|
||||
@@ -1949,10 +2030,13 @@ suite('ChatAgentToolsContribution - tool registration refresh', () => {
|
||||
isEnabled: async () => sandboxEnabled,
|
||||
wrapCommand: (command: string) => `sandbox:${command}`,
|
||||
getSandboxConfigPath: async () => sandboxEnabled ? '/tmp/sandbox.json' : undefined,
|
||||
checkForSandboxingPrereqs: async () => ({ enabled: sandboxEnabled, sandboxConfigPath: sandboxEnabled ? '/tmp/sandbox.json' : undefined, failedCheck: undefined }),
|
||||
getTempDir: () => undefined,
|
||||
setNeedsForceUpdateConfigFile: () => { },
|
||||
getOS: async () => OperatingSystem.Linux,
|
||||
getResolvedNetworkDomains: () => ({ allowedDomains: [], deniedDomains: [] }),
|
||||
getMissingSandboxDependencies: async () => [],
|
||||
installMissingSandboxDependencies: async () => ({ exitCode: 0 }),
|
||||
};
|
||||
instantiationService.stub(ITerminalSandboxService, terminalSandboxService);
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ import './services/extensions/electron-browser/nativeExtensionService.js';
|
||||
import '../platform/userDataProfile/electron-browser/userDataProfileStorageService.js';
|
||||
import './services/auxiliaryWindow/electron-browser/auxiliaryWindowService.js';
|
||||
import '../platform/extensionManagement/electron-browser/extensionsProfileScannerService.js';
|
||||
import '../platform/sandbox/electron-browser/sandboxHelperService.js';
|
||||
import '../platform/webContentExtractor/electron-browser/webContentExtractorService.js';
|
||||
import '../platform/agentHost/electron-browser/agentHostService.js';
|
||||
import './services/browserView/electron-browser/playwrightWorkbenchService.js';
|
||||
|
||||
@@ -72,6 +72,7 @@ import '../platform/extensionResourceLoader/browser/extensionResourceLoaderServi
|
||||
import './services/auxiliaryWindow/browser/auxiliaryWindowService.js';
|
||||
import './services/browserElements/browser/webBrowserElementsService.js';
|
||||
import './services/power/browser/powerService.js';
|
||||
import '../platform/sandbox/browser/sandboxHelperService.js';
|
||||
|
||||
import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js';
|
||||
import { IAccessibilityService } from '../platform/accessibility/common/accessibility.js';
|
||||
|
||||
Reference in New Issue
Block a user