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:
dileepyavan
2026-03-21 02:20:44 -07:00
committed by GitHub
parent a1254fd4c2
commit b978bf74b2
8 changed files with 118 additions and 22 deletions

10
package-lock.json generated
View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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`);

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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';