diff --git a/package-lock.json b/package-lock.json index 11ee47ac0fa..5ae5fdb83e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 36a96b45e45..110b3ff6ff2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/remote/package-lock.json b/remote/package-lock.json index 383aa08c216..46f4e699f5e 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -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", diff --git a/remote/package.json b/remote/package.json index b06d9bb358a..6b0764ac427 100644 --- a/remote/package.json +++ b/remote/package.json @@ -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", diff --git a/src/vs/platform/sandbox/common/sandboxHelperService.ts b/src/vs/platform/sandbox/common/sandboxHelperService.ts index f00f8238ad9..d0687b970e1 100644 --- a/src/vs/platform/sandbox/common/sandboxHelperService.ts +++ b/src/vs/platform/sandbox/common/sandboxHelperService.ts @@ -59,7 +59,6 @@ export interface IWindowsMxcConfig { timeout?: number; }; processContainer?: { - name?: string; leastPrivilege?: boolean; capabilities?: string[]; ui?: { diff --git a/src/vs/platform/sandbox/common/terminalSandboxEngine.ts b/src/vs/platform/sandbox/common/terminalSandboxEngine.ts index 8cd937622bc..4735c27450c 100644 --- a/src/vs/platform/sandbox/common/terminalSandboxEngine.ts +++ b/src/vs/platform/sandbox/common/terminalSandboxEngine.ts @@ -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 { const resolvedPaths = await Promise.all((paths ?? []).map(path => this._resolveFileSystemPath(path))); - return [...new Set(resolvedPaths.flat())]; + const seenPaths = new Set(); + 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 { diff --git a/src/vs/platform/sandbox/common/terminalSandboxMxcRuntime.ts b/src/vs/platform/sandbox/common/terminalSandboxMxcRuntime.ts index b0b07fa8ea3..d109c521aa1 100644 --- a/src/vs/platform/sandbox/common/terminalSandboxMxcRuntime.ts +++ b/src/vs/platform/sandbox/common/terminalSandboxMxcRuntime.ts @@ -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 { - 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 = { - allowOutbound, - }; - if (allowOutbound && allowedHosts) { - network.allowedHosts = allowedHosts; - } - if (allowOutbound && blockedHosts) { - network.blockedHosts = blockedHosts; - } - return network; + private _createNetworkPolicy(allowNetwork: boolean): NonNullable { + // 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 { diff --git a/src/vs/platform/sandbox/test/common/terminalSandboxEngine.test.ts b/src/vs/platform/sandbox/test/common/terminalSandboxEngine.test.ts index db01f21ac68..6cfe5e6ca98 100644 --- a/src/vs/platform/sandbox/test/common/terminalSandboxEngine.test.ts +++ b/src/vs/platform/sandbox/test/common/terminalSandboxEngine.test.ts @@ -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 })._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(); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts index ef8d8c6102c..fa50259acd1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/terminalSandboxService.test.ts @@ -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');