Updates for windows sandboxing (#323062)

This commit is contained in:
dileepyavan
2026-06-26 13:18:47 -07:00
committed by GitHub
parent 6d7542b870
commit be5b7e42b7
9 changed files with 105 additions and 52 deletions
+4 -4
View File
@@ -20,7 +20,7 @@
"@microsoft/dev-tunnels-management": "^1.3.41",
"@microsoft/dev-tunnels-ssh": "^3.12.22",
"@microsoft/dev-tunnels-ssh-tcp": "^3.12.22",
"@microsoft/mxc-sdk": "0.6.0",
"@microsoft/mxc-sdk": "0.6.1",
"@parcel/watcher": "^2.5.6",
"@types/semver": "^7.5.8",
"@vscode/codicons": "^0.0.46-21",
@@ -1911,9 +1911,9 @@
"integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ=="
},
"node_modules/@microsoft/mxc-sdk": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@microsoft/mxc-sdk/-/mxc-sdk-0.6.0.tgz",
"integrity": "sha512-O+cKLjO4mE/D4dDp2GmVJ8hAj43vQHLf1YTMUWUtU4+41ddThhb1SYkn6W9b3FLl63bJW/4dqReJG6PIBk8jqQ==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@microsoft/mxc-sdk/-/mxc-sdk-0.6.1.tgz",
"integrity": "sha512-jpbJU/xfF4qLWcNMplDTUX/q13m2A6vYao1QN3lkZaQlzsRce95H+iU0Qu0wlweJZ2gx6eY1PRQU+/bnQki/dw==",
"license": "MIT",
"dependencies": {
"node-pty": "^1.2.0-beta.12",
+1 -1
View File
@@ -104,7 +104,7 @@
"@microsoft/dev-tunnels-management": "^1.3.41",
"@microsoft/dev-tunnels-ssh": "^3.12.22",
"@microsoft/dev-tunnels-ssh-tcp": "^3.12.22",
"@microsoft/mxc-sdk": "0.6.0",
"@microsoft/mxc-sdk": "0.6.1",
"@parcel/watcher": "^2.5.6",
"@types/semver": "^7.5.8",
"@vscode/codicons": "^0.0.46-21",
+4 -4
View File
@@ -12,7 +12,7 @@
"@github/copilot-sdk": "^1.0.4",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/mxc-sdk": "0.6.0",
"@microsoft/mxc-sdk": "0.6.1",
"@parcel/watcher": "^2.5.6",
"@vscode/copilot-api": "^0.4.2",
"@vscode/deviceid": "^0.1.1",
@@ -287,9 +287,9 @@
"integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ=="
},
"node_modules/@microsoft/mxc-sdk": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@microsoft/mxc-sdk/-/mxc-sdk-0.6.0.tgz",
"integrity": "sha512-O+cKLjO4mE/D4dDp2GmVJ8hAj43vQHLf1YTMUWUtU4+41ddThhb1SYkn6W9b3FLl63bJW/4dqReJG6PIBk8jqQ==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@microsoft/mxc-sdk/-/mxc-sdk-0.6.1.tgz",
"integrity": "sha512-jpbJU/xfF4qLWcNMplDTUX/q13m2A6vYao1QN3lkZaQlzsRce95H+iU0Qu0wlweJZ2gx6eY1PRQU+/bnQki/dw==",
"license": "MIT",
"dependencies": {
"node-pty": "^1.2.0-beta.12",
+1 -1
View File
@@ -7,7 +7,7 @@
"@github/copilot-sdk": "^1.0.4",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/mxc-sdk": "0.6.0",
"@microsoft/mxc-sdk": "0.6.1",
"@parcel/watcher": "^2.5.6",
"@vscode/copilot-api": "^0.4.2",
"@vscode/deviceid": "^0.1.1",
@@ -59,7 +59,6 @@ export interface IWindowsMxcConfig {
timeout?: number;
};
processContainer?: {
name?: string;
leastPrivilege?: boolean;
capabilities?: string[];
ui?: {
@@ -665,7 +665,6 @@ export class TerminalSandboxEngine extends Disposable {
tempDir: this._tempDir,
schemaVersion: windowsSchemaVersion,
allowNetwork,
networkDomains: this.getResolvedNetworkDomains(),
allowReadPaths,
allowWritePaths,
denyReadPaths,
@@ -928,7 +927,19 @@ export class TerminalSandboxEngine extends Disposable {
private async _resolveFileSystemPaths(paths: string[] | undefined): Promise<string[]> {
const resolvedPaths = await Promise.all((paths ?? []).map(path => this._resolveFileSystemPath(path)));
return [...new Set(resolvedPaths.flat())];
const seenPaths = new Set<string>();
return resolvedPaths.flat().filter(path => {
const comparisonKey = this._getFileSystemPathComparisonKey(path);
if (seenPaths.has(comparisonKey)) {
return false;
}
seenPaths.add(comparisonKey);
return true;
});
}
private _getFileSystemPathComparisonKey(path: string): string {
return this._os === OperatingSystem.Windows ? path.replace(/\//g, '\\').toLowerCase() : path;
}
private async _resolveFileSystemPath(path: string): Promise<string[]> {
@@ -7,7 +7,6 @@ import { win32 } from '../../../base/common/path.js';
import { URI } from '../../../base/common/uri.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import type { IWindowsMxcConfig, IWindowsMxcPolicyContainment, IWindowsMxcSandboxPolicy } from './sandboxHelperService.js';
import type { ITerminalSandboxResolvedNetworkDomains } from './terminalSandboxService.js';
export interface IWindowsMxcConfigOptions {
command: string;
@@ -16,7 +15,6 @@ export interface IWindowsMxcConfigOptions {
tempDir: URI;
schemaVersion?: string;
allowNetwork: boolean;
networkDomains: ITerminalSandboxResolvedNetworkDomains;
allowReadPaths: string[];
allowWritePaths: string[];
denyReadPaths: string[];
@@ -47,8 +45,7 @@ export interface IWindowsMxcTerminalSandboxRuntime {
export class WindowsMxcTerminalSandboxRuntime implements IWindowsMxcTerminalSandboxRuntime {
declare readonly _serviceBrand: undefined;
private readonly _configVersion = '0.4.0-alpha';
private readonly _containerName = 'vscode-terminal-sandbox';
private readonly _configVersion = '0.6.0-alpha';
getExecutablePath(appRoot: string, arch: string | undefined): string {
const binArch = arch === 'arm64' ? 'arm64' : 'x64';
@@ -71,17 +68,17 @@ export class WindowsMxcTerminalSandboxRuntime implements IWindowsMxcTerminalSand
const shell = options.shell
? this._quoteWindowsCommandLineArgument(options.shell)
: 'pwsh.exe';
const commandLine = `${shell} -NoProfile -ExecutionPolicy Bypass -Command ${this._quoteWindowsCommandLineArgument(options.command)}`;
const commandLine = `${shell} -NoProfile -Command ${this._quoteWindowsCommandLineArgument(options.command)}`;
const cwd = options.cwd ? this.toWindowsPath(options.cwd) : tempDirPath;
const policy: IWindowsMxcSandboxPolicy = {
version: options.schemaVersion ?? this._configVersion,
timeoutMs: 0,
filesystem: {
readwritePaths: [...new Set(options.allowWritePaths.map(path => this._normalizeWindowsPath(path)))],
readonlyPaths: [...new Set([tempDirPath, ...(options.shell && win32.isAbsolute(options.shell) ? [win32.dirname(options.shell)] : []), ...options.allowReadPaths].map(path => this._normalizeWindowsPath(path)))],
deniedPaths: [...new Set(options.denyReadPaths.map(path => this._normalizeWindowsPath(path)))],
readwritePaths: options.allowWritePaths.map(path => this._normalizeWindowsPath(path)),
readonlyPaths: [tempDirPath, ...(options.shell && win32.isAbsolute(options.shell) ? [win32.dirname(options.shell)] : []), ...options.allowReadPaths].map(path => this._normalizeWindowsPath(path)),
deniedPaths: options.denyReadPaths.map(path => this._normalizeWindowsPath(path)),
},
network: this._createNetworkPolicy(options.allowNetwork, options.networkDomains),
network: this._createNetworkPolicy(options.allowNetwork),
ui: {
allowWindows: true,
clipboard: 'none',
@@ -89,7 +86,7 @@ export class WindowsMxcTerminalSandboxRuntime implements IWindowsMxcTerminalSand
},
};
const config = await buildSandboxPayload(commandLine, policy, cwd, this._containerName);
const config = await buildSandboxPayload(commandLine, policy, cwd);
if (!config?.process) {
throw new Error('Unable to build Windows MXC sandbox payload');
}
@@ -123,20 +120,10 @@ export class WindowsMxcTerminalSandboxRuntime implements IWindowsMxcTerminalSand
return path.replace(/\//g, '\\');
}
private _createNetworkPolicy(allowNetwork: boolean, networkDomains: ITerminalSandboxResolvedNetworkDomains): NonNullable<IWindowsMxcSandboxPolicy['network']> {
const allowedHosts = networkDomains.allowedDomains.length > 0 ? networkDomains.allowedDomains : undefined;
const blockedHosts = networkDomains.deniedDomains.length > 0 ? networkDomains.deniedDomains : undefined;
const allowOutbound = allowNetwork || !!allowedHosts?.length;
const network: NonNullable<IWindowsMxcSandboxPolicy['network']> = {
allowOutbound,
};
if (allowOutbound && allowedHosts) {
network.allowedHosts = allowedHosts;
}
if (allowOutbound && blockedHosts) {
network.blockedHosts = blockedHosts;
}
return network;
private _createNetworkPolicy(allowNetwork: boolean): NonNullable<IWindowsMxcSandboxPolicy['network']> {
// MXC does not support per-host network policies on Windows. Rely on the
// overall allow/block policy instead of emitting unsupported host lists.
return { allowOutbound: allowNetwork };
}
private _quotePowerShellArgument(value: string): string {
@@ -70,9 +70,7 @@ suite('TerminalSandboxEngine', () => {
const network = {
defaultPolicy: policy.network?.allowOutbound ? 'allow' : 'block' as 'allow' | 'block',
...(policy.network?.allowLocalNetwork !== undefined ? { allowLocalNetwork: policy.network.allowLocalNetwork } : {}),
...(policy.network?.allowedHosts ? { allowedHosts: policy.network.allowedHosts } : {}),
...(policy.network?.blockedHosts ? { blockedHosts: policy.network.blockedHosts } : {}),
...(policy.network ? { enforcementMode: policy.network.allowedHosts?.length || policy.network.blockedHosts?.length ? 'both' as const : 'capabilities' as const } : {}),
...(policy.network ? { enforcementMode: 'capabilities' as const } : {}),
};
return {
version: policy.version,
@@ -88,7 +86,6 @@ suite('TerminalSandboxEngine', () => {
timeout: policy.timeoutMs ?? 0,
},
processContainer: {
name: containerName,
leastPrivilege: false,
capabilities: policy.network?.allowOutbound ? ['internetClient'] : [],
ui: {
@@ -527,10 +524,9 @@ suite('TerminalSandboxEngine', () => {
strictEqual(wrapped.isSandboxWrapped, true);
ok(wrapped.command.startsWith(`& 'C:\\app\\node_modules\\@microsoft\\mxc-sdk\\bin\\x64\\wxc-exec.exe'`), `Expected MXC executable. Actual: ${wrapped.command}`);
ok(wrapped.command.includes(` '${configPath}'`), `Expected wrapped command to pass the MXC config path. Actual: ${wrapped.command}`);
strictEqual(config.version, '0.4.0-alpha');
strictEqual(config.version, '0.6.0-alpha');
strictEqual(config.containment, 'process');
strictEqual(config.processContainer.name, 'vscode-terminal-sandbox');
strictEqual(config.process.commandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo hello"');
strictEqual(config.process.commandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -Command "echo hello"');
strictEqual(normalizeWindowsPathForAssert(config.process.cwd), 'c:/workspace');
strictEqual(config.ui.disable, false);
ok(config.process.env.includes('SystemRoot=C:\\Windows'), 'SystemRoot should be injected into the MXC process env');
@@ -579,6 +575,54 @@ suite('TerminalSandboxEngine', () => {
ok(!config.filesystem.deniedPaths.some((path: string) => normalizeWindowsPathForAssert(path) === 'c:/users/user'), 'User home should not be denied by default on Windows');
});
test('deduplicates Windows filesystem paths regardless of case or separator', async () => {
enableWindowsSandbox();
setSandboxSetting(AgentSandboxSettingId.AgentSandboxWindowsFileSystem, {
allowWrite: ['C:/configured/write'],
allowRead: ['C:\\configured\\read'],
denyRead: ['C:/configured/secret', 'c:\\configured\\secret'],
});
const host = createWindowsHost({
getWindowsMxcFilesystemPolicy: () => Promise.resolve({
readwritePaths: ['c:\\configured\\write'],
readonlyPaths: ['c:/configured/read'],
}),
});
const engine = store.add(instantiationService.createInstance(TerminalSandboxEngine, host));
await engine.wrapCommand('echo hello', false, 'pwsh');
const configPath = await engine.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const config = JSON.parse(createdFiles.get(configPath)!);
const matchingPaths = (paths: string[], expectedPath: string) => paths.filter(path => normalizeWindowsPathForAssert(path) === expectedPath);
deepStrictEqual({
readwrite: matchingPaths(config.filesystem.readwritePaths, 'c:/configured/write'),
readonly: matchingPaths(config.filesystem.readonlyPaths, 'c:/configured/read'),
denied: matchingPaths(config.filesystem.deniedPaths, 'c:/configured/secret'),
}, {
readwrite: ['C:\\configured\\write'],
readonly: ['C:\\configured\\read'],
denied: ['C:\\configured\\secret'],
});
});
test('deduplicates resolved Windows paths regardless of case or separator', async () => {
enableWindowsSandbox();
const engine = store.add(instantiationService.createInstance(TerminalSandboxEngine, createWindowsHost()));
await engine.getOS();
const resolveFileSystemPaths = (engine as unknown as { _resolveFileSystemPaths(paths: string[]): Promise<string[]> })._resolveFileSystemPaths.bind(engine);
deepStrictEqual(await resolveFileSystemPaths([
'C:/configured/path',
'c:\\configured\\path',
'C:\\configured\\other-path',
]), [
'C:/configured/path',
'C:\\configured\\other-path',
]);
});
test('wrapCommand applies configured Windows MXC schema version', async () => {
enableWindowsSandbox();
setSandboxSetting(AgentSandboxSettingId.AgentSandboxWindowsSchemaVersion, '0.5.0-alpha');
@@ -651,13 +695,13 @@ suite('TerminalSandboxEngine', () => {
let configPath = await engine.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const firstCommandLine = JSON.parse(createdFiles.get(configPath)!).process.commandLine;
strictEqual(firstCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo first"');
strictEqual(firstCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -Command "echo first"');
await engine.wrapCommand('echo second', false, 'C:\\Program Files\\PowerShell\\7\\pwsh.exe');
configPath = await engine.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const secondCommandLine = JSON.parse(createdFiles.get(configPath)!).process.commandLine;
strictEqual(secondCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo second"');
strictEqual(secondCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -Command "echo second"');
});
test('allowNetwork maps to MXC allow network config on Windows', async () => {
@@ -674,6 +718,21 @@ suite('TerminalSandboxEngine', () => {
deepStrictEqual(config.network, { defaultPolicy: 'allow', enforcementMode: 'capabilities' });
});
test('Windows MXC config ignores unsupported network host lists', async () => {
enableWindowsSandbox();
setSandboxSetting(AgentNetworkDomainSettingId.AllowedNetworkDomains, ['example.com']);
setSandboxSetting(AgentNetworkDomainSettingId.DeniedNetworkDomains, ['blocked.example.com']);
const host = createWindowsHost();
const engine = store.add(instantiationService.createInstance(TerminalSandboxEngine, host));
await engine.wrapCommand('curl https://example.com', false, 'pwsh');
const configPath = await engine.getSandboxConfigPath();
ok(configPath, 'Config path should be defined');
const config = JSON.parse(createdFiles.get(configPath)!);
deepStrictEqual(config.network, { defaultPolicy: 'allow', enforcementMode: 'capabilities' });
});
test('uses OS-specific filesystem absolute path detection', async () => {
const linuxEngine = store.add(instantiationService.createInstance(TerminalSandboxEngine, createHost()));
await linuxEngine.getOS();
@@ -225,9 +225,7 @@ suite('TerminalSandboxService - network domains', () => {
},
network: {
defaultPolicy: policy.network?.allowOutbound ? 'allow' : 'block',
...(policy.network ? { enforcementMode: policy.network.allowedHosts?.length || policy.network.blockedHosts?.length ? 'both' : 'capabilities' } : {}),
...(policy.network?.allowedHosts ? { allowedHosts: policy.network.allowedHosts } : {}),
...(policy.network?.blockedHosts ? { blockedHosts: policy.network.blockedHosts } : {}),
...(policy.network ? { enforcementMode: 'capabilities' } : {}),
},
ui: {
disable: !(policy.ui?.allowWindows ?? false),
@@ -1458,10 +1456,9 @@ suite('TerminalSandboxService - network domains', () => {
strictEqual(wrapped.isSandboxWrapped, true);
ok(wrapped.command.includes('node_modules\\@microsoft\\mxc-sdk\\bin\\arm64\\wxc-exec.exe'), `Wrapped command should use the MXC Windows executable. Actual: ${wrapped.command}`);
ok(wrapped.command.includes(configPath), `Wrapped command should pass the MXC config path. Actual: ${wrapped.command}`);
strictEqual(config.version, '0.4.0-alpha');
strictEqual(config.version, '0.6.0-alpha');
strictEqual(config.containment, 'process');
strictEqual(config.processContainer.name, 'vscode-terminal-sandbox');
strictEqual(config.process.commandLine, '"c:\\program files\\powershell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo test"');
strictEqual(config.process.commandLine, '"c:\\program files\\powershell\\7\\pwsh.exe" -NoProfile -Command "echo test"');
strictEqual(config.process.cwd, 'c:\\workspace-one');
ok(config.process.env.includes('SystemRoot=c:\\windows'), 'SystemRoot should be injected into the MXC process env');
ok(config.process.env.includes('PATH=c:\\tools\\node;c:\\windows\\system32'), 'PATH should be injected into the MXC process env');