Skip to content

Commit be5b7e4

Browse files
authored
Updates for windows sandboxing (#323062)
1 parent 6d7542b commit be5b7e4

9 files changed

Lines changed: 105 additions & 52 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
"@microsoft/dev-tunnels-management": "^1.3.41",
105105
"@microsoft/dev-tunnels-ssh": "^3.12.22",
106106
"@microsoft/dev-tunnels-ssh-tcp": "^3.12.22",
107-
"@microsoft/mxc-sdk": "0.6.0",
107+
"@microsoft/mxc-sdk": "0.6.1",
108108
"@parcel/watcher": "^2.5.6",
109109
"@types/semver": "^7.5.8",
110110
"@vscode/codicons": "^0.0.46-21",

remote/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

remote/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"@github/copilot-sdk": "^1.0.4",
88
"@microsoft/1ds-core-js": "^3.2.13",
99
"@microsoft/1ds-post-js": "^3.2.13",
10-
"@microsoft/mxc-sdk": "0.6.0",
10+
"@microsoft/mxc-sdk": "0.6.1",
1111
"@parcel/watcher": "^2.5.6",
1212
"@vscode/copilot-api": "^0.4.2",
1313
"@vscode/deviceid": "^0.1.1",

src/vs/platform/sandbox/common/sandboxHelperService.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export interface IWindowsMxcConfig {
5959
timeout?: number;
6060
};
6161
processContainer?: {
62-
name?: string;
6362
leastPrivilege?: boolean;
6463
capabilities?: string[];
6564
ui?: {

src/vs/platform/sandbox/common/terminalSandboxEngine.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,6 @@ export class TerminalSandboxEngine extends Disposable {
665665
tempDir: this._tempDir,
666666
schemaVersion: windowsSchemaVersion,
667667
allowNetwork,
668-
networkDomains: this.getResolvedNetworkDomains(),
669668
allowReadPaths,
670669
allowWritePaths,
671670
denyReadPaths,
@@ -928,7 +927,19 @@ export class TerminalSandboxEngine extends Disposable {
928927

929928
private async _resolveFileSystemPaths(paths: string[] | undefined): Promise<string[]> {
930929
const resolvedPaths = await Promise.all((paths ?? []).map(path => this._resolveFileSystemPath(path)));
931-
return [...new Set(resolvedPaths.flat())];
930+
const seenPaths = new Set<string>();
931+
return resolvedPaths.flat().filter(path => {
932+
const comparisonKey = this._getFileSystemPathComparisonKey(path);
933+
if (seenPaths.has(comparisonKey)) {
934+
return false;
935+
}
936+
seenPaths.add(comparisonKey);
937+
return true;
938+
});
939+
}
940+
941+
private _getFileSystemPathComparisonKey(path: string): string {
942+
return this._os === OperatingSystem.Windows ? path.replace(/\//g, '\\').toLowerCase() : path;
932943
}
933944

934945
private async _resolveFileSystemPath(path: string): Promise<string[]> {

src/vs/platform/sandbox/common/terminalSandboxMxcRuntime.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { win32 } from '../../../base/common/path.js';
77
import { URI } from '../../../base/common/uri.js';
88
import { createDecorator } from '../../instantiation/common/instantiation.js';
99
import type { IWindowsMxcConfig, IWindowsMxcPolicyContainment, IWindowsMxcSandboxPolicy } from './sandboxHelperService.js';
10-
import type { ITerminalSandboxResolvedNetworkDomains } from './terminalSandboxService.js';
1110

1211
export interface IWindowsMxcConfigOptions {
1312
command: string;
@@ -16,7 +15,6 @@ export interface IWindowsMxcConfigOptions {
1615
tempDir: URI;
1716
schemaVersion?: string;
1817
allowNetwork: boolean;
19-
networkDomains: ITerminalSandboxResolvedNetworkDomains;
2018
allowReadPaths: string[];
2119
allowWritePaths: string[];
2220
denyReadPaths: string[];
@@ -47,8 +45,7 @@ export interface IWindowsMxcTerminalSandboxRuntime {
4745
export class WindowsMxcTerminalSandboxRuntime implements IWindowsMxcTerminalSandboxRuntime {
4846
declare readonly _serviceBrand: undefined;
4947

50-
private readonly _configVersion = '0.4.0-alpha';
51-
private readonly _containerName = 'vscode-terminal-sandbox';
48+
private readonly _configVersion = '0.6.0-alpha';
5249

5350
getExecutablePath(appRoot: string, arch: string | undefined): string {
5451
const binArch = arch === 'arm64' ? 'arm64' : 'x64';
@@ -71,25 +68,25 @@ export class WindowsMxcTerminalSandboxRuntime implements IWindowsMxcTerminalSand
7168
const shell = options.shell
7269
? this._quoteWindowsCommandLineArgument(options.shell)
7370
: 'pwsh.exe';
74-
const commandLine = `${shell} -NoProfile -ExecutionPolicy Bypass -Command ${this._quoteWindowsCommandLineArgument(options.command)}`;
71+
const commandLine = `${shell} -NoProfile -Command ${this._quoteWindowsCommandLineArgument(options.command)}`;
7572
const cwd = options.cwd ? this.toWindowsPath(options.cwd) : tempDirPath;
7673
const policy: IWindowsMxcSandboxPolicy = {
7774
version: options.schemaVersion ?? this._configVersion,
7875
timeoutMs: 0,
7976
filesystem: {
80-
readwritePaths: [...new Set(options.allowWritePaths.map(path => this._normalizeWindowsPath(path)))],
81-
readonlyPaths: [...new Set([tempDirPath, ...(options.shell && win32.isAbsolute(options.shell) ? [win32.dirname(options.shell)] : []), ...options.allowReadPaths].map(path => this._normalizeWindowsPath(path)))],
82-
deniedPaths: [...new Set(options.denyReadPaths.map(path => this._normalizeWindowsPath(path)))],
77+
readwritePaths: options.allowWritePaths.map(path => this._normalizeWindowsPath(path)),
78+
readonlyPaths: [tempDirPath, ...(options.shell && win32.isAbsolute(options.shell) ? [win32.dirname(options.shell)] : []), ...options.allowReadPaths].map(path => this._normalizeWindowsPath(path)),
79+
deniedPaths: options.denyReadPaths.map(path => this._normalizeWindowsPath(path)),
8380
},
84-
network: this._createNetworkPolicy(options.allowNetwork, options.networkDomains),
81+
network: this._createNetworkPolicy(options.allowNetwork),
8582
ui: {
8683
allowWindows: true,
8784
clipboard: 'none',
8885
allowInputInjection: false,
8986
},
9087
};
9188

92-
const config = await buildSandboxPayload(commandLine, policy, cwd, this._containerName);
89+
const config = await buildSandboxPayload(commandLine, policy, cwd);
9390
if (!config?.process) {
9491
throw new Error('Unable to build Windows MXC sandbox payload');
9592
}
@@ -123,20 +120,10 @@ export class WindowsMxcTerminalSandboxRuntime implements IWindowsMxcTerminalSand
123120
return path.replace(/\//g, '\\');
124121
}
125122

126-
private _createNetworkPolicy(allowNetwork: boolean, networkDomains: ITerminalSandboxResolvedNetworkDomains): NonNullable<IWindowsMxcSandboxPolicy['network']> {
127-
const allowedHosts = networkDomains.allowedDomains.length > 0 ? networkDomains.allowedDomains : undefined;
128-
const blockedHosts = networkDomains.deniedDomains.length > 0 ? networkDomains.deniedDomains : undefined;
129-
const allowOutbound = allowNetwork || !!allowedHosts?.length;
130-
const network: NonNullable<IWindowsMxcSandboxPolicy['network']> = {
131-
allowOutbound,
132-
};
133-
if (allowOutbound && allowedHosts) {
134-
network.allowedHosts = allowedHosts;
135-
}
136-
if (allowOutbound && blockedHosts) {
137-
network.blockedHosts = blockedHosts;
138-
}
139-
return network;
123+
private _createNetworkPolicy(allowNetwork: boolean): NonNullable<IWindowsMxcSandboxPolicy['network']> {
124+
// MXC does not support per-host network policies on Windows. Rely on the
125+
// overall allow/block policy instead of emitting unsupported host lists.
126+
return { allowOutbound: allowNetwork };
140127
}
141128

142129
private _quotePowerShellArgument(value: string): string {

src/vs/platform/sandbox/test/common/terminalSandboxEngine.test.ts

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ suite('TerminalSandboxEngine', () => {
7070
const network = {
7171
defaultPolicy: policy.network?.allowOutbound ? 'allow' : 'block' as 'allow' | 'block',
7272
...(policy.network?.allowLocalNetwork !== undefined ? { allowLocalNetwork: policy.network.allowLocalNetwork } : {}),
73-
...(policy.network?.allowedHosts ? { allowedHosts: policy.network.allowedHosts } : {}),
74-
...(policy.network?.blockedHosts ? { blockedHosts: policy.network.blockedHosts } : {}),
75-
...(policy.network ? { enforcementMode: policy.network.allowedHosts?.length || policy.network.blockedHosts?.length ? 'both' as const : 'capabilities' as const } : {}),
73+
...(policy.network ? { enforcementMode: 'capabilities' as const } : {}),
7674
};
7775
return {
7876
version: policy.version,
@@ -88,7 +86,6 @@ suite('TerminalSandboxEngine', () => {
8886
timeout: policy.timeoutMs ?? 0,
8987
},
9088
processContainer: {
91-
name: containerName,
9289
leastPrivilege: false,
9390
capabilities: policy.network?.allowOutbound ? ['internetClient'] : [],
9491
ui: {
@@ -527,10 +524,9 @@ suite('TerminalSandboxEngine', () => {
527524
strictEqual(wrapped.isSandboxWrapped, true);
528525
ok(wrapped.command.startsWith(`& 'C:\\app\\node_modules\\@microsoft\\mxc-sdk\\bin\\x64\\wxc-exec.exe'`), `Expected MXC executable. Actual: ${wrapped.command}`);
529526
ok(wrapped.command.includes(` '${configPath}'`), `Expected wrapped command to pass the MXC config path. Actual: ${wrapped.command}`);
530-
strictEqual(config.version, '0.4.0-alpha');
527+
strictEqual(config.version, '0.6.0-alpha');
531528
strictEqual(config.containment, 'process');
532-
strictEqual(config.processContainer.name, 'vscode-terminal-sandbox');
533-
strictEqual(config.process.commandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo hello"');
529+
strictEqual(config.process.commandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -Command "echo hello"');
534530
strictEqual(normalizeWindowsPathForAssert(config.process.cwd), 'c:/workspace');
535531
strictEqual(config.ui.disable, false);
536532
ok(config.process.env.includes('SystemRoot=C:\\Windows'), 'SystemRoot should be injected into the MXC process env');
@@ -579,6 +575,54 @@ suite('TerminalSandboxEngine', () => {
579575
ok(!config.filesystem.deniedPaths.some((path: string) => normalizeWindowsPathForAssert(path) === 'c:/users/user'), 'User home should not be denied by default on Windows');
580576
});
581577

578+
test('deduplicates Windows filesystem paths regardless of case or separator', async () => {
579+
enableWindowsSandbox();
580+
setSandboxSetting(AgentSandboxSettingId.AgentSandboxWindowsFileSystem, {
581+
allowWrite: ['C:/configured/write'],
582+
allowRead: ['C:\\configured\\read'],
583+
denyRead: ['C:/configured/secret', 'c:\\configured\\secret'],
584+
});
585+
const host = createWindowsHost({
586+
getWindowsMxcFilesystemPolicy: () => Promise.resolve({
587+
readwritePaths: ['c:\\configured\\write'],
588+
readonlyPaths: ['c:/configured/read'],
589+
}),
590+
});
591+
const engine = store.add(instantiationService.createInstance(TerminalSandboxEngine, host));
592+
593+
await engine.wrapCommand('echo hello', false, 'pwsh');
594+
const configPath = await engine.getSandboxConfigPath();
595+
ok(configPath, 'Config path should be defined');
596+
const config = JSON.parse(createdFiles.get(configPath)!);
597+
const matchingPaths = (paths: string[], expectedPath: string) => paths.filter(path => normalizeWindowsPathForAssert(path) === expectedPath);
598+
599+
deepStrictEqual({
600+
readwrite: matchingPaths(config.filesystem.readwritePaths, 'c:/configured/write'),
601+
readonly: matchingPaths(config.filesystem.readonlyPaths, 'c:/configured/read'),
602+
denied: matchingPaths(config.filesystem.deniedPaths, 'c:/configured/secret'),
603+
}, {
604+
readwrite: ['C:\\configured\\write'],
605+
readonly: ['C:\\configured\\read'],
606+
denied: ['C:\\configured\\secret'],
607+
});
608+
});
609+
610+
test('deduplicates resolved Windows paths regardless of case or separator', async () => {
611+
enableWindowsSandbox();
612+
const engine = store.add(instantiationService.createInstance(TerminalSandboxEngine, createWindowsHost()));
613+
await engine.getOS();
614+
const resolveFileSystemPaths = (engine as unknown as { _resolveFileSystemPaths(paths: string[]): Promise<string[]> })._resolveFileSystemPaths.bind(engine);
615+
616+
deepStrictEqual(await resolveFileSystemPaths([
617+
'C:/configured/path',
618+
'c:\\configured\\path',
619+
'C:\\configured\\other-path',
620+
]), [
621+
'C:/configured/path',
622+
'C:\\configured\\other-path',
623+
]);
624+
});
625+
582626
test('wrapCommand applies configured Windows MXC schema version', async () => {
583627
enableWindowsSandbox();
584628
setSandboxSetting(AgentSandboxSettingId.AgentSandboxWindowsSchemaVersion, '0.5.0-alpha');
@@ -651,13 +695,13 @@ suite('TerminalSandboxEngine', () => {
651695
let configPath = await engine.getSandboxConfigPath();
652696
ok(configPath, 'Config path should be defined');
653697
const firstCommandLine = JSON.parse(createdFiles.get(configPath)!).process.commandLine;
654-
strictEqual(firstCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo first"');
698+
strictEqual(firstCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -Command "echo first"');
655699

656700
await engine.wrapCommand('echo second', false, 'C:\\Program Files\\PowerShell\\7\\pwsh.exe');
657701
configPath = await engine.getSandboxConfigPath();
658702
ok(configPath, 'Config path should be defined');
659703
const secondCommandLine = JSON.parse(createdFiles.get(configPath)!).process.commandLine;
660-
strictEqual(secondCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo second"');
704+
strictEqual(secondCommandLine, '"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -NoProfile -Command "echo second"');
661705
});
662706

663707
test('allowNetwork maps to MXC allow network config on Windows', async () => {
@@ -674,6 +718,21 @@ suite('TerminalSandboxEngine', () => {
674718
deepStrictEqual(config.network, { defaultPolicy: 'allow', enforcementMode: 'capabilities' });
675719
});
676720

721+
test('Windows MXC config ignores unsupported network host lists', async () => {
722+
enableWindowsSandbox();
723+
setSandboxSetting(AgentNetworkDomainSettingId.AllowedNetworkDomains, ['example.com']);
724+
setSandboxSetting(AgentNetworkDomainSettingId.DeniedNetworkDomains, ['blocked.example.com']);
725+
const host = createWindowsHost();
726+
const engine = store.add(instantiationService.createInstance(TerminalSandboxEngine, host));
727+
728+
await engine.wrapCommand('curl https://example.com', false, 'pwsh');
729+
const configPath = await engine.getSandboxConfigPath();
730+
ok(configPath, 'Config path should be defined');
731+
const config = JSON.parse(createdFiles.get(configPath)!);
732+
733+
deepStrictEqual(config.network, { defaultPolicy: 'allow', enforcementMode: 'capabilities' });
734+
});
735+
677736
test('uses OS-specific filesystem absolute path detection', async () => {
678737
const linuxEngine = store.add(instantiationService.createInstance(TerminalSandboxEngine, createHost()));
679738
await linuxEngine.getOS();

src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,7 @@ suite('TerminalSandboxService - network domains', () => {
225225
},
226226
network: {
227227
defaultPolicy: policy.network?.allowOutbound ? 'allow' : 'block',
228-
...(policy.network ? { enforcementMode: policy.network.allowedHosts?.length || policy.network.blockedHosts?.length ? 'both' : 'capabilities' } : {}),
229-
...(policy.network?.allowedHosts ? { allowedHosts: policy.network.allowedHosts } : {}),
230-
...(policy.network?.blockedHosts ? { blockedHosts: policy.network.blockedHosts } : {}),
228+
...(policy.network ? { enforcementMode: 'capabilities' } : {}),
231229
},
232230
ui: {
233231
disable: !(policy.ui?.allowWindows ?? false),
@@ -1458,10 +1456,9 @@ suite('TerminalSandboxService - network domains', () => {
14581456
strictEqual(wrapped.isSandboxWrapped, true);
14591457
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}`);
14601458
ok(wrapped.command.includes(configPath), `Wrapped command should pass the MXC config path. Actual: ${wrapped.command}`);
1461-
strictEqual(config.version, '0.4.0-alpha');
1459+
strictEqual(config.version, '0.6.0-alpha');
14621460
strictEqual(config.containment, 'process');
1463-
strictEqual(config.processContainer.name, 'vscode-terminal-sandbox');
1464-
strictEqual(config.process.commandLine, '"c:\\program files\\powershell\\7\\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -Command "echo test"');
1461+
strictEqual(config.process.commandLine, '"c:\\program files\\powershell\\7\\pwsh.exe" -NoProfile -Command "echo test"');
14651462
strictEqual(config.process.cwd, 'c:\\workspace-one');
14661463
ok(config.process.env.includes('SystemRoot=c:\\windows'), 'SystemRoot should be injected into the MXC process env');
14671464
ok(config.process.env.includes('PATH=c:\\tools\\node;c:\\windows\\system32'), 'PATH should be injected into the MXC process env');

0 commit comments

Comments
 (0)