diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 09173beb696831..fff4b090a0d2ff 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -201,14 +201,16 @@ export function isAgentEnabled(envValue: string | undefined, defaultEnabled: boo /** * Configuration key that controls the sandbox mode for the Copilot SDK's built-in * shell tool (the path taken when `AgentHostCustomTerminalToolEnabledSettingId` - * is `false`). Values mirror {@link AgentSandboxEnabledValue}: + * is `false`). Supported values are: * * - `'off'` (the default): no sandbox policy is forwarded for the SDK shell * path \u2014 commands run unsandboxed. * - `'on'`: the Agent Host runs the SDK\u2019s shell tool inside a sandbox * using the user's `chat.agent.sandbox.fileSystem.*` filesystem policy. - * Outbound network is enforced via the user's allow/deny host lists. - * - `'allowNetwork'`: same as `'on'` but with unrestricted outbound network. + * Outbound network is blocked. + * + * Unrestricted outbound network is controlled separately by + * `chat.agent.sandbox.allowNetwork`. * * Has no effect when `AgentHostCustomTerminalToolEnabledSettingId` is * `true` \u2014 the host\u2019s own terminal sandbox engine then handles shell @@ -216,6 +218,15 @@ export function isAgentEnabled(envValue: string | undefined, defaultEnabled: boo */ export const AgentHostSdkSandboxEnabledSettingId = 'chat.agentHost.sdkSandbox.enabled'; +/** + * Configuration key that controls the sandbox mode for the Copilot SDK's + * built-in shell tool on Windows. This is independent of + * {@link AgentHostSdkSandboxEnabledSettingId} so Windows support can be rolled + * out separately. Supported values are `'off'` and `'on'`; the default is + * `'off'`. + */ +export const AgentHostSdkSandboxWindowsEnabledSettingId = 'chat.agentHost.sdkSandbox.enabledWindows'; + /** * Selects whether the regular workbench surfaces Codex from the agent host * instead of the OpenAI extension. diff --git a/src/vs/platform/agentHost/common/sandboxConfigSchema.ts b/src/vs/platform/agentHost/common/sandboxConfigSchema.ts index 2b834e0684f1fb..1aebd0e19a5ae2 100644 --- a/src/vs/platform/agentHost/common/sandboxConfigSchema.ts +++ b/src/vs/platform/agentHost/common/sandboxConfigSchema.ts @@ -61,7 +61,7 @@ export type ISandboxConfigValue = Partial<{ * normalized form of each setting is declared here — the workbench is * expected to: * - * - map legacy boolean sandbox enabled values to the `'on' | 'off' | 'allowNetwork'` + * - map legacy boolean sandbox enabled values to the `'on' | 'off'` * agent-host enum, and * - migrate values from any deprecated setting IDs to their modern key * @@ -76,12 +76,12 @@ export const sandboxConfigSchema = createSchema({ [AgentHostSandboxKey.Enabled]: { type: 'string', title: localize('agentHost.config.sandbox.enabled.title', "Sandbox Enabled"), - enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On, AgentSandboxEnabledValue.AllowNetwork], + enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On], }, [AgentHostSandboxKey.WindowsEnabled]: { type: 'string', title: localize('agentHost.config.sandbox.windowsEnabled.title', "Sandbox Enabled (Windows)"), - enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On, AgentSandboxEnabledValue.AllowNetwork], + enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On], }, [AgentHostSandboxKey.AllowNetwork]: { type: 'boolean', diff --git a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts index c86d4f16a0b35c..00c523ef39b186 100644 --- a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts +++ b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts @@ -7,15 +7,6 @@ import type { CopilotSession } from '@github/copilot-sdk'; import { AgentSandboxEnabledValue } from '../../../sandbox/common/settings.js'; import { AgentHostSandboxKey, type ISandboxConfigValue } from '../../common/sandboxConfigSchema.js'; -/** - * Whether the SDK sandbox is supported on Windows. Not enabled yet, so the - * builders bail out early on `win32`; the Windows handling is kept so support - * can be turned on by flipping this flag once the runtime is ready. Typed as - * `boolean` (not the `false` literal) so the Windows branches are not flagged - * as unreachable by control-flow narrowing. - */ -const WINDOWS_SANDBOX_SUPPORTED: boolean = false; - /** * Per-platform filesystem rule bundle accepted under each `fileSystem.` * sub-key (`AgentHostSandboxKey.LinuxFileSystem` etc.) in the AgentHost root @@ -52,16 +43,13 @@ export type CopilotSandboxConfig = SdkSandboxConfig & { * - Path precedence: `denyRead` > `denyWrite` > `allowWrite` > `allowRead`. * Each path appears in exactly one of `deniedPaths` / `readonlyPaths` / * `readwritePaths`. - * - Network: `allowNetwork` opens outbound to everything and drops the - * allow/deny lists. Otherwise the allow/deny lists open outbound when - * set so they're actually enforced; host lists are currently disabled on - * all platforms (fail closed) because the runtime does not yet enforce - * them reliably everywhere. + * - Network: the separate `allowNetwork` policy opens outbound to everything. + * Domain allow/deny lists are ignored because the SDK's `SandboxConfig` + * does not support host-level rules. * - * Windows is not supported yet, so this bails out early and returns `undefined` - * there. The Windows handling below is intentionally kept (and exercised when - * {@link WINDOWS_SANDBOX_SUPPORTED} is flipped) so support can be turned on once - * the runtime is ready. + * Windows uses its platform-specific enablement and filesystem settings. It + * does not fall back to the shared enablement setting so Windows rollout is + * controlled independently. */ export function buildSandboxConfigForSdk( platform: NodeJS.Platform, @@ -71,16 +59,10 @@ export function buildSandboxConfigForSdk( return undefined; } - // Typed as `boolean` (not the `false` literal) so the Windows branches below - // are not flagged as unreachable by control-flow narrowing. - if (platform === 'win32' && !WINDOWS_SANDBOX_SUPPORTED) { - return undefined; - } - - const enabledRaw = platform === 'win32' && sandbox[AgentHostSandboxKey.WindowsEnabled] !== undefined + const enabledRaw = platform === 'win32' ? sandbox[AgentHostSandboxKey.WindowsEnabled] : sandbox[AgentHostSandboxKey.Enabled]; - if (enabledRaw !== AgentSandboxEnabledValue.On && enabledRaw !== AgentSandboxEnabledValue.AllowNetwork) { + if (enabledRaw !== AgentSandboxEnabledValue.On) { return undefined; } @@ -89,7 +71,8 @@ export function buildSandboxConfigForSdk( : platform === 'darwin' ? sandbox[AgentHostSandboxKey.MacFileSystem] : sandbox[AgentHostSandboxKey.LinuxFileSystem]; - const fs = (fsRaw && typeof fsRaw === 'object') ? fsRaw as IAgentSandboxFileSystemSetting : {}; + const hasFileSystemPolicy = fsRaw !== undefined && typeof fsRaw === 'object'; + const fs = hasFileSystemPolicy ? fsRaw as IAgentSandboxFileSystemSetting : {}; const denied = new Set(fs.denyRead ?? []); const readonly = new Set(); @@ -110,20 +93,25 @@ export function buildSandboxConfigForSdk( } } - const legacyAllowAllNetwork = enabledRaw === AgentSandboxEnabledValue.AllowNetwork; - const allowAllNetwork = legacyAllowAllNetwork || (enabledRaw === AgentSandboxEnabledValue.On && sandbox[AgentHostSandboxKey.AllowNetwork] === true); + const allowNetwork = sandbox[AgentHostSandboxKey.AllowNetwork]; + const allowBypass = sandbox[AgentHostSandboxKey.AllowUnsandboxedCommands]; + const filesystem = hasFileSystemPolicy + ? { + ...(denied.size ? { deniedPaths: [...denied] } : {}), + ...(readonly.size ? { readonlyPaths: [...readonly] } : {}), + ...(readwrite.size ? { readwritePaths: [...readwrite] } : {}), + } + : undefined; + const network = typeof allowNetwork === 'boolean' ? { allowOutbound: allowNetwork } : undefined; + const userPolicy = filesystem || network + ? { + ...(filesystem ? { filesystem } : {}), + ...(network ? { network } : {}), + } + : undefined; return { enabled: true, - allowBypass: true, - userPolicy: { - filesystem: { - ...(readwrite.size ? { readwritePaths: [...readwrite] } : {}), - ...(readonly.size ? { readonlyPaths: [...readonly] } : {}), - ...(denied.size ? { deniedPaths: [...denied] } : {}), - }, - network: { - allowOutbound: allowAllNetwork, - }, - }, + ...(typeof allowBypass === 'boolean' ? { allowBypass } : {}), + ...(userPolicy ? { userPolicy } : {}), }; } diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index e880989042fd88..88b07fc79edd79 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -38,6 +38,7 @@ import { STREAMING_TOOL_DISPLAY_INTERVAL_MS } from '../../common/streamingToolCa import { CustomizationType, McpAuthRequiredReason, McpServerStatus, type Customization } from '../../common/state/protocol/channels-session/state.js'; import { CopilotAgentSession } from '../../node/copilot/copilotAgentSession.js'; import { buildNonPtyShellTerminalUri } from '../../node/copilot/copilotNonPtyShellTerminals.js'; +import { buildSandboxConfigForSdk } from '../../node/copilot/sandboxConfigForSdk.js'; import { ActiveClientToolSet } from '../../node/activeClientState.js'; import { type CopilotSessionLaunchPlan, type IActiveClientSnapshot, type ICopilotSessionLauncher, type ICopilotSessionRuntime } from '../../node/copilot/copilotSessionLauncher.js'; import { CopilotSessionWrapper } from '../../node/copilot/copilotSessionWrapper.js'; @@ -3045,17 +3046,14 @@ suite('CopilotAgentSession', () => { }); test('per-request sandbox: applies the configured policy under default permissions', async () => { + const sandbox = { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On }; const { session, mockSession } = await createAgentSession(disposables, { - rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } }, + rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox }, }); await session.send('hello', undefined, 'turn-1'); - assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { - enabled: true, - allowBypass: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), buildSandboxConfigForSdk('linux', sandbox)); assert.deepStrictEqual(mockSession.permissionModeSetCalls, ['off']); }); @@ -3442,8 +3440,9 @@ suite('CopilotAgentSession', () => { }); test('syncs sandbox when the session approval level changes', async () => { + const sandbox = { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On }; const { session, mockSession, setConfigValue, fireSessionConfigChange } = await createAgentSession(disposables, { - rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } }, + rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox }, configValues: { [SessionConfigKey.AutoApprove]: 'default' }, }); await session.send('hello', undefined, 'turn-1'); @@ -3462,17 +3461,9 @@ suite('CopilotAgentSession', () => { }, { permissionModes: ['off', 'on', 'off'], sandboxConfigs: [ - { - enabled: true, - allowBypass: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }, + buildSandboxConfigForSdk('linux', sandbox), { enabled: false }, - { - enabled: true, - allowBypass: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }, + buildSandboxConfigForSdk('linux', sandbox), ], }); }); @@ -3534,8 +3525,9 @@ suite('CopilotAgentSession', () => { }); test('per-request permissions: Autopilot with Ask When Needed keeps SDK approval mode off', async () => { + const sandbox = { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On }; const { session, mockSession } = await createAgentSession(disposables, { - rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } }, + rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox }, configValues: { [SessionConfigKey.Mode]: 'autopilot', [SessionConfigKey.AutoApprove]: 'default', @@ -3549,11 +3541,7 @@ suite('CopilotAgentSession', () => { sandbox: mockSession.sandboxConfigUpdates.at(-1), }, { permissionModes: ['off'], - sandbox: { - enabled: true, - allowBypass: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }, + sandbox: buildSandboxConfigForSdk('linux', sandbox), }); }); @@ -3570,15 +3558,16 @@ suite('CopilotAgentSession', () => { assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { enabled: false }); }); - test('per-request sandbox: explicitly disabled on Windows', async () => { + test('per-request sandbox: applies the configured policy on Windows', async () => { + const sandbox = { [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On }; const { session, mockSession } = await createAgentSession(disposables, { - rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } }, + rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox }, platform: 'win32', }); await session.send('hello', undefined, 'turn-1'); - assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { enabled: false }); + assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), buildSandboxConfigForSdk('win32', sandbox)); }); test('per-request sandbox: explicitly disabled when the sandbox setting is off', async () => { diff --git a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts index 5dcbe7df861480..f4299cbf01d2b9 100644 --- a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts @@ -178,10 +178,10 @@ suite('CopilotShellTools', () => { if (options?.sandboxEnabled) { initialSandboxValues[AgentHostSandboxKey.Enabled] = AgentSandboxEnabledValue.On; // Windows uses a separate enable key; the engine treats - // `Enabled=On` on non-Windows and `WindowsEnabled=AllowNetwork` + // `Enabled=On` on non-Windows and `WindowsEnabled=On` // on Windows as "sandbox active". Set both so tests exercise // the sandbox path on every OS. - initialSandboxValues[AgentHostSandboxKey.WindowsEnabled] = AgentSandboxEnabledValue.AllowNetwork; + initialSandboxValues[AgentHostSandboxKey.WindowsEnabled] = AgentSandboxEnabledValue.On; } const agentConfigurationService = createFakeAgentConfigurationService(initialSandboxValues); const services = new ServiceCollection(); diff --git a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts index 900191beb6d93c..8b3f78ddb51898 100644 --- a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts +++ b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { AgentHostSandboxKey, type ISandboxConfigValue } from '../../common/sandboxConfigSchema.js'; import { AgentSandboxEnabledValue } from '../../../sandbox/common/settings.js'; -import { buildSandboxConfigForSdk, type IAgentSandboxFileSystemSetting } from '../../node/copilot/sandboxConfigForSdk.js'; +import { buildSandboxConfigForSdk, type CopilotSandboxConfig, type IAgentSandboxFileSystemSetting } from '../../node/copilot/sandboxConfigForSdk.js'; /** * Build the host-side `sandbox` root-config bag (the shape the workbench @@ -24,13 +24,14 @@ function sandbox( enabled: AgentSandboxEnabledValue | undefined, fs?: IAgentSandboxFileSystemSetting, hosts?: { allowedHosts?: readonly string[]; blockedHosts?: readonly string[] }, + allowNetwork?: boolean, ): ISandboxConfigValue | undefined { if (!enabled && !fs && !hosts) { return undefined; } const cfg: ISandboxConfigValue = {}; if (enabled !== undefined) { - cfg[AgentHostSandboxKey.Enabled] = enabled; + cfg[platform === 'win32' ? AgentHostSandboxKey.WindowsEnabled : AgentHostSandboxKey.Enabled] = enabled; } if (fs) { const fsKey = platform === 'win32' @@ -46,9 +47,48 @@ function sandbox( if (hosts?.blockedHosts?.length) { cfg[AgentHostSandboxKey.DeniedNetworkDomains] = [...hosts.blockedHosts]; } + if (allowNetwork !== undefined) { + cfg[AgentHostSandboxKey.AllowNetwork] = allowNetwork; + } return cfg; } +function expectedSandboxConfig(options?: { + hasFileSystemPolicy?: boolean; + readwritePaths?: string[]; + readonlyPaths?: string[]; + deniedPaths?: string[]; + allowOutbound?: boolean; + allowBypass?: boolean; +}): CopilotSandboxConfig { + const hasFileSystemPolicy = options?.hasFileSystemPolicy === true + || options?.readwritePaths !== undefined + || options?.readonlyPaths !== undefined + || options?.deniedPaths !== undefined; + return { + enabled: true, + ...(options?.allowBypass !== undefined ? { allowBypass: options.allowBypass } : {}), + ...(hasFileSystemPolicy || options?.allowOutbound !== undefined + ? { + userPolicy: { + ...(hasFileSystemPolicy + ? { + filesystem: { + ...(options?.deniedPaths?.length ? { deniedPaths: options.deniedPaths } : {}), + ...(options?.readonlyPaths?.length ? { readonlyPaths: options.readonlyPaths } : {}), + ...(options?.readwritePaths?.length ? { readwritePaths: options.readwritePaths } : {}), + }, + } + : {}), + ...(options?.allowOutbound !== undefined + ? { network: { allowOutbound: options.allowOutbound } } + : {}), + }, + } + : {}), + }; +} + suite('buildSandboxConfigForSdk', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -68,30 +108,51 @@ suite('buildSandboxConfigForSdk', () => { assert.strictEqual(buildSandboxConfigForSdk('win32', sandbox('win32', AgentSandboxEnabledValue.Off)), undefined); }); - test('enables sandbox for `on` on non-Windows platforms', () => { - for (const platform of ['darwin', 'linux'] as const) { - assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On)), { - enabled: true, - allowBypass: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + test('returns undefined for `off` when allowNetwork is set', () => { + assert.strictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.Off, undefined, undefined, true)), undefined); + assert.strictEqual(buildSandboxConfigForSdk('win32', sandbox('win32', AgentSandboxEnabledValue.Off, undefined, undefined, true)), undefined); + }); + + test('enables sandbox for `on` on supported platforms', () => { + for (const platform of ['darwin', 'linux', 'win32'] as const) { + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On)), expectedSandboxConfig()); } }); - test('enables sandbox and outbound network for `allowNetwork` on non-Windows platforms', () => { - for (const platform of ['darwin', 'linux'] as const) { - assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.AllowNetwork)), { - enabled: true, - allowBypass: true, - userPolicy: { filesystem: {}, network: { allowOutbound: true } }, - }); + test('enables outbound network through the separate allowNetwork policy', () => { + for (const platform of ['darwin', 'linux', 'win32'] as const) { + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On, undefined, undefined, true)), expectedSandboxConfig({ allowOutbound: true })); } }); - test('ignores the enable settings on Windows', () => { - // The sandbox is not supported on Windows, so the enable settings are ignored. - assert.strictEqual(buildSandboxConfigForSdk('win32', sandbox('win32', AgentSandboxEnabledValue.On)), undefined); - assert.strictEqual(buildSandboxConfigForSdk('win32', sandbox('win32', AgentSandboxEnabledValue.AllowNetwork)), undefined); + test('maps the unsandboxed commands setting to SDK bypass', () => { + assert.deepStrictEqual([ + buildSandboxConfigForSdk('linux', { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.AllowUnsandboxedCommands]: true, + }), + buildSandboxConfigForSdk('linux', { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.AllowUnsandboxedCommands]: false, + }), + ], [ + expectedSandboxConfig({ allowBypass: true }), + expectedSandboxConfig({ allowBypass: false }), + ]); + }); + + test('prefers the Windows-specific enable setting', () => { + const cfg: ISandboxConfigValue = { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.Off, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, + }; + assert.deepStrictEqual(buildSandboxConfigForSdk('win32', cfg), expectedSandboxConfig()); + }); + + test('does not fall back to the non-Windows enable setting on Windows', () => { + assert.strictEqual(buildSandboxConfigForSdk('win32', { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + }), undefined); }); }); @@ -99,13 +160,14 @@ suite('buildSandboxConfigForSdk', () => { test('selects the OS-specific slice from the per-OS filesystem keys', () => { const cfg: ISandboxConfigValue = { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, [AgentHostSandboxKey.LinuxFileSystem]: { allowWrite: ['/linux'] }, [AgentHostSandboxKey.MacFileSystem]: { allowWrite: ['/mac'] }, + [AgentHostSandboxKey.WindowsFileSystem]: { allowWrite: ['C:\\windows'] }, }; - assert.deepStrictEqual(buildSandboxConfigForSdk('linux', cfg)?.userPolicy?.filesystem, { readwritePaths: ['/linux'] }); - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', cfg)?.userPolicy?.filesystem, { readwritePaths: ['/mac'] }); - // Windows is ignored entirely. - assert.strictEqual(buildSandboxConfigForSdk('win32', cfg), undefined); + assert.deepStrictEqual(buildSandboxConfigForSdk('linux', cfg)?.userPolicy?.filesystem, expectedSandboxConfig({ readwritePaths: ['/linux'] }).userPolicy?.filesystem); + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', cfg)?.userPolicy?.filesystem, expectedSandboxConfig({ readwritePaths: ['/mac'] }).userPolicy?.filesystem); + assert.deepStrictEqual(buildSandboxConfigForSdk('win32', cfg)?.userPolicy?.filesystem, expectedSandboxConfig({ readwritePaths: ['C:\\windows'] }).userPolicy?.filesystem); }); test('maps each setting to the corresponding SDK list', () => { @@ -115,26 +177,15 @@ suite('buildSandboxConfigForSdk', () => { denyWrite: ['/readonly'], denyRead: ['/secret'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs)), { - enabled: true, - allowBypass: true, - userPolicy: { - filesystem: { - readwritePaths: ['/work'], - readonlyPaths: ['/readonly', '/read'], - deniedPaths: ['/secret'], - }, - network: { allowOutbound: false }, - }, - }); + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs)), expectedSandboxConfig({ + readwritePaths: ['/work'], + readonlyPaths: ['/readonly', '/read'], + deniedPaths: ['/secret'], + })); }); - test('omits filesystem lists that are empty', () => { - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, {})), { - enabled: true, - allowBypass: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + test('does not add defaults for an empty filesystem policy', () => { + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, {})), expectedSandboxConfig({ hasFileSystemPolicy: true })); }); test('denyRead wins over every other setting for the same path', () => { @@ -144,9 +195,7 @@ suite('buildSandboxConfigForSdk', () => { denyWrite: ['/p'], denyRead: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, { - deniedPaths: ['/p'], - }); + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ deniedPaths: ['/p'] }).userPolicy?.filesystem); }); test('denyWrite wins over allowWrite / allowRead for the same path', () => { @@ -155,9 +204,7 @@ suite('buildSandboxConfigForSdk', () => { allowWrite: ['/p'], denyWrite: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, { - readonlyPaths: ['/p'], - }); + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ readonlyPaths: ['/p'] }).userPolicy?.filesystem); }); test('allowWrite wins over allowRead for the same path', () => { @@ -165,9 +212,7 @@ suite('buildSandboxConfigForSdk', () => { allowRead: ['/p'], allowWrite: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, { - readwritePaths: ['/p'], - }); + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ readwritePaths: ['/p'] }).userPolicy?.filesystem); }); test('keeps distinct paths in their own lists when settings overlap on some paths', () => { @@ -175,34 +220,30 @@ suite('buildSandboxConfigForSdk', () => { allowWrite: ['/work', '/shared'], denyWrite: ['/shared'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, { + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ readwritePaths: ['/work'], readonlyPaths: ['/shared'], - }); + }).userPolicy?.filesystem); }); }); suite('network hosts', () => { - test('drops host lists and keeps outbound closed when sandbox is `on` (host lists disabled on all platforms)', () => { + test('drops host lists without adding a network policy', () => { for (const platform of ['darwin', 'linux'] as const) { - assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On, undefined, { allowedHosts: ['github.com'], blockedHosts: ['evil.example'] }))?.userPolicy?.network, { - allowOutbound: false, - }, platform); + assert.strictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On, undefined, { allowedHosts: ['github.com'], blockedHosts: ['evil.example'] }))?.userPolicy?.network, undefined, platform); } }); - test('ignores host lists when sandbox is `allowNetwork` (allow all)', () => { + test('allows all outbound network through the separate allowNetwork policy', () => { for (const platform of ['darwin', 'linux'] as const) { - assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.AllowNetwork, undefined, { allowedHosts: ['a.example'], blockedHosts: ['b.example'] }))?.userPolicy?.network, { + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On, undefined, { allowedHosts: ['a.example'], blockedHosts: ['b.example'] }, true))?.userPolicy?.network, { allowOutbound: true, }, platform); } }); test('ignores empty host lists', () => { - assert.deepStrictEqual(buildSandboxConfigForSdk('linux', sandbox('linux', AgentSandboxEnabledValue.On, undefined, { allowedHosts: [], blockedHosts: [] }))?.userPolicy?.network, { - allowOutbound: false, - }); + assert.strictEqual(buildSandboxConfigForSdk('linux', sandbox('linux', AgentSandboxEnabledValue.On, undefined, { allowedHosts: [], blockedHosts: [] }))?.userPolicy?.network, undefined); }); }); }); diff --git a/src/vs/platform/sandbox/common/settings.ts b/src/vs/platform/sandbox/common/settings.ts index f9f0db1cdfc572..06ab9fe05eb997 100644 --- a/src/vs/platform/sandbox/common/settings.ts +++ b/src/vs/platform/sandbox/common/settings.ts @@ -23,7 +23,6 @@ export const enum AgentSandboxSettingId { export const enum AgentSandboxEnabledValue { Off = 'off', On = 'on', - AllowNetwork = 'allowNetwork', } export type AgentSandboxEnabledSettingValue = AgentSandboxEnabledValue | boolean; diff --git a/src/vs/platform/sandbox/common/terminalSandboxEngine.ts b/src/vs/platform/sandbox/common/terminalSandboxEngine.ts index 2a09313fdac746..c54ea5cb416352 100644 --- a/src/vs/platform/sandbox/common/terminalSandboxEngine.ts +++ b/src/vs/platform/sandbox/common/terminalSandboxEngine.ts @@ -1072,13 +1072,7 @@ export class TerminalSandboxEngine extends Disposable { } private _isSandboxAllowNetworkConfigured(): boolean { - if (this._host.getSandboxSetting(AgentSandboxSettingId.AgentSandboxAllowNetwork) === true) { - return true; - } - if (this._os === OperatingSystem.Windows) { - return this._getSandboxConfiguredWindowsEnabledValue() === AgentSandboxEnabledValue.AllowNetwork; - } - return this._getSandboxConfiguredEnabledValue() === AgentSandboxEnabledValue.AllowNetwork; + return this._host.getSandboxSetting(AgentSandboxSettingId.AgentSandboxAllowNetwork) === true; } private _areUnsandboxedCommandsAllowed(): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts index 9f3d44361711f8..1b4ca8c690d135 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts @@ -12,13 +12,15 @@ import { Delayer } from '../../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { isWindows } from '../../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; -import { IActionListOptions, ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../../../platform/actionWidget/browser/actionList.js'; +import { IActionListOptions, ActionListItemKind, IActionListDelegate, IActionListItem, IActionListItemInlineToggle } from '../../../../../../platform/actionWidget/browser/actionList.js'; import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; import { getCodexApprovalsPickerListOptions } from '../../../../../../platform/agentHost/browser/codexApprovalsPicker.js'; -import { IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId, IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostCustomTerminalToolEnabledSettingId } from '../../../../../../platform/agentHost/common/copilotCliConfig.js'; import { KNOWN_AUTO_APPROVE_VALUES, SessionConfigKey } from '../../../../../../platform/agentHost/common/sessionConfigKeys.js'; import { ClaudeSessionConfigKey } from '../../../../../../platform/agentHost/common/claudeSessionConfigKeys.js'; import { CodexSessionConfigKey } from '../../../../../../platform/agentHost/common/codexSessionConfigKeys.js'; @@ -31,14 +33,16 @@ import { IHoverService } from '../../../../../../platform/hover/browser/hover.js import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { AgentSandboxEnabledSettingValue, AgentSandboxEnabledValue, AgentSandboxSettingId, isAgentSandboxEnabledValue } from '../../../../../../platform/sandbox/common/settings.js'; import type { IAction } from '../../../../../../base/common/actions.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import type { IChatWidget } from '../../chat.js'; import { ChatConfiguration, ChatPermissionLevel, isChatPermissionLevel } from '../../../common/constants.js'; +import { SessionType } from '../../../common/chatSessionsService.js'; import { isAssistedPermissionsEnabled, isAutoApprovePolicyRestricted, isAutoApproveValuePolicyRestricted, isPermissionLevelVisible, normalizeSessionConfigValue } from '../../../common/agentHostConfigPolicy.js'; import { maybeConfirmElevatedPermissionLevel } from '../../../common/chatPermissionWarnings.js'; -import { isUntitledChatSession } from '../../../common/model/chatUri.js'; +import { getChatSessionType, isUntitledChatSession } from '../../../common/model/chatUri.js'; import { withChatInputPickerMotion } from '../../widget/input/chatInputPickerActionItem.js'; import { IAgentHostSessionWorkingDirectoryResolver } from './agentHostSessionWorkingDirectoryResolver.js'; import { IAgentHostNewSessionFolderService } from './agentHostNewSessionFolderService.js'; @@ -97,7 +101,7 @@ function getConfigIcon(property: string, value: unknown | undefined): ThemeIcon return undefined; } -function toActionItems(property: string, items: readonly IConfigPickerItem[], currentValue: unknown | undefined, policyRestricted = false): IActionListItem[] { +function toActionItems(property: string, items: readonly IConfigPickerItem[], currentValue: unknown | undefined, policyRestricted = false, sandboxToggle?: IActionListItemInlineToggle): IActionListItem[] { return items.map(item => { const disabled = property === SessionConfigKey.AutoApprove && isAutoApproveValuePolicyRestricted(item.value, policyRestricted); const hover = getConfigPickerItemHover(property, item, disabled); @@ -108,11 +112,32 @@ function toActionItems(property: string, items: readonly IConfigPickerItem[], cu group: { title: '', icon: getConfigIcon(property, item.value) }, disabled, ...(hover ? { hover: { content: hover } } : {}), + ...(isAgentHostSandboxToggleItem(property, item.value) && sandboxToggle ? { inlineToggle: sandboxToggle } : {}), item: { ...item, checked: isSelectedValue(currentValue, item.value) }, }; }); } +export function isAgentHostSandboxToggleItem(property: string, value: string): boolean { + return property === SessionConfigKey.AutoApprove && value === ChatPermissionLevel.Default; +} + +type AgentHostSandboxSettingId = + | AgentSandboxSettingId.AgentSandboxEnabled + | AgentSandboxSettingId.AgentSandboxWindowsEnabled + | typeof AgentHostSdkSandboxEnabledSettingId + | typeof AgentHostSdkSandboxWindowsEnabledSettingId; + +export function getAgentHostSandboxSettingId(sessionType: string | undefined, customTerminalToolEnabled: boolean, windows = isWindows): AgentHostSandboxSettingId | undefined { + if (sessionType !== SessionType.AgentHostCopilot) { + return undefined; + } + if (customTerminalToolEnabled) { + return windows ? AgentSandboxSettingId.AgentSandboxWindowsEnabled : AgentSandboxSettingId.AgentSandboxEnabled; + } + return windows ? AgentHostSdkSandboxWindowsEnabledSettingId : AgentHostSdkSandboxEnabledSettingId; +} + function isSelectedValue(currentValue: unknown | undefined, itemValue: string): boolean { if (typeof currentValue === 'boolean') { return currentValue === (itemValue === 'true'); @@ -307,6 +332,7 @@ export function resolveConfigChipValue(isUntitled: boolean, serverValue: unknown export class AgentHostChatInputPicker extends Disposable { private _container: HTMLElement | undefined; + private _trigger: HTMLElement | undefined; private _initialResolved: { readonly sessionResource: URI; readonly result: ResolveSessionConfigResult } | undefined; private readonly _initialResolveCts = this._registerInitialResolveCts(); private readonly _renderDisposables = this._register(new DisposableStore()); @@ -339,6 +365,14 @@ export class AgentHostChatInputPicker extends Disposable { this._reattach(); } })); + this._register(this._configurationService.onDidChangeConfiguration(e => { + const sandboxSettingId = this._getSandboxSettingId(); + if (e.affectsConfiguration(ChatConfiguration.PermissionsSandboxToggleEnabled) + || e.affectsConfiguration(AgentHostCustomTerminalToolEnabledSettingId) + || (sandboxSettingId && e.affectsConfiguration(sandboxSettingId))) { + this._refreshTrigger(); + } + })); this._reattach(); } @@ -346,6 +380,7 @@ export class AgentHostChatInputPicker extends Disposable { const cts = new MutableDisposable(); this._register(toDisposable(() => { this._container = undefined; + this._trigger = undefined; this._cancelInitialResolve(); })); return this._register(cts); @@ -443,6 +478,7 @@ export class AgentHostChatInputPicker extends Disposable { if (!this._container || this._renderDisposables.isDisposed) { return; } + this._trigger = undefined; this._renderDisposables.clear(); dom.clearNode(this._container); @@ -477,6 +513,7 @@ export class AgentHostChatInputPicker extends Disposable { const isReadOnly = !!ctx.schema.readOnly || (isStartedSession && ctx.schema.sessionMutable === false); const trigger = renderPickerTrigger(slot, isReadOnly, this._renderDisposables, () => this._showPicker(trigger)); + this._trigger = trigger; const tooltip = getConfigPickerTriggerHover(this._property, ctx.schema, ctx.value, isReadOnly); if (tooltip) { this._renderDisposables.add(this._hoverService.setupDelayedHover(trigger, { content: tooltip })); @@ -504,7 +541,25 @@ export class AgentHostChatInputPicker extends Disposable { : localize('agentHostChatInputPicker.triggerAria', "{0}: {1}", schema.title, label)); } + private _refreshTrigger(): void { + const trigger = this._trigger; + const ctx = this._readContext(); + if (!trigger || !ctx) { + return; + } + const sessionResource = this._widget.viewModel?.sessionResource; + const isStartedSession = !!sessionResource && !isUntitledChatSession(sessionResource); + const isReadOnly = !!ctx.schema.readOnly || (isStartedSession && ctx.schema.sessionMutable === false); + this._renderTrigger(trigger, ctx.schema, ctx.value, isReadOnly); + } + private _labelFor(schema: SessionConfigPropertySchema, value: unknown | undefined): string { + if (this._property === SessionConfigKey.AutoApprove + && value === ChatPermissionLevel.Default + && this._isSandboxToggleSettingEnabled() + && this._isSandboxingEnabled()) { + return localize('agentHostChatInputPicker.defaultSandboxedLabel', "Default permissions (sandboxed)"); + } if (schema.type === 'boolean') { return value === true ? localize('agentHostChatInputPicker.boolean.onLabel', "On") @@ -576,7 +631,7 @@ export class AgentHostChatInputPicker extends Disposable { } const currentValue = ctx.value; const policyRestricted = isAutoApprovePolicyRestricted(this._configurationService); - const actionItems = toActionItems(this._property, items, currentValue, policyRestricted); + const actionItems = toActionItems(this._property, items, currentValue, policyRestricted, this._getSandboxInlineToggle()); const permissionsLearnMoreUrl = getPermissionsLearnMoreUrl(this._property); if (permissionsLearnMoreUrl) { const learnMoreLabel = localize('agentHostChatInputPicker.learnMorePermissions', "Learn more about permissions"); @@ -609,7 +664,7 @@ export class AgentHostChatInputPicker extends Disposable { if (!refreshed) { return []; } - return toActionItems(this._property, await this._getItems(refreshed.schema, query), refreshed.value, isAutoApprovePolicyRestricted(this._configurationService)); + return toActionItems(this._property, await this._getItems(refreshed.schema, query), refreshed.value, isAutoApprovePolicyRestricted(this._configurationService), this._getSandboxInlineToggle()); }) : undefined, onHide: () => trigger.focus(), @@ -636,6 +691,38 @@ export class AgentHostChatInputPicker extends Disposable { ); } + private _getSandboxSettingId(): ReturnType { + const sessionResource = this._widget.viewModel?.sessionResource; + const sessionType = sessionResource ? getChatSessionType(sessionResource) : undefined; + const customTerminalToolEnabled = this._configurationService.getValue(AgentHostCustomTerminalToolEnabledSettingId) === true; + return getAgentHostSandboxSettingId(sessionType, customTerminalToolEnabled); + } + + private _isSandboxToggleSettingEnabled(): boolean { + return this._configurationService.getValue(ChatConfiguration.PermissionsSandboxToggleEnabled) === true; + } + + private _isSandboxingEnabled(): boolean { + const settingId = this._getSandboxSettingId(); + return settingId !== undefined && isAgentSandboxEnabledValue(this._configurationService.getValue(settingId)); + } + + private _getSandboxInlineToggle(): IActionListItemInlineToggle | undefined { + const settingId = this._getSandboxSettingId(); + if (this._property !== SessionConfigKey.AutoApprove || !this._isSandboxToggleSettingEnabled() || !settingId) { + return undefined; + } + return { + label: localize('agentHostChatInputPicker.defaultSandboxToggle', "Sandboxing for terminal"), + title: localize('agentHostChatInputPicker.defaultSandboxToggleTitle', "Run terminal commands inside a sandbox that restricts file system and network access"), + checked: this._isSandboxingEnabled(), + onChange: checked => { + const target = checked ? AgentSandboxEnabledValue.On : AgentSandboxEnabledValue.Off; + void this._configurationService.updateValue(settingId, target); + }, + }; + } + private async _getItems(schema: SessionConfigPropertySchema, query?: string): Promise { if (schema.type === 'boolean') { return [ diff --git a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts index ab326decd1c1ea..45de2dff2148fa 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -12,7 +12,7 @@ import { PolicyCategory } from '../../../../base/common/policy.js'; import '../../../../platform/agentHost/common/agentHostEnablementService.js'; import '../../../../platform/agentHost/browser/agentHostEnablementService.js'; import '../../../../platform/agentHost/common/agentHostStarter.config.contribution.js'; -import { AgentHostAhpJsonlLoggingSettingId, AgentHostAllowSignedOutWhenUsableSettingId, AgentHostSdkSandboxEnabledSettingId, CodexPreferAgentHostEditorSettingId } from '../../../../platform/agentHost/common/agentService.js'; +import { AgentHostAhpJsonlLoggingSettingId, AgentHostAllowSignedOutWhenUsableSettingId, AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId, CodexPreferAgentHostEditorSettingId } from '../../../../platform/agentHost/common/agentService.js'; import { AgentHostCopilotSdkLogLevelSettingId, AgentHostCustomTerminalToolEnabledSettingId, AgentHostModelCapabilityOverridesSettingId, AgentHostOpus48PromptEnabledSettingId, AgentHostReasoningEffortOverrideSettingId, AgentHostToolSearchDeferThresholdSettingId, AgentHostToolSearchEnabledSettingId, copilotSdkLogLevelSettingValues } from '../../../../platform/agentHost/common/copilotCliConfig.js'; import { AgentHostAutoReplyEnabledConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostMigrateLegacyCopilotCliEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey } from '../../../../platform/agentHost/common/agentHostSchema.js'; import { DEFAULT_LOCAL_TRANSCRIPTION_MODEL } from '../../../../platform/localTranscription/common/localTranscription.js'; @@ -601,7 +601,7 @@ configurationRegistry.registerConfiguration({ [ChatConfiguration.PermissionsSandboxToggleEnabled]: { type: 'boolean', default: false, - markdownDescription: nls.localize('chat.experimental.permissionsSandboxToggle.enabled', "Controls whether the permissions picker shows an inline \"Sandboxing for terminal\" toggle on the Default Permissions option. The toggle reflects and updates `#chat.agent.sandbox.enabled#`."), + markdownDescription: nls.localize('chat.experimental.permissionsSandboxToggle.enabled', "Controls whether the permissions picker shows an inline \"Sandboxing for terminal\" toggle on the Default Permissions option. For Copilot SDK sessions using the built-in shell tool, the toggle reflects and updates `#chat.agentHost.sdkSandbox.enabled#` or `#chat.agentHost.sdkSandbox.enabledWindows#`."), tags: ['experimental'], experiment: { mode: 'auto' @@ -1572,13 +1572,26 @@ configurationRegistry.registerConfiguration({ }, [AgentHostSdkSandboxEnabledSettingId]: { type: 'string', - enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On, AgentSandboxEnabledValue.AllowNetwork], + enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On], enumDescriptions: [ nls.localize('chat.agentHost.sdkSandbox.enabled.off', "No sandbox policy is forwarded for the SDK's built-in shell tool — commands run unsandboxed."), - nls.localize('chat.agentHost.sdkSandbox.enabled.on', "The SDK's built-in shell tool runs inside a sandbox using the configured filesystem policy and host-list-restricted network."), - nls.localize('chat.agentHost.sdkSandbox.enabled.allowNetwork', "The SDK's built-in shell tool runs inside a sandbox with unrestricted outbound network access."), + nls.localize('chat.agentHost.sdkSandbox.enabled.on', "The SDK's built-in shell tool runs inside a sandbox using the configured filesystem policy with outbound network blocked."), ], - markdownDescription: nls.localize('chat.agentHost.sdkSandbox.enabled', "Sandbox mode for the Copilot SDK's built-in shell tool. Only takes effect when `#chat.agentHost.customTerminalTool.enabled#` is `false`; when the Agent Host's own terminal tool is enabled, the engine sandbox is controlled by `#chat.agent.sandbox.enabled#`. The sandbox applies only to requests that run with default permissions — not when approvals are bypassed — and is not supported on Windows yet."), + markdownDescription: nls.localize('chat.agentHost.sdkSandbox.enabled', "Sandbox mode for the Copilot SDK's built-in shell tool on macOS and Linux. Only takes effect when `#chat.agentHost.customTerminalTool.enabled#` is `false`; when the Agent Host's own terminal tool is enabled, the engine sandbox is controlled by `#chat.agent.sandbox.enabled#`. The sandbox applies only to requests that run with default permissions — not when approvals are bypassed. Unrestricted network is controlled by `#chat.agent.sandbox.allowNetwork#`. Use `#chat.agentHost.sdkSandbox.enabledWindows#` on Windows."), + default: AgentSandboxEnabledValue.Off, + tags: ['experimental', 'advanced'], + experiment: { + mode: 'auto' + }, + }, + [AgentHostSdkSandboxWindowsEnabledSettingId]: { + type: 'string', + enum: [AgentSandboxEnabledValue.Off, AgentSandboxEnabledValue.On], + enumDescriptions: [ + nls.localize('chat.agentHost.sdkSandbox.enabledWindows.off', "No sandbox policy is forwarded for the SDK's built-in shell tool on Windows — commands run unsandboxed."), + nls.localize('chat.agentHost.sdkSandbox.enabledWindows.on', "The SDK's built-in shell tool runs inside the Windows sandbox using the configured filesystem policy."), + ], + markdownDescription: nls.localize('chat.agentHost.sdkSandbox.enabledWindows', "Sandbox mode for the Copilot SDK's built-in shell tool on Windows. Only takes effect when `#chat.agentHost.customTerminalTool.enabled#` is `false`. This setting is independent of `#chat.agentHost.sdkSandbox.enabled#` so Windows sandbox support can be enabled separately. Unrestricted network is controlled by `#chat.agent.sandbox.allowNetwork#`."), default: AgentSandboxEnabledValue.Off, tags: ['experimental', 'advanced'], experiment: { diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts index 927100f1c68983..f3f3514a5af268 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatInputPicker.test.ts @@ -9,7 +9,10 @@ import { ClaudeSessionConfigKey } from '../../../../../../platform/agentHost/com import { SessionConfigKey } from '../../../../../../platform/agentHost/common/sessionConfigKeys.js'; import { CodexSessionConfigKey } from '../../../../../../platform/agentHost/common/codexSessionConfigKeys.js'; import type { SessionConfigPropertySchema } from '../../../../../../platform/agentHost/common/state/protocol/commands.js'; -import { getConfigPickerItemHover, getConfigPickerListOptions, getConfigPickerTriggerHover, resolveConfigChipValue } from '../../../browser/agentSessions/agentHost/agentHostChatInputPicker.js'; +import { getAgentHostSandboxSettingId, getConfigPickerItemHover, getConfigPickerListOptions, getConfigPickerTriggerHover, isAgentHostSandboxToggleItem, resolveConfigChipValue } from '../../../browser/agentSessions/agentHost/agentHostChatInputPicker.js'; +import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId } from '../../../../../../platform/agentHost/common/agentService.js'; +import { AgentSandboxSettingId } from '../../../../../../platform/sandbox/common/settings.js'; +import { SessionType } from '../../../common/chatSessionsService.js'; import { getAgentHostPickerProperty, OpenAgentHostAutoApprovePickerAction, OpenAgentHostCodexApprovalsPickerAction, OpenAgentHostModePickerAction, OpenAgentHostPermissionModePickerAction } from '../../../browser/agentSessions/agentHost/agentHostChatInputPicker.contribution.js'; import { isAutoApproveValuePolicyRestricted, isPermissionLevelVisible, normalizeSessionConfigValue } from '../../../common/agentHostConfigPolicy.js'; import { ChatPermissionLevel } from '../../../common/constants.js'; @@ -55,6 +58,34 @@ suite('AgentHostChatInputPicker - list options', () => { }, }); }); + + test('attaches the sandbox toggle only to Default permissions', () => { + assert.deepStrictEqual({ + defaultPermissions: isAgentHostSandboxToggleItem(SessionConfigKey.AutoApprove, ChatPermissionLevel.Default), + assistedPermissions: isAgentHostSandboxToggleItem(SessionConfigKey.AutoApprove, ChatPermissionLevel.Assisted), + modeDefault: isAgentHostSandboxToggleItem(SessionConfigKey.Mode, ChatPermissionLevel.Default), + }, { + defaultPermissions: true, + assistedPermissions: false, + modeDefault: false, + }); + }); + + test('resolves the Copilot Agent Host sandbox setting', () => { + assert.deepStrictEqual({ + sdk: getAgentHostSandboxSettingId(SessionType.AgentHostCopilot, false, false), + sdkWindows: getAgentHostSandboxSettingId(SessionType.AgentHostCopilot, false, true), + customTerminal: getAgentHostSandboxSettingId(SessionType.AgentHostCopilot, true, false), + customTerminalWindows: getAgentHostSandboxSettingId(SessionType.AgentHostCopilot, true, true), + claude: getAgentHostSandboxSettingId(SessionType.AgentHostClaude, false, false), + }, { + sdk: AgentHostSdkSandboxEnabledSettingId, + sdkWindows: AgentHostSdkSandboxWindowsEnabledSettingId, + customTerminal: AgentSandboxSettingId.AgentSandboxEnabled, + customTerminalWindows: AgentSandboxSettingId.AgentSandboxWindowsEnabled, + claude: undefined, + }); + }); }); suite('AgentHostChatInputPicker - resolveConfigChipValue', () => { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 1486e2b2fd31a5..80c5267d6751d0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -12,7 +12,6 @@ import { localize } from '../../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationRegistry, type IConfigurationPropertySchema } from '../../../../platform/configuration/common/configurationRegistry.js'; import product from '../../../../platform/product/common/product.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { AgentSandboxEnabledValue } from '../../../../platform/sandbox/common/settings.js'; import { TerminalLocationConfigValue, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { terminalColorSchema, terminalIconSchema } from '../../../../platform/terminal/common/terminalPlatformConfiguration.js'; import { ConfigurationKeyValuePairs, IConfigurationMigrationRegistry, Extensions as WorkbenchExtensions } from '../../../common/configuration.js'; @@ -715,30 +714,6 @@ export async function registerTerminalConfiguration(getFontSnippets: () => Promi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ - key: TerminalContribSettingId.AgentSandboxEnabled, - migrateFn: (value: unknown, valueAccessor) => { - if (value !== AgentSandboxEnabledValue.AllowNetwork) { - return []; - } - const configurationKeyValuePairs: ConfigurationKeyValuePairs = [[TerminalContribSettingId.AgentSandboxEnabled, { value: AgentSandboxEnabledValue.On }]]; - if (valueAccessor(TerminalContribSettingId.AgentSandboxAllowNetwork) === undefined) { - configurationKeyValuePairs.push([TerminalContribSettingId.AgentSandboxAllowNetwork, { value: true }]); - } - return configurationKeyValuePairs; - } - }, { - key: TerminalContribSettingId.AgentSandboxWindowsEnabled, - migrateFn: (value: unknown, valueAccessor) => { - if (value !== AgentSandboxEnabledValue.AllowNetwork) { - return []; - } - const configurationKeyValuePairs: ConfigurationKeyValuePairs = [[TerminalContribSettingId.AgentSandboxWindowsEnabled, { value: AgentSandboxEnabledValue.On }]]; - if (valueAccessor(TerminalContribSettingId.AgentSandboxAllowNetwork) === undefined) { - configurationKeyValuePairs.push([TerminalContribSettingId.AgentSandboxAllowNetwork, { value: true }]); - } - return configurationKeyValuePairs; - } - }, { key: TerminalSettingId.EnableBell, migrateFn: (enableBell, accessor) => { const configurationKeyValuePairs: ConfigurationKeyValuePairs = []; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts index 88df3908f169de..7323029e51abad 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts @@ -5,7 +5,7 @@ import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { equals } from '../../../../../base/common/objects.js'; -import { AgentHostSdkSandboxEnabledSettingId, IAgentConnection } from '../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId, IAgentConnection } from '../../../../../platform/agentHost/common/agentService.js'; import { AgentHostCustomTerminalToolEnabledSettingId } from '../../../../../platform/agentHost/common/copilotCliConfig.js'; import { IAgentHostConnectionsService } from '../../../../../platform/agentHost/common/agentHostConnectionsService.js'; import { AgentHostSandboxConfigKey, AgentHostSandboxKey } from '../../../../../platform/agentHost/common/sandboxConfigSchema.js'; @@ -25,6 +25,7 @@ import { readAgentHostSandboxValues, SANDBOX_SETTING_KEYS } from '../common/sand const HOST_POLICY_SETTING_KEYS: readonly string[] = [ AgentHostCustomTerminalToolEnabledSettingId, AgentHostSdkSandboxEnabledSettingId, + AgentHostSdkSandboxWindowsEnabledSettingId, ]; /** @@ -165,13 +166,14 @@ export class AgentHostSandboxForwarder extends Disposable implements IWorkbenchC * those values directly. * * - Otherwise (the SDK runs the shell tool), gate on - * `chat.agentHost.sdkSandbox.enabled`: - * - `'off'` (the default) — forward an empty object so any + * `chat.agentHost.sdkSandbox.enabled` and + * `chat.agentHost.sdkSandbox.enabledWindows` independently: + * - both `'off'` (the default) — forward an empty object so any * previously-pushed values are cleared and the SDK runs commands * unsandboxed. - * - `'on'` / `'allowNetwork'` — forward the user's policy but - * override both `enabled` and `enabled.windows` with the SDK - * sandbox value. The SDK sandbox mode is independent of the + * - either `'on'` — forward the user's policy and + * set `enabled` and `enabled.windows` from their corresponding SDK + * settings. The SDK sandbox modes are independent of the * engine sandbox mode, so the user can run the SDK sandboxed * even when the engine sandbox is off. */ @@ -182,11 +184,14 @@ export class AgentHostSandboxForwarder extends Disposable implements IWorkbenchC return values; } const sdkSandbox = this._configurationService.getValue(AgentHostSdkSandboxEnabledSettingId) ?? AgentSandboxEnabledValue.Off; - if (sdkSandbox !== AgentSandboxEnabledValue.On && sdkSandbox !== AgentSandboxEnabledValue.AllowNetwork) { + const windowsSdkSandbox = this._configurationService.getValue(AgentHostSdkSandboxWindowsEnabledSettingId) ?? AgentSandboxEnabledValue.Off; + const sdkSandboxEnabled = sdkSandbox === AgentSandboxEnabledValue.On; + const windowsSdkSandboxEnabled = windowsSdkSandbox === AgentSandboxEnabledValue.On; + if (!sdkSandboxEnabled && !windowsSdkSandboxEnabled) { return {}; } - values[AgentHostSandboxKey.Enabled] = sdkSandbox; - values[AgentHostSandboxKey.WindowsEnabled] = sdkSandbox; + values[AgentHostSandboxKey.Enabled] = sdkSandboxEnabled ? AgentSandboxEnabledValue.On : AgentSandboxEnabledValue.Off; + values[AgentHostSandboxKey.WindowsEnabled] = windowsSdkSandboxEnabled ? AgentSandboxEnabledValue.On : AgentSandboxEnabledValue.Off; return values; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/sandboxSettingsReader.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/sandboxSettingsReader.ts index a664b2556b900d..f12798eb6aad5e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/sandboxSettingsReader.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/sandboxSettingsReader.ts @@ -54,7 +54,7 @@ export function readAgentHostSandboxValues(configurationService: IConfigurationS /** * Coerce values into the canonical shape the agent-host schema expects. * Today the non-trivial cases are the boolean sandbox enabled settings, - * which are forwarded as the `'on' | 'off' | 'allowNetwork'` enum for + * which are forwarded as the `'on' | 'off'` enum for * agent-host compatibility. */ function normalizeSandboxSettingValue(settingId: string, value: T | undefined): T | undefined { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/agentHostSandboxForwarder.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/agentHostSandboxForwarder.test.ts index c899aa80692c12..70daacb99594f1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/agentHostSandboxForwarder.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/agentHostSandboxForwarder.test.ts @@ -12,7 +12,7 @@ import { TestConfigurationService } from '../../../../../../platform/configurati import { ConfigurationTarget, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; -import { AgentHostSdkSandboxEnabledSettingId, IAgentConnection, IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId, IAgentConnection, IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js'; import { AgentHostCustomTerminalToolEnabledSettingId } from '../../../../../../platform/agentHost/common/copilotCliConfig.js'; import { IAgentHostConnectionsService } from '../../../../../../platform/agentHost/common/agentHostConnectionsService.js'; import { AgentHostConnectionsService } from '../../../../../../platform/agentHost/browser/agentHostConnectionsService.js'; @@ -250,7 +250,7 @@ suite('AgentHostSandboxForwarder', () => { // Initial state already matches → no dispatch. assert.deepStrictEqual(local.dispatched, []); - configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxEnabled, AgentSandboxEnabledValue.AllowNetwork); + configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxEnabled, AgentSandboxEnabledValue.Off); configurationService.onDidChangeConfigurationEmitter.fire({ source: ConfigurationTarget.USER, affectsConfiguration: (key: string) => key === AgentSandboxSettingId.AgentSandboxEnabled, @@ -260,7 +260,7 @@ suite('AgentHostSandboxForwarder', () => { assert.deepStrictEqual(local.dispatched, [{ type: ActionType.RootConfigChanged, - config: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.AllowNetwork } }, + config: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.Off } }, }]); }); @@ -375,7 +375,8 @@ suite('AgentHostSandboxForwarder', () => { suite('SDK-sandbox gating', () => { test('forwards user values verbatim when customTerminalTool is enabled, regardless of sdkSandbox', () => { const { local } = setup(disposables, { - [AgentSandboxSettingId.AgentSandboxEnabled]: AgentSandboxEnabledValue.AllowNetwork, + [AgentSandboxSettingId.AgentSandboxEnabled]: AgentSandboxEnabledValue.On, + [AgentSandboxSettingId.AgentSandboxAllowNetwork]: true, [AgentHostCustomTerminalToolEnabledSettingId]: true, [AgentHostSdkSandboxEnabledSettingId]: AgentSandboxEnabledValue.Off, }); @@ -384,13 +385,19 @@ suite('AgentHostSandboxForwarder', () => { assert.deepStrictEqual(local.dispatched, [{ type: ActionType.RootConfigChanged, - config: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.AllowNetwork } }, + config: { + [AgentHostSandboxConfigKey.Sandbox]: { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.AllowNetwork]: true, + } + }, }]); }); test('forwards an empty sandbox object when both customTerminalTool and sdkSandbox are off (default)', () => { const { local } = setup(disposables, { [AgentSandboxSettingId.AgentSandboxEnabled]: AgentSandboxEnabledValue.On, + [AgentSandboxSettingId.AgentSandboxAllowNetwork]: true, [AgentHostCustomTerminalToolEnabledSettingId]: false, // sdkSandbox unset → defaults to 'off'. }); @@ -404,7 +411,7 @@ suite('AgentHostSandboxForwarder', () => { }]); }); - test('overrides Enabled/WindowsEnabled with the sdkSandbox value when set to `on`', () => { + test('enables non-Windows SDK sandbox independently', () => { const { local } = setup(disposables, { // User has the engine sandbox off entirely — the SDK sandbox // setting should still drive the SDK path independently. @@ -421,18 +428,19 @@ suite('AgentHostSandboxForwarder', () => { config: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, - [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.Off, [AgentHostSandboxKey.AllowUnsandboxedCommands]: true, }, }, }]); }); - test('overrides Enabled/WindowsEnabled with `allowNetwork` when sdkSandbox is set to that', () => { + test('forwards the separate allowNetwork policy for the non-Windows SDK sandbox', () => { const { local } = setup(disposables, { [AgentSandboxSettingId.AgentSandboxEnabled]: AgentSandboxEnabledValue.On, + [AgentSandboxSettingId.AgentSandboxAllowNetwork]: true, [AgentHostCustomTerminalToolEnabledSettingId]: false, - [AgentHostSdkSandboxEnabledSettingId]: AgentSandboxEnabledValue.AllowNetwork, + [AgentHostSdkSandboxEnabledSettingId]: AgentSandboxEnabledValue.On, }); local.setRootState(rootStateWithSandboxSchema()); @@ -441,8 +449,57 @@ suite('AgentHostSandboxForwarder', () => { type: ActionType.RootConfigChanged, config: { [AgentHostSandboxConfigKey.Sandbox]: { - [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.AllowNetwork, - [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.AllowNetwork, + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.Off, + [AgentHostSandboxKey.AllowNetwork]: true, + }, + }, + }]); + }); + + test('enables Windows SDK sandbox independently', () => { + const { local } = setup(disposables, { + [AgentHostCustomTerminalToolEnabledSettingId]: false, + [AgentHostSdkSandboxEnabledSettingId]: AgentSandboxEnabledValue.Off, + [AgentHostSdkSandboxWindowsEnabledSettingId]: AgentSandboxEnabledValue.On, + }); + + local.setRootState(rootStateWithSandboxSchema()); + + assert.deepStrictEqual(local.dispatched, [{ + type: ActionType.RootConfigChanged, + config: { + [AgentHostSandboxConfigKey.Sandbox]: { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.Off, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, + }, + }, + }]); + }); + + test('re-dispatches when the Windows SDK sandbox setting changes', () => { + const { local, configurationService } = setup(disposables, { + [AgentHostCustomTerminalToolEnabledSettingId]: false, + [AgentHostSdkSandboxEnabledSettingId]: AgentSandboxEnabledValue.Off, + [AgentHostSdkSandboxWindowsEnabledSettingId]: AgentSandboxEnabledValue.Off, + }); + local.setRootState(rootStateWithSandboxSchema()); + assert.deepStrictEqual(local.dispatched, []); + + configurationService.setUserConfiguration(AgentHostSdkSandboxWindowsEnabledSettingId, AgentSandboxEnabledValue.On); + configurationService.onDidChangeConfigurationEmitter.fire({ + source: ConfigurationTarget.USER, + affectsConfiguration: key => key === AgentHostSdkSandboxWindowsEnabledSettingId, + affectedKeys: new Set([AgentHostSdkSandboxWindowsEnabledSettingId]), + change: { keys: [AgentHostSdkSandboxWindowsEnabledSettingId], overrides: [] }, + }); + + assert.deepStrictEqual(local.dispatched, [{ + type: ActionType.RootConfigChanged, + config: { + [AgentHostSandboxConfigKey.Sandbox]: { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.Off, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, }, }, }]); @@ -456,7 +513,7 @@ suite('AgentHostSandboxForwarder', () => { }); local.setRootState(rootStateWithSandboxSchema({ [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, - [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.Off, })); // Initial state already matches → no dispatch. assert.deepStrictEqual(local.dispatched, []); @@ -475,7 +532,7 @@ suite('AgentHostSandboxForwarder', () => { }]); }); - test('re-dispatches when sdkSandbox switches between `on` and `allowNetwork`', () => { + test('forwards the separate allowNetwork policy when SDK sandboxing is on', () => { const { local, configurationService } = setup(disposables, { [AgentSandboxSettingId.AgentSandboxEnabled]: AgentSandboxEnabledValue.On, [AgentHostCustomTerminalToolEnabledSettingId]: false, @@ -483,24 +540,25 @@ suite('AgentHostSandboxForwarder', () => { }); local.setRootState(rootStateWithSandboxSchema({ [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, - [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.Off, })); assert.deepStrictEqual(local.dispatched, []); - configurationService.setUserConfiguration(AgentHostSdkSandboxEnabledSettingId, AgentSandboxEnabledValue.AllowNetwork); + configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxAllowNetwork, true); configurationService.onDidChangeConfigurationEmitter.fire({ source: ConfigurationTarget.USER, - affectsConfiguration: (key: string) => key === AgentHostSdkSandboxEnabledSettingId, - affectedKeys: new Set([AgentHostSdkSandboxEnabledSettingId]), - change: { keys: [AgentHostSdkSandboxEnabledSettingId], overrides: [] }, + affectsConfiguration: (key: string) => key === AgentSandboxSettingId.AgentSandboxAllowNetwork, + affectedKeys: new Set([AgentSandboxSettingId.AgentSandboxAllowNetwork]), + change: { keys: [AgentSandboxSettingId.AgentSandboxAllowNetwork], overrides: [] }, }); assert.deepStrictEqual(local.dispatched, [{ type: ActionType.RootConfigChanged, config: { [AgentHostSandboxConfigKey.Sandbox]: { - [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.AllowNetwork, - [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.AllowNetwork, + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.Off, + [AgentHostSandboxKey.AllowNetwork]: true, }, }, }]); 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 e926ba12648837..7d61d9997ee26d 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 @@ -294,7 +294,7 @@ suite('TerminalSandboxService - network domains', () => { }); test('should report enabled when configured to allow network', async () => { - configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxEnabled, AgentSandboxEnabledValue.AllowNetwork); + configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxAllowNetwork, true); const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService)); @@ -505,7 +505,7 @@ suite('TerminalSandboxService - network domains', () => { }); test('should disable runtime network config when configured to allow network', async () => { - configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxEnabled, AgentSandboxEnabledValue.AllowNetwork); + configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxAllowNetwork, true); configurationService.setUserConfiguration(AgentNetworkDomainSettingId.AllowedNetworkDomains, ['example.com']); configurationService.setUserConfiguration(AgentNetworkDomainSettingId.DeniedNetworkDomains, ['blocked.example.com']); configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.AgentSandboxAdvancedRuntime, { @@ -1302,7 +1302,7 @@ suite('TerminalSandboxService - network domains', () => { }); test('should skip domain checks when configured to allow network', async () => { - configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxEnabled, AgentSandboxEnabledValue.AllowNetwork); + configurationService.setUserConfiguration(AgentSandboxSettingId.AgentSandboxAllowNetwork, true); configurationService.setUserConfiguration(AgentNetworkDomainSettingId.AllowedNetworkDomains, ['example.com']); configurationService.setUserConfiguration(AgentNetworkDomainSettingId.DeniedNetworkDomains, ['api.github.com']); const sandboxService = store.add(instantiationService.createInstance(TerminalSandboxService));