mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 23:35:54 +01:00
tmp directory should not be used by sandboxed commands (#303699)
* Fix terminal sandbox tmp handling and upgrade sandbox runtime Fixes #299224 Fixes #303568 * fixing test * merging changes
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.23",
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.42",
|
||||
"@github/copilot": "^1.0.4-0",
|
||||
"@github/copilot-sdk": "^0.1.32",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
@@ -419,15 +419,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sandbox-runtime": {
|
||||
"version": "0.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.23.tgz",
|
||||
"integrity": "sha512-Np0VRH6D71cGoJZvd8hCz1LMfwg9ERJovrOJSCz5aSQSQJPWPNIFPV1wfc8oAhJpStOuYkot+EmXOkRRxuGMCQ==",
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.42.tgz",
|
||||
"integrity": "sha512-kJpuhU4hHMumeygIkKvNhscEsTtQK1sat1kZwhb6HLYBznwjMGOdnuBI/RM9HeFwxArn71/ciD2WJbxttXBMHw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pondwader/socks5-server": "^1.0.10",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"commander": "^12.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-es": "^4.17.23",
|
||||
"shell-quote": "^1.8.3",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"install-latest-component-explorer": "npm install @vscode/component-explorer@next @vscode/component-explorer-cli@next && cd build/vite && npm install @vscode/component-explorer-vite-plugin@next && npm install @vscode/component-explorer@next"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.23",
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.42",
|
||||
"@github/copilot": "^1.0.4-0",
|
||||
"@github/copilot-sdk": "^0.1.32",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
|
||||
10
remote/package-lock.json
generated
10
remote/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "vscode-reh",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.23",
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.42",
|
||||
"@github/copilot": "^1.0.4-0",
|
||||
"@github/copilot-sdk": "^0.1.32",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
@@ -52,15 +52,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sandbox-runtime": {
|
||||
"version": "0.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.23.tgz",
|
||||
"integrity": "sha512-Np0VRH6D71cGoJZvd8hCz1LMfwg9ERJovrOJSCz5aSQSQJPWPNIFPV1wfc8oAhJpStOuYkot+EmXOkRRxuGMCQ==",
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.42.tgz",
|
||||
"integrity": "sha512-kJpuhU4hHMumeygIkKvNhscEsTtQK1sat1kZwhb6HLYBznwjMGOdnuBI/RM9HeFwxArn71/ciD2WJbxttXBMHw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pondwader/socks5-server": "^1.0.10",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"commander": "^12.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-es": "^4.17.23",
|
||||
"shell-quote": "^1.8.3",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.23",
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.42",
|
||||
"@github/copilot": "^1.0.4-0",
|
||||
"@github/copilot-sdk": "^0.1.32",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
|
||||
@@ -151,6 +151,8 @@ Background Processes:
|
||||
parts.push(`
|
||||
Sandboxing:
|
||||
- ATTENTION: Terminal sandboxing is enabled, commands run in a sandbox by default
|
||||
- When executing commands within the sandboxed environment, all operations requiring a temporary directory must utilize the $TMPDIR environment variable. The /tmp directory is not guaranteed to be accessible or writable and must be avoided
|
||||
- Tools and scripts should respect the TMPDIR environment variable, which is automatically set to an appropriate path within the sandbox
|
||||
- When a command fails due to sandbox restrictions, immediately re-run it with requestUnsandboxedExecution=true and prompt the user to bypass the sandbox
|
||||
- Only set requestUnsandboxedExecution=true when there is evidence of failures caused by the sandbox, e.g. 'Operation not permitted' errors, network failures, or file access errors, etc
|
||||
- When setting requestUnsandboxedExecution=true, also provide requestUnsandboxedExecutionReason; the user will be prompted before it runs unsandboxed`);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { dirname, posix, win32 } from '../../../../../base/common/path.js';
|
||||
import { OperatingSystem, OS } from '../../../../../base/common/platform.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
import { localize } from '../../../../../nls.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';
|
||||
@@ -22,6 +23,8 @@ import { TerminalChatAgentToolsSettingId } from './terminalChatAgentToolsConfigu
|
||||
import { IRemoteAgentEnvironment } from '../../../../../platform/remote/common/remoteAgentEnvironment.js';
|
||||
import { ITrustedDomainService } from '../../../url/common/trustedDomainService.js';
|
||||
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';
|
||||
|
||||
export const ITerminalSandboxService = createDecorator<ITerminalSandboxService>('terminalSandboxService');
|
||||
|
||||
@@ -50,6 +53,7 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
private _appRoot: string;
|
||||
private _os: OperatingSystem = OS;
|
||||
private _defaultWritePaths: string[] = ['~/.npm'];
|
||||
private static readonly _sandboxTempDirName = 'tmp';
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@@ -59,6 +63,8 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
|
||||
@ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
) {
|
||||
super();
|
||||
this._appRoot = dirname(FileAccess.asFileUri('').path);
|
||||
@@ -87,6 +93,17 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
this._register(this._workspaceContextService.onDidChangeWorkspaceFolders(() => {
|
||||
this.setNeedsForceUpdateConfigFile();
|
||||
}));
|
||||
|
||||
this._register(this._lifecycleService.onWillShutdown(e => {
|
||||
if (!this._tempDir) {
|
||||
return;
|
||||
}
|
||||
e.join(this._cleanupSandboxTempDir(), {
|
||||
id: 'join.deleteFilesInSandboxTempDir',
|
||||
label: localize('deleteFilesInSandboxTempDir', "Delete Files in Sandbox Temp Dir"),
|
||||
order: WillShutdownJoinerOrder.Default
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public async isEnabled(): Promise<boolean> {
|
||||
@@ -119,9 +136,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
// 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)}`;
|
||||
const wrappedCommand = `PATH="$PATH:${dirname(this._rgPath)}" TMPDIR="${this._tempDir.path}" CLAUDE_TMPDIR="${this._tempDir.path}" "${this._srtPath}" --settings "${this._sandboxConfigPath}" -c ${this._quoteShellArgument(command)}`;
|
||||
if (this._remoteEnvDetails) {
|
||||
return `${wrappedCommand}`;
|
||||
return `"${this._execPath}" ${wrappedCommand}`;
|
||||
}
|
||||
return `ELECTRON_RUN_AS_NODE=1 ${wrappedCommand}`;
|
||||
}
|
||||
@@ -212,13 +229,9 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
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;
|
||||
}
|
||||
this._tempDir = this._getSandboxTempDirPath(remoteEnv);
|
||||
if (this._tempDir) {
|
||||
await this._fileService.createFolder(this._tempDir);
|
||||
this._defaultWritePaths.push(this._tempDir.path);
|
||||
}
|
||||
if (!this._tempDir) {
|
||||
@@ -227,6 +240,30 @@ export class TerminalSandboxService extends Disposable implements ITerminalSandb
|
||||
}
|
||||
}
|
||||
|
||||
private async _cleanupSandboxTempDir(): Promise<void> {
|
||||
if (!this._tempDir) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this._fileService.del(this._tempDir, { recursive: true, useTrash: false });
|
||||
} catch (error) {
|
||||
this._logService.warn('TerminalSandboxService: Failed to delete sandbox temp dir', error);
|
||||
}
|
||||
}
|
||||
|
||||
private _getSandboxTempDirPath(remoteEnv: IRemoteAgentEnvironment | null): URI | undefined {
|
||||
if (remoteEnv?.userHome) {
|
||||
return URI.joinPath(remoteEnv.userHome, this._productService.serverDataFolderName ?? this._productService.dataFolderName, TerminalSandboxService._sandboxTempDirName);
|
||||
}
|
||||
|
||||
const nativeEnv = this._environmentService as IEnvironmentService & { userHome?: URI };
|
||||
if (nativeEnv.userHome) {
|
||||
return URI.joinPath(nativeEnv.userHome, this._productService.dataFolderName, TerminalSandboxService._sandboxTempDirName);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _addTrustedDomainsToAllowedDomains(allowedDomains: string[]): string[] {
|
||||
const allowedDomainsSet = new Set(allowedDomains);
|
||||
for (const domain of this._trustedDomainService.trustedDomains) {
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
import { strictEqual, ok } from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
|
||||
import { TestLifecycleService, workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
|
||||
import { TestProductService } from '../../../../../test/common/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 { IProductService } from '../../../../../../platform/product/common/productService.js';
|
||||
import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js';
|
||||
import { ITrustedDomainService } from '../../../../url/common/trustedDomainService.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
@@ -23,6 +25,7 @@ import { OperatingSystem } from '../../../../../../base/common/platform.js';
|
||||
import { IRemoteAgentEnvironment } from '../../../../../../platform/remote/common/remoteAgentEnvironment.js';
|
||||
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';
|
||||
|
||||
suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -31,8 +34,12 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
let configurationService: TestConfigurationService;
|
||||
let trustedDomainService: MockTrustedDomainService;
|
||||
let fileService: MockFileService;
|
||||
let lifecycleService: TestLifecycleService;
|
||||
let workspaceContextService: MockWorkspaceContextService;
|
||||
let productService: IProductService;
|
||||
let createdFiles: Map<string, string>;
|
||||
let createdFolders: string[];
|
||||
let deletedFolders: string[];
|
||||
|
||||
class MockTrustedDomainService implements ITrustedDomainService {
|
||||
_serviceBrand: undefined;
|
||||
@@ -50,6 +57,15 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
createdFiles.set(uri.path, contentString);
|
||||
return {};
|
||||
}
|
||||
|
||||
async createFolder(uri: URI): Promise<any> {
|
||||
createdFolders.push(uri.path);
|
||||
return {};
|
||||
}
|
||||
|
||||
async del(uri: URI): Promise<void> {
|
||||
deletedFolders.push(uri.path);
|
||||
}
|
||||
}
|
||||
|
||||
class MockRemoteAgentService {
|
||||
@@ -132,11 +148,19 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
|
||||
setup(() => {
|
||||
createdFiles = new Map();
|
||||
createdFolders = [];
|
||||
deletedFolders = [];
|
||||
instantiationService = workbenchInstantiationService({}, store);
|
||||
configurationService = new TestConfigurationService();
|
||||
trustedDomainService = new MockTrustedDomainService();
|
||||
fileService = new MockFileService();
|
||||
lifecycleService = store.add(new TestLifecycleService());
|
||||
workspaceContextService = new MockWorkspaceContextService();
|
||||
productService = {
|
||||
...TestProductService,
|
||||
dataFolderName: '.test-data',
|
||||
serverDataFolderName: '.test-server-data'
|
||||
};
|
||||
workspaceContextService.setWorkspaceFolders([URI.file('/workspace-one')]);
|
||||
|
||||
// Setup default configuration
|
||||
@@ -155,9 +179,11 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
execPath: '/usr/bin/node'
|
||||
});
|
||||
instantiationService.stub(ILogService, new NullLogService());
|
||||
instantiationService.stub(IProductService, productService);
|
||||
instantiationService.stub(IRemoteAgentService, new MockRemoteAgentService());
|
||||
instantiationService.stub(ITrustedDomainService, trustedDomainService);
|
||||
instantiationService.stub(IWorkspaceContextService, workspaceContextService);
|
||||
instantiationService.stub(ILifecycleService, lifecycleService);
|
||||
});
|
||||
|
||||
test('should filter out sole wildcard (*) from trusted domains', async () => {
|
||||
@@ -341,6 +367,28 @@ suite('TerminalSandboxService - allowTrustedDomains', () => {
|
||||
ok(refreshedConfig.filesystem.allowWrite.includes('/configured/path'), 'Refreshed config should preserve configured allowWrite paths');
|
||||
});
|
||||
|
||||
test('should create sandbox temp dir under the server data folder', async () => {
|
||||
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
|
||||
const configPath = await sandboxService.getSandboxConfigPath();
|
||||
const expectedTempDir = URI.joinPath(URI.file('/home/user'), productService.serverDataFolderName ?? productService.dataFolderName, 'tmp');
|
||||
|
||||
strictEqual(sandboxService.getTempDir()?.path, expectedTempDir.path, 'Sandbox temp dir should live under the server data folder');
|
||||
strictEqual(createdFolders[0], expectedTempDir.path, 'Sandbox temp dir should be created before writing the config');
|
||||
ok(configPath?.startsWith(expectedTempDir.path), 'Sandbox config file should be written inside the sandbox temp dir');
|
||||
});
|
||||
|
||||
test('should delete sandbox temp dir on shutdown', async () => {
|
||||
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
|
||||
await sandboxService.getSandboxConfigPath();
|
||||
const expectedTempDir = URI.joinPath(URI.file('/home/user'), productService.serverDataFolderName ?? productService.dataFolderName, 'tmp');
|
||||
|
||||
lifecycleService.fireShutdown();
|
||||
await Promise.all(lifecycleService.shutdownJoiners);
|
||||
|
||||
strictEqual(lifecycleService.shutdownJoiners.length, 1, 'Shutdown should register a temp-dir cleanup joiner');
|
||||
strictEqual(deletedFolders[0], expectedTempDir.path, 'Shutdown should delete the sandbox temp dir');
|
||||
});
|
||||
|
||||
test('should add ripgrep bin directory to PATH when wrapping command', async () => {
|
||||
const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));
|
||||
await sandboxService.getSandboxConfigPath();
|
||||
|
||||
@@ -36,7 +36,7 @@ import { ITerminalSandboxService } from '../../common/terminalSandboxService.js'
|
||||
import { ILanguageModelToolsService, IPreparedToolInvocation, IToolInvocationPreparationContext, 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';
|
||||
import { RunInTerminalTool, type IRunInTerminalInputParams } from '../../browser/tools/runInTerminalTool.js';
|
||||
import { createRunInTerminalToolData, RunInTerminalTool, type IRunInTerminalInputParams } from '../../browser/tools/runInTerminalTool.js';
|
||||
import { ShellIntegrationQuality } from '../../browser/toolTerminalCreator.js';
|
||||
import { terminalChatAgentToolsConfiguration, TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';
|
||||
import { TerminalChatService } from '../../../chat/browser/terminalChatService.js';
|
||||
@@ -204,6 +204,15 @@ suite('RunInTerminalTool', () => {
|
||||
}
|
||||
|
||||
suite('sandbox invocation messaging', () => {
|
||||
test('should instruct models to use $TMPDIR instead of /tmp when sandboxed', async () => {
|
||||
sandboxEnabled = true;
|
||||
|
||||
const toolData = await instantiationService.invokeFunction(createRunInTerminalToolData);
|
||||
|
||||
ok(toolData.modelDescription?.includes('must utilize the $TMPDIR environment variable'), 'Expected sandboxed tool description to require $TMPDIR usage');
|
||||
ok(toolData.modelDescription?.includes('The /tmp directory is not guaranteed to be accessible or writable and must be avoided'), 'Expected sandboxed tool description to discourage /tmp usage');
|
||||
});
|
||||
|
||||
test('should use sandbox labels when command is sandbox wrapped', async () => {
|
||||
terminalSandboxService.isEnabled = async () => true;
|
||||
terminalSandboxService.getSandboxConfigPath = async () => '/tmp/vscode-sandbox-settings.json';
|
||||
|
||||
Reference in New Issue
Block a user