From 83d8d0217c3292c84461ff6793b1e0211720ffa3 Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Wed, 5 Aug 2026 17:41:12 -0700 Subject: [PATCH 1/9] Sandboxing investigation --- src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts index 78dbfd7158d707..cce2a20f11a611 100644 --- a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts +++ b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts @@ -13,7 +13,7 @@ import { AgentHostSandboxKey, type ISandboxConfigValue } from '../../common/sand * `boolean` (not the `false` literal) so the Windows branches are not flagged * as unreachable by control-flow narrowing. */ -const WINDOWS_SANDBOX_SUPPORTED: boolean = false; +const WINDOWS_SANDBOX_SUPPORTED: boolean = true; /** * Per-platform filesystem rule bundle accepted under each `fileSystem.` From ec252516a53c057163c636d116125e2f417874b6 Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Thu, 6 Aug 2026 17:42:30 -0700 Subject: [PATCH 2/9] Sandboxing investigation --- .../platform/agentHost/common/agentService.ts | 17 +++- .../node/copilot/sandboxConfigForSdk.ts | 84 ++++++++----------- .../test/node/copilotAgentSession.test.ts | 11 +-- .../test/node/sandboxConfigForSdk.test.ts | 60 +++++++------ .../chat/browser/chat.shared.contribution.ts | 23 +++-- .../browser/agentHostSandboxForwarder.ts | 28 +++++-- .../browser/agentHostSandboxForwarder.test.ts | 75 ++++++++++++++--- 7 files changed, 192 insertions(+), 106 deletions(-) diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 9d690a7184cad0..e075bd1893ff2e 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -205,14 +205,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 @@ -220,6 +222,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 which Claude integration fulfills Claude sessions opened from the * **Agents Window**: diff --git a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts index cce2a20f11a611..2fd0f267f956b7 100644 --- a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts +++ b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts @@ -6,15 +6,6 @@ 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 = true; - /** * Per-platform filesystem rule bundle accepted under each `fileSystem.` * sub-key (`AgentHostSandboxKey.LinuxFileSystem` etc.) in the AgentHost root @@ -31,25 +22,42 @@ export interface IAgentSandboxFileSystemSetting { /** * SDK-side sandbox configuration produced by {@link buildSandboxConfigForSdk}. * - * Structurally a narrowed form of the SDK's `SandboxConfig` type (from + * Mirrors the SDK's `SandboxConfig` type (from * `@github/copilot-sdk`'s `SessionUpdateOptionsParams.sandboxConfig`) — the * same shape the Copilot extension produces via its own `buildSandboxConfigForCLI`. * Defined locally because `SandboxConfig` is not re-exported from the SDK's - * public entry point; this shape stays assignable to it. + * public entry point. */ export interface ISdkSandboxConfig { - enabled: true; - allowBypass?: boolean; - userPolicy: { - filesystem: { + enabled: boolean; + addCurrentWorkingDirectory?: boolean; + allowDevToolAccess?: boolean; + gitAuth?: boolean; + ghAuth?: boolean; + userPolicy?: { + filesystem?: { readwritePaths?: string[]; readonlyPaths?: string[]; deniedPaths?: string[]; + clearPolicyOnExit?: boolean; + }; + network?: { + allowOutbound?: boolean; + allowLocalNetwork?: boolean; + proxy?: { + url: string; + username?: string; + password?: string; + }; }; - network: { - allowOutbound: boolean; - allowedHosts?: string[]; - blockedHosts?: string[]; + seatbelt?: { + keychainAccess?: boolean; + }; + /** @deprecated Use `seatbelt` instead. */ + experimental?: { + seatbelt?: { + keychainAccess?: boolean; + }; }; }; } @@ -71,16 +79,14 @@ export interface ISdkSandboxConfig { * - 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. + * The legacy `allowNetwork` enablement value is treated equivalently. + * 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, @@ -90,13 +96,7 @@ 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) { @@ -129,17 +129,9 @@ export function buildSandboxConfigForSdk( } } - const legacyAllowAllNetwork = enabledRaw === AgentSandboxEnabledValue.AllowNetwork; - const allowAllNetwork = legacyAllowAllNetwork || (enabledRaw === AgentSandboxEnabledValue.On && sandbox[AgentHostSandboxKey.AllowNetwork] === true); - const hostListsEnforceable = false; - const rawAllow = sandbox[AgentHostSandboxKey.AllowedNetworkDomains]; - const rawBlock = sandbox[AgentHostSandboxKey.DeniedNetworkDomains]; - const allowedHosts = !allowAllNetwork && hostListsEnforceable && rawAllow?.length ? [...rawAllow] : undefined; - const blockedHosts = !allowAllNetwork && hostListsEnforceable && rawBlock?.length ? [...rawBlock] : undefined; - const allowOutbound = allowAllNetwork || !!allowedHosts || !!blockedHosts; + const allowAllNetwork = enabledRaw === AgentSandboxEnabledValue.AllowNetwork || sandbox[AgentHostSandboxKey.AllowNetwork] === true; return { enabled: true, - allowBypass: true, userPolicy: { filesystem: { ...(readwrite.size ? { readwritePaths: [...readwrite] } : {}), @@ -147,9 +139,7 @@ export function buildSandboxConfigForSdk( ...(denied.size ? { deniedPaths: [...denied] } : {}), }, network: { - allowOutbound, - ...(allowOutbound && allowedHosts ? { allowedHosts } : {}), - ...(allowOutbound && blockedHosts ? { blockedHosts } : {}), + allowOutbound: allowAllNetwork, }, }, }; diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index dbc3b8e170724b..5deff27ede284f 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -2983,7 +2983,6 @@ suite('CopilotAgentSession', () => { assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { enabled: true, - allowBypass: true, userPolicy: { filesystem: {}, network: { allowOutbound: false } }, }); assert.deepStrictEqual(mockSession.permissionModeSetCalls, ['off']); @@ -3427,7 +3426,6 @@ suite('CopilotAgentSession', () => { permissionModes: ['off'], sandbox: { enabled: true, - allowBypass: true, userPolicy: { filesystem: {}, network: { allowOutbound: false } }, }, }); @@ -3446,15 +3444,18 @@ 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 { session, mockSession } = await createAgentSession(disposables, { - rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } }, + rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On } }, platform: 'win32', }); await session.send('hello', undefined, 'turn-1'); - assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { enabled: false }); + assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { + enabled: true, + userPolicy: { filesystem: {}, network: { allowOutbound: false } }, + }); }); test('per-request sandbox: explicitly disabled when the sandbox setting is off', async () => { diff --git a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts index b7b014da8c3657..2f7a9837ef583b 100644 --- a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts +++ b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts @@ -30,7 +30,7 @@ function sandbox( } const cfg: ISandboxConfigValue = {}; if (enabled !== undefined) { - cfg[AgentHostSandboxKey.Enabled] = enabled; + cfg[platform === 'win32' ? AgentHostSandboxKey.WindowsEnabled : AgentHostSandboxKey.Enabled] = enabled; } if (fs) { const fsKey = platform === 'win32' @@ -68,30 +68,39 @@ 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) { + 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)), { enabled: true, - allowBypass: true, userPolicy: { filesystem: {}, network: { allowOutbound: false } }, }); } }); - test('enables sandbox and outbound network for `allowNetwork` on non-Windows platforms', () => { - for (const platform of ['darwin', 'linux'] as const) { + test('normalizes the legacy `allowNetwork` mode to enabled with outbound network', () => { + for (const platform of ['darwin', 'linux', 'win32'] as const) { assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.AllowNetwork)), { enabled: true, - allowBypass: true, userPolicy: { filesystem: {}, network: { 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('prefers the Windows-specific enable setting', () => { + const cfg: ISandboxConfigValue = { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.Off, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, + }; + assert.deepStrictEqual(buildSandboxConfigForSdk('win32', cfg), { + enabled: true, + userPolicy: { filesystem: {}, network: { allowOutbound: false } }, + }); + }); + + test('does not fall back to the non-Windows enable setting on Windows', () => { + assert.strictEqual(buildSandboxConfigForSdk('win32', { + [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, + }), undefined); }); }); @@ -99,13 +108,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, { readwritePaths: ['/linux'] }); + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', cfg)?.userPolicy?.filesystem, { readwritePaths: ['/mac'] }); + assert.deepStrictEqual(buildSandboxConfigForSdk('win32', cfg)?.userPolicy?.filesystem, { readwritePaths: ['C:\\windows'] }); }); test('maps each setting to the corresponding SDK list', () => { @@ -117,7 +127,6 @@ suite('buildSandboxConfigForSdk', () => { }; assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs)), { enabled: true, - allowBypass: true, userPolicy: { filesystem: { readwritePaths: ['/work'], @@ -132,7 +141,6 @@ suite('buildSandboxConfigForSdk', () => { test('omits filesystem lists that are empty', () => { assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, {})), { enabled: true, - allowBypass: true, userPolicy: { filesystem: {}, network: { allowOutbound: false } }, }); }); @@ -144,7 +152,7 @@ suite('buildSandboxConfigForSdk', () => { denyWrite: ['/p'], denyRead: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy.filesystem, { + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, { deniedPaths: ['/p'], }); }); @@ -155,7 +163,7 @@ suite('buildSandboxConfigForSdk', () => { allowWrite: ['/p'], denyWrite: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy.filesystem, { + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, { readonlyPaths: ['/p'], }); }); @@ -165,7 +173,7 @@ suite('buildSandboxConfigForSdk', () => { allowRead: ['/p'], allowWrite: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy.filesystem, { + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, { readwritePaths: ['/p'], }); }); @@ -175,7 +183,7 @@ 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, { readwritePaths: ['/work'], readonlyPaths: ['/shared'], }); @@ -185,22 +193,24 @@ suite('buildSandboxConfigForSdk', () => { suite('network hosts', () => { test('drops host lists and keeps outbound closed when sandbox is `on` (host lists disabled on all platforms)', () => { 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, { + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On, undefined, { allowedHosts: ['github.com'], blockedHosts: ['evil.example'] }))?.userPolicy?.network, { allowOutbound: false, }, 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, { + const cfg = sandbox(platform, AgentSandboxEnabledValue.On, undefined, { allowedHosts: ['a.example'], blockedHosts: ['b.example'] })!; + cfg[AgentHostSandboxKey.AllowNetwork] = true; + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, cfg)?.userPolicy?.network, { allowOutbound: true, }, platform); } }); test('ignores empty host lists', () => { - assert.deepStrictEqual(buildSandboxConfigForSdk('linux', sandbox('linux', AgentSandboxEnabledValue.On, undefined, { allowedHosts: [], blockedHosts: [] }))?.userPolicy.network, { + assert.deepStrictEqual(buildSandboxConfigForSdk('linux', sandbox('linux', AgentSandboxEnabledValue.On, undefined, { allowedHosts: [], blockedHosts: [] }))?.userPolicy?.network, { allowOutbound: false, }); }); 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 1998f566e4995c..e75b6f1974e9a7 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, ClaudePreferAgentHostAgentsSettingId, ClaudePreferAgentHostEditorSettingId, CodexPreferAgentHostEditorSettingId } from '../../../../platform/agentHost/common/agentService.js'; +import { AgentHostAhpJsonlLoggingSettingId, AgentHostAllowSignedOutWhenUsableSettingId, AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId, ClaudePreferAgentHostAgentsSettingId, ClaudePreferAgentHostEditorSettingId, CodexPreferAgentHostEditorSettingId } from '../../../../platform/agentHost/common/agentService.js'; import { AgentHostCopilotSdkLogLevelSettingId, AgentHostCustomTerminalToolEnabledSettingId, AgentHostModelCapabilityOverridesSettingId, AgentHostOpus48PromptEnabledSettingId, AgentHostReasoningEffortOverrideSettingId, AgentHostToolSearchDeferThresholdSettingId, AgentHostToolSearchEnabledSettingId, copilotSdkLogLevelSettingValues } from '../../../../platform/agentHost/common/copilotCliConfig.js'; import { DEFAULT_LOCAL_TRANSCRIPTION_MODEL } from '../../../../platform/localTranscription/common/localTranscription.js'; import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../../../platform/networkFilter/common/networkFilterService.js'; @@ -1571,13 +1571,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/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts index 88df3908f169de..9432f4bdf5310b 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,15 +166,18 @@ 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. + * - legacy `'allowNetwork'` values are normalized to `'on'` plus the + * existing `allowNetwork: true` policy. */ private _computeDesired(): Record { const customTerminalToolEnabled = this._configurationService.getValue(AgentHostCustomTerminalToolEnabledSettingId) === true; @@ -182,11 +186,17 @@ 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 || sdkSandbox === AgentSandboxEnabledValue.AllowNetwork; + const windowsSdkSandboxEnabled = windowsSdkSandbox === AgentSandboxEnabledValue.On || windowsSdkSandbox === AgentSandboxEnabledValue.AllowNetwork; + 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; + if (sdkSandbox === AgentSandboxEnabledValue.AllowNetwork || windowsSdkSandbox === AgentSandboxEnabledValue.AllowNetwork) { + values[AgentHostSandboxKey.AllowNetwork] = true; + } return values; } 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..4b6d4509d5d704 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'; @@ -404,7 +404,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 +421,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 +442,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 +506,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 +525,7 @@ suite('AgentHostSandboxForwarder', () => { }]); }); - test('re-dispatches when sdkSandbox switches between `on` and `allowNetwork`', () => { + test('normalizes the legacy SDK `allowNetwork` mode to `on` plus allowNetwork policy', () => { const { local, configurationService } = setup(disposables, { [AgentSandboxSettingId.AgentSandboxEnabled]: AgentSandboxEnabledValue.On, [AgentHostCustomTerminalToolEnabledSettingId]: false, @@ -483,7 +533,7 @@ suite('AgentHostSandboxForwarder', () => { }); local.setRootState(rootStateWithSandboxSchema({ [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On, - [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, + [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.Off, })); assert.deepStrictEqual(local.dispatched, []); @@ -499,8 +549,9 @@ 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, }, }, }]); From 6499596daef3d155680992a9c293275468074d97 Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Mon, 10 Aug 2026 13:00:12 -0700 Subject: [PATCH 3/9] Sandbox updates --- .../browser/widget/input/chatInputPart.ts | 2 +- .../input/permissionPickerActionItem.ts | 63 +++++++++++-------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index dff05965501e2b..9587198fbcb2e9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -3463,7 +3463,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this.permissionWidget?.refresh(); }, - isSandboxToggleApplicable: () => this.getEffectiveSessionType(this.getCurrentSessionResource()) === SessionType.Local, + getSandboxSessionType: () => this.getEffectiveSessionType(this.getCurrentSessionResource()), }; const widget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, secondaryPickerOptions); this.permissionWidget = widget; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index d3e3b087afe136..95e8bd890acb9d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -29,6 +29,8 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; import { maybeConfirmElevatedPermissionLevel } from '../../../common/chatPermissionWarnings.js'; import { AgentSandboxEnabledSettingValue, AgentSandboxEnabledValue, AgentSandboxSettingId, isAgentSandboxEnabledValue } from '../../../../../../platform/sandbox/common/settings.js'; +import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId } from '../../../../../../platform/agentHost/common/agentService.js'; +import { AgentHostCustomTerminalToolEnabledSettingId } from '../../../../../../platform/agentHost/common/copilotCliConfig.js'; export interface IExtensionPermissionState { /** Stable identifier for the contributing chat session type, used to namespace action ids. */ @@ -62,13 +64,10 @@ export interface IPermissionPickerDelegate { readonly setExtensionPermission?: (groupId: string, item: IChatSessionProviderOptionItem) => void; readonly getPermissionLevelHover?: (level: ChatPermissionLevel, meta: IPermissionLevelMeta) => string | undefined; /** - * Whether the experimental "Sandboxing for terminal" toggle may be shown on - * the Default permissions option. The toggle is specific to the local harness - * (which runs the built-in terminal tool); agent-host harnesses such as - * Copilot CLI and Claude Code do not implement this and never show it. - * Evaluated each time the picker opens so a harness switch is reflected. + * The session type used to select the sandbox setting for the experimental + * "Sandboxing for terminal" toggle. */ - readonly isSandboxToggleApplicable?: () => boolean; + readonly getSandboxSessionType?: () => string | undefined; } /** Default level set offered when a delegate does not specify {@link IPermissionPickerDelegate.availableLevels}. */ @@ -140,8 +139,20 @@ function sanitizeIdSegment(value: string): string { return value.replace(/[^a-zA-Z0-9_-]/g, '_'); } -function getSandboxEnabledSettingId(): AgentSandboxSettingId.AgentSandboxEnabled | AgentSandboxSettingId.AgentSandboxWindowsEnabled { - return isWindows ? AgentSandboxSettingId.AgentSandboxWindowsEnabled : AgentSandboxSettingId.AgentSandboxEnabled; +type SandboxSettingId = + | AgentSandboxSettingId.AgentSandboxEnabled + | AgentSandboxSettingId.AgentSandboxWindowsEnabled + | typeof AgentHostSdkSandboxEnabledSettingId + | typeof AgentHostSdkSandboxWindowsEnabledSettingId; + +export function getPermissionSandboxSettingId(sessionType: string | undefined, customTerminalToolEnabled: boolean, windows = isWindows): SandboxSettingId | undefined { + if (sessionType === SessionType.Local || (sessionType === SessionType.AgentHostCopilot && customTerminalToolEnabled)) { + return windows ? AgentSandboxSettingId.AgentSandboxWindowsEnabled : AgentSandboxSettingId.AgentSandboxEnabled; + } + if (sessionType === SessionType.AgentHostCopilot) { + return windows ? AgentHostSdkSandboxWindowsEnabledSettingId : AgentHostSdkSandboxEnabledSettingId; + } + return undefined; } export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { @@ -196,11 +207,12 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { } const currentLevel = delegate.currentPermissionLevel.get(); const policyRestricted = isAutoApprovePolicyRestricted(); - const sandboxToggleEnabled = this.isSandboxToggleAvailable(); + const sandboxSettingId = this.getSandboxSettingId(); + const sandboxToggleEnabled = this.isSandboxToggleSettingEnabled() && sandboxSettingId !== undefined; const setSandboxEnabled = async (enableSandbox: boolean) => { const target: AgentSandboxEnabledValue = enableSandbox ? AgentSandboxEnabledValue.On : AgentSandboxEnabledValue.Off; - if (this.isSandboxingEnabled() !== enableSandbox) { - await configurationService.updateValue(getSandboxEnabledSettingId(), target); + if (sandboxSettingId && this.isSandboxingEnabled(sandboxSettingId) !== enableSandbox) { + await configurationService.updateValue(sandboxSettingId, target); } }; const levels = delegate.availableLevels ?? DEFAULT_PERMISSION_LEVELS; @@ -218,7 +230,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ? { label: localize('permissions.default.sandbox.toggle', "Sandboxing for terminal"), title: localize('permissions.default.sandbox.toggle.title', "Run terminal commands inside a sandbox that restricts file system and network access"), - checked: this.isSandboxingEnabled(), + checked: sandboxSettingId ? this.isSandboxingEnabled(sandboxSettingId) : false, onChange: (checked: boolean) => { void setSandboxEnabled(checked); }, } : undefined; @@ -276,14 +288,23 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); this._register(configurationService.onDidChangeConfiguration(e => { - if ((e.affectsConfiguration(getSandboxEnabledSettingId()) || e.affectsConfiguration(ChatConfiguration.PermissionsSandboxToggleEnabled)) && this.element) { + const sandboxSettingId = this.getSandboxSettingId(); + if ((e.affectsConfiguration(ChatConfiguration.PermissionsSandboxToggleEnabled) + || e.affectsConfiguration(AgentHostCustomTerminalToolEnabledSettingId) + || (sandboxSettingId && e.affectsConfiguration(sandboxSettingId))) && this.element) { this.renderLabel(this.element); } })); } - private isSandboxingEnabled(): boolean { - const value = this.configurationService.getValue(getSandboxEnabledSettingId()); + private getSandboxSettingId(): SandboxSettingId | undefined { + const sessionType = this.delegate.getSandboxSessionType?.(); + const customTerminalToolEnabled = this.configurationService.getValue(AgentHostCustomTerminalToolEnabledSettingId) === true; + return getPermissionSandboxSettingId(sessionType, customTerminalToolEnabled); + } + + private isSandboxingEnabled(settingId: SandboxSettingId): boolean { + const value = this.configurationService.getValue(settingId); return isAgentSandboxEnabledValue(value); } @@ -291,15 +312,6 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { return this.configurationService.getValue(ChatConfiguration.PermissionsSandboxToggleEnabled) === true; } - /** - * Whether the sandbox toggle should surface for the current harness: the - * experimental setting must be on and the delegate must opt in (only the - * local harness does). - */ - private isSandboxToggleAvailable(): boolean { - return this.isSandboxToggleSettingEnabled() && this.delegate.isSandboxToggleApplicable?.() === true; - } - protected override renderLabel(element: HTMLElement): IDisposable | null { this.setAriaLabelAttributes(element); @@ -320,7 +332,8 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { icon = meta.icon; label = meta.shortLabel; tooltip = this.delegate.getPermissionLevelHover?.(level, meta) ?? meta.description; - if (level === ChatPermissionLevel.Default && this.isSandboxToggleAvailable() && this.isSandboxingEnabled()) { + const sandboxSettingId = this.getSandboxSettingId(); + if (level === ChatPermissionLevel.Default && this.isSandboxToggleSettingEnabled() && sandboxSettingId && this.isSandboxingEnabled(sandboxSettingId)) { label = localize('permissions.defaultSandboxed.label', "Default permissions (sandboxed)"); } } From a8242f4f62ceb4b9e3559cb0a8eca1971e6b8e9f Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Mon, 10 Aug 2026 17:35:04 -0700 Subject: [PATCH 4/9] Sandbox updates --- .../node/copilot/sandboxConfigForSdk.ts | 110 +++++++++++++++++- .../test/node/sandboxConfigForSdk.test.ts | 106 ++++++++++------- .../agentHost/agentHostChatInputPicker.ts | 80 ++++++++++++- .../chat/browser/chat.shared.contribution.ts | 2 +- .../browser/widget/input/chatInputPart.ts | 8 +- .../agentHostChatInputPicker.test.ts | 14 ++- .../input/permissionPickerActionItem.test.ts | 33 ++++++ 7 files changed, 293 insertions(+), 60 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/widget/input/permissionPickerActionItem.test.ts diff --git a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts index 45a1d8fae63be2..fa91193878be89 100644 --- a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts +++ b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { CopilotSession } from '@github/copilot-sdk'; import { AgentSandboxEnabledValue } from '../../../sandbox/common/settings.js'; import { AgentHostSandboxKey, type ISandboxConfigValue } from '../../common/sandboxConfigSchema.js'; @@ -20,9 +19,90 @@ export interface IAgentSandboxFileSystemSetting { denyWrite?: string[]; } -type SdkSandboxConfig = NonNullable[0]['sandboxConfig']>; +export interface SandboxConfig { + /** Whether to auto-add the current working directory to readwritePaths. Default: true. */ + addCurrentWorkingDirectory?: boolean; + /** Whether to auto-grant read access to common developer-tool caches, registries, and toolchains in their default home locations (cargo, go, npm, Maven, and more), plus read-write access to (and, on Unix, up-front creation of) the scratch caches builds write on every run (go-build, ccache, sccache, Gradle caches, Cargo lock/tracker files), so builds work without extra configuration; a relocated CARGO_HOME additionally gets its Cargo lock files granted read-write. Default: true (enabled by default; set to false to opt out). */ + allowDevToolAccess?: boolean; + /** Credential-injection capability flags. */ + auth?: SandboxConfigAuth; + /** Whether sandboxing is enabled for the session. */ + enabled: boolean; + /** User-managed sandbox policy fragment merged into the auto-discovered base policy. */ + userPolicy?: SandboxConfigUserPolicy; +} + +/** User-managed sandbox policy fragment merged into the auto-discovered base policy. */ +export interface SandboxConfigUserPolicy { + /** Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. */ + experimental?: SandboxConfigUserPolicyExperimental; + /** Filesystem rules to merge into the base policy. */ + filesystem?: SandboxConfigUserPolicyFilesystem; + /** Network rules to merge into the base policy. */ + network?: SandboxConfigUserPolicyNetwork; + /** macOS seatbelt options to merge into the base policy. */ + seatbelt?: SandboxConfigUserPolicySeatbelt; +} + +/** Platform-specific experimental policy fields. */ +export interface SandboxConfigUserPolicyExperimental { + /** macOS seatbelt experimental options. */ + seatbelt?: SandboxConfigUserPolicyExperimentalSeatbelt; +} + +/** macOS seatbelt experimental options. */ +export interface SandboxConfigUserPolicyExperimentalSeatbelt { + /** Whether the macOS seatbelt profile may access the keychain. */ + keychainAccess?: boolean; +} + +/** Filesystem rules to merge into the base policy. */ +export interface SandboxConfigUserPolicyFilesystem { + /** Whether to clear the policy when the session exits. */ + clearPolicyOnExit?: boolean; + /** Paths explicitly denied. */ + deniedPaths?: string[]; + /** Paths granted read-only access. */ + readonlyPaths?: string[]; + /** Paths granted read/write access. */ + readwritePaths?: string[]; +} + +/** Network rules to merge into the base policy. */ +export interface SandboxConfigUserPolicyNetwork { + /** Whether traffic to local/loopback addresses is allowed. */ + allowLocalNetwork?: boolean; + /** Whether outbound network traffic is allowed at all. */ + allowOutbound?: boolean; + /** HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. */ + proxy?: SandboxConfigUserPolicyNetworkProxy; +} + +/** HTTP proxy configuration for sandboxed traffic. */ +export interface SandboxConfigUserPolicyNetworkProxy { + /** Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. */ + password?: string; + /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ + url: string; + /** Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. */ + username?: string; +} + +/** macOS seatbelt-specific options. */ +export interface SandboxConfigUserPolicySeatbelt { + /** Whether the macOS seatbelt profile may access the keychain. */ + keychainAccess?: boolean; +} -export type CopilotSandboxConfig = SdkSandboxConfig & { +/** Credential-injection capability flags applied while the sandbox is enabled. */ +export interface SandboxConfigAuth { + /** Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the OS keyring the sandbox blocks. Default: false (opt-in). */ + gh?: boolean; + /** Whether to inject git credentials as an `http..extraheader` so authenticated HTTPS git works inside the sandbox without the shell-based credential helper the sandbox blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's own helper before the sandbox is applied. Default: false (opt-in). */ + git?: boolean; +} + +export type CopilotSandboxConfig = SandboxConfig & { readonly allowBypass?: boolean; }; @@ -95,15 +175,33 @@ export function buildSandboxConfigForSdk( const allowAllNetwork = enabledRaw === AgentSandboxEnabledValue.AllowNetwork || sandbox[AgentHostSandboxKey.AllowNetwork] === true; return { + addCurrentWorkingDirectory: true, + allowBypass: true, + allowDevToolAccess: true, + auth: { + gh: false, + git: false, + }, enabled: true, userPolicy: { + experimental: { + seatbelt: { + keychainAccess: false, + }, + }, filesystem: { - ...(readwrite.size ? { readwritePaths: [...readwrite] } : {}), - ...(readonly.size ? { readonlyPaths: [...readonly] } : {}), - ...(denied.size ? { deniedPaths: [...denied] } : {}), + clearPolicyOnExit: false, + deniedPaths: [...denied], + readonlyPaths: [...readonly], + readwritePaths: [...readwrite], }, network: { + allowLocalNetwork: false, allowOutbound: allowAllNetwork, + proxy: undefined, + }, + seatbelt: { + keychainAccess: false, }, }, }; diff --git a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts index 440aa8c9700b45..fad389f33b5b99 100644 --- a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts +++ b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts @@ -49,6 +49,45 @@ function sandbox( return cfg; } +function expectedSandboxConfig(options?: { + readwritePaths?: string[]; + readonlyPaths?: string[]; + deniedPaths?: string[]; + allowOutbound?: boolean; +}) { + return { + addCurrentWorkingDirectory: true, + allowBypass: true, + allowDevToolAccess: true, + auth: { + gh: false, + git: false, + }, + enabled: true, + userPolicy: { + experimental: { + seatbelt: { + keychainAccess: false, + }, + }, + filesystem: { + clearPolicyOnExit: false, + deniedPaths: options?.deniedPaths ?? [], + readonlyPaths: options?.readonlyPaths ?? [], + readwritePaths: options?.readwritePaths ?? [], + }, + network: { + allowLocalNetwork: false, + allowOutbound: options?.allowOutbound ?? false, + proxy: undefined, + }, + seatbelt: { + keychainAccess: false, + }, + }, + }; +} + suite('buildSandboxConfigForSdk', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -70,19 +109,13 @@ suite('buildSandboxConfigForSdk', () => { 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)), { - enabled: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On)), expectedSandboxConfig()); } }); test('normalizes the legacy `allowNetwork` mode to enabled with outbound network', () => { for (const platform of ['darwin', 'linux', 'win32'] as const) { - assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.AllowNetwork)), { - enabled: true, - userPolicy: { filesystem: {}, network: { allowOutbound: true } }, - }); + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.AllowNetwork)), expectedSandboxConfig({ allowOutbound: true })); } }); @@ -91,10 +124,7 @@ suite('buildSandboxConfigForSdk', () => { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.Off, [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On, }; - assert.deepStrictEqual(buildSandboxConfigForSdk('win32', cfg), { - enabled: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + assert.deepStrictEqual(buildSandboxConfigForSdk('win32', cfg), expectedSandboxConfig()); }); test('does not fall back to the non-Windows enable setting on Windows', () => { @@ -113,10 +143,9 @@ suite('buildSandboxConfigForSdk', () => { [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', () => { @@ -126,24 +155,15 @@ suite('buildSandboxConfigForSdk', () => { denyWrite: ['/readonly'], denyRead: ['/secret'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs)), { - enabled: 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, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + test('uses defaults for empty filesystem lists', () => { + assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, {})), expectedSandboxConfig()); }); test('denyRead wins over every other setting for the same path', () => { @@ -153,9 +173,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', () => { @@ -164,9 +182,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', () => { @@ -174,9 +190,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', () => { @@ -184,10 +198,10 @@ 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); }); }); @@ -195,7 +209,9 @@ suite('buildSandboxConfigForSdk', () => { test('drops host lists and keeps outbound closed when sandbox is `on` (host lists disabled on all platforms)', () => { 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, { + allowLocalNetwork: false, allowOutbound: false, + proxy: undefined, }, platform); } }); @@ -203,14 +219,18 @@ suite('buildSandboxConfigForSdk', () => { 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, { + allowLocalNetwork: false, allowOutbound: true, + proxy: undefined, }, platform); } }); test('ignores empty host lists', () => { assert.deepStrictEqual(buildSandboxConfigForSdk('linux', sandbox('linux', AgentSandboxEnabledValue.On, undefined, { allowedHosts: [], blockedHosts: [] }))?.userPolicy?.network, { + allowLocalNetwork: false, allowOutbound: false, + proxy: undefined, }); }); }); 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..5ef68027ba8835 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostChatInputPicker.ts @@ -15,10 +15,11 @@ import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposab 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 { 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,6 +32,7 @@ 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, 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'; @@ -38,8 +40,9 @@ import type { IChatWidget } from '../../chat.js'; import { ChatConfiguration, ChatPermissionLevel, isChatPermissionLevel } from '../../../common/constants.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 { getPermissionSandboxSettingId } from '../../widget/input/permissionPickerActionItem.js'; import { IAgentHostSessionWorkingDirectoryResolver } from './agentHostSessionWorkingDirectoryResolver.js'; import { IAgentHostNewSessionFolderService } from './agentHostNewSessionFolderService.js'; import { IAgentHostUntitledProvisionalSessionService } from './agentHostUntitledProvisionalSessionService.js'; @@ -97,7 +100,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 +111,16 @@ 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; +} + function isSelectedValue(currentValue: unknown | undefined, itemValue: string): boolean { if (typeof currentValue === 'boolean') { return currentValue === (itemValue === 'true'); @@ -307,6 +315,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 +348,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 +363,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 +461,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 +496,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 +524,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 +614,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 +647,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 +674,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 getPermissionSandboxSettingId(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 fcc1cfdf371505..3c6f10f053ad47 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'; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 6677f6b6414ea6..0182ac62567c61 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -722,7 +722,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private set _currentSessionType(value: string | undefined) { this._currentSessionTypeObservable.set(value, undefined); } private readonly _currentSessionResourceObservable = observableValue(this, undefined); - private readonly _notificationModelTargetChatSessionType = derived(this, reader => + private readonly _modelTargetChatSessionType = derived(this, reader => this._pendingDelegationTargetObservable.read(reader) ?? this._currentSessionTypeObservable.read(reader) ?? this.getCurrentSessionType() @@ -2243,7 +2243,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Auto-dismiss notifications that requested it. Scope to this input's // session so a message here doesn't hide notifications for other sessions. this.chatInputNotificationService.handleMessageSent({ - sessionType: this._notificationModelTargetChatSessionType.get(), + sessionType: this._modelTargetChatSessionType.get(), sessionResource: this._currentSessionResourceObservable.get(), }); @@ -2747,7 +2747,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // the user creates a session and `sessionTypes`-gated // notifications never render. this._notificationWidget.value = this.instantiationService.createInstance(ChatInputNotificationWidget, { - modelTargetChatSessionType: this._notificationModelTargetChatSessionType, + modelTargetChatSessionType: this._modelTargetChatSessionType, sessionResource: this._currentSessionResourceObservable, openModelPicker: () => this.openModelPicker(), switchToModel: modelIdentifier => this.switchModelByIdentifier(modelIdentifier, /* storeSelection */ true, /* isUserAction */ true), @@ -3571,7 +3571,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this.permissionWidget?.refresh(); }, - getSandboxSessionType: () => this.getEffectiveSessionType(this.getCurrentSessionResource()), + getSandboxSessionType: () => this._modelTargetChatSessionType.get(), }; const widget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, secondaryPickerOptions); this.permissionWidget = widget; 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..3fd3ea5815c5b7 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,7 @@ 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 { getConfigPickerItemHover, getConfigPickerListOptions, getConfigPickerTriggerHover, isAgentHostSandboxToggleItem, resolveConfigChipValue } from '../../../browser/agentSessions/agentHost/agentHostChatInputPicker.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 +55,18 @@ 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, + }); + }); }); suite('AgentHostChatInputPicker - resolveConfigChipValue', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/permissionPickerActionItem.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/permissionPickerActionItem.test.ts new file mode 100644 index 00000000000000..95c610dd8a68f8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/permissionPickerActionItem.test.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId } from '../../../../../../../platform/agentHost/common/agentService.js'; +import { AgentSandboxSettingId } from '../../../../../../../platform/sandbox/common/settings.js'; +import { getPermissionSandboxSettingId } from '../../../../browser/widget/input/permissionPickerActionItem.js'; +import { SessionType } from '../../../../common/chatSessionsService.js'; + +suite('PermissionPickerActionItem', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('resolves the sandbox setting for supported session types', () => { + assert.deepStrictEqual({ + local: getPermissionSandboxSettingId(SessionType.Local, false, false), + localWindows: getPermissionSandboxSettingId(SessionType.Local, false, true), + copilotSdk: getPermissionSandboxSettingId(SessionType.AgentHostCopilot, false, false), + copilotSdkWindows: getPermissionSandboxSettingId(SessionType.AgentHostCopilot, false, true), + copilotCustomTerminal: getPermissionSandboxSettingId(SessionType.AgentHostCopilot, true, false), + claude: getPermissionSandboxSettingId(SessionType.AgentHostClaude, false, false), + }, { + local: AgentSandboxSettingId.AgentSandboxEnabled, + localWindows: AgentSandboxSettingId.AgentSandboxWindowsEnabled, + copilotSdk: AgentHostSdkSandboxEnabledSettingId, + copilotSdkWindows: AgentHostSdkSandboxWindowsEnabledSettingId, + copilotCustomTerminal: AgentSandboxSettingId.AgentSandboxEnabled, + claude: undefined, + }); + }); +}); From cf7c1e4136d3f2c4e18c7abcce96c41bcede953e Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Mon, 10 Aug 2026 18:22:10 -0700 Subject: [PATCH 5/9] Sandbox updates --- .../platform/agentHost/common/agentService.ts | 24 ------- .../agentHost/common/sandboxConfigSchema.ts | 6 +- .../node/copilot/sandboxConfigForSdk.ts | 8 ++- .../test/node/copilotShellTools.test.ts | 4 +- .../test/node/sandboxConfigForSdk.test.ts | 15 ++++- src/vs/platform/sandbox/common/settings.ts | 1 - .../sandbox/common/terminalSandboxEngine.ts | 8 +-- .../agentHost/agentHostChatInputPicker.ts | 27 ++++++-- .../browser/widget/input/chatInputPart.ts | 8 +-- .../input/permissionPickerActionItem.ts | 63 ++++++++----------- .../agentHostChatInputPicker.test.ts | 21 ++++++- .../input/permissionPickerActionItem.test.ts | 33 ---------- .../terminal/common/terminalConfiguration.ts | 4 +- .../browser/agentHostSandboxForwarder.ts | 9 +-- .../common/sandboxSettingsReader.ts | 2 +- .../browser/agentHostSandboxForwarder.test.ts | 25 +++++--- .../browser/terminalSandboxService.test.ts | 6 +- 17 files changed, 118 insertions(+), 146 deletions(-) delete mode 100644 src/vs/workbench/contrib/chat/test/browser/widget/input/permissionPickerActionItem.test.ts diff --git a/src/vs/platform/agentHost/common/agentService.ts b/src/vs/platform/agentHost/common/agentService.ts index 27ba7513071e3c..fff4b090a0d2ff 100644 --- a/src/vs/platform/agentHost/common/agentService.ts +++ b/src/vs/platform/agentHost/common/agentService.ts @@ -227,30 +227,6 @@ export const AgentHostSdkSandboxEnabledSettingId = 'chat.agentHost.sdkSandbox.en */ export const AgentHostSdkSandboxWindowsEnabledSettingId = 'chat.agentHost.sdkSandbox.enabledWindows'; -/** - * Selects which Claude integration fulfills Claude sessions opened from the - * **Agents Window**: - * - `true` (default) — Claude is provided by the agent host process. - * - `false` — Claude is provided by the GitHub Copilot Chat extension. - * - * When Agent Host is enabled, this controls whether the per-window bridge in - * `AgentHostContribution` surfaces the AH provider in the Agents Window. The - * extension's `chatSessions` contribution mirrors the rule declaratively and - * remains visible when Agent Host is unavailable. - * - * Paired with {@link ClaudePreferAgentHostEditorSettingId} which governs the - * regular workbench (sidebar). EXP-backed (`experiment: { mode: 'startup' }`). - */ -export const ClaudePreferAgentHostAgentsSettingId = 'chat.agents.claude.preferAgentHost'; - -/** - * Sibling of {@link ClaudePreferAgentHostAgentsSettingId} that selects the - * Claude implementation for the **regular workbench** (sidebar chat in a - * non-Agents-Window window). Same shape, same semantics — just a different - * surface scope. - */ -export const ClaudePreferAgentHostEditorSettingId = 'chat.editor.claude.preferAgentHost'; - /** * 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 fa91193878be89..b2841f985ff3d3 100644 --- a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts +++ b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts @@ -19,6 +19,9 @@ export interface IAgentSandboxFileSystemSetting { denyWrite?: string[]; } +// These interfaces mirror the sandbox configuration contract currently accepted +// by the Copilot SDK. Replace them with the SDK-exported types once those become +// available so this file refers to the SDK contract directly. export interface SandboxConfig { /** Whether to auto-add the current working directory to readwritePaths. Default: true. */ addCurrentWorkingDirectory?: boolean; @@ -124,7 +127,6 @@ export type CopilotSandboxConfig = SandboxConfig & { * Each path appears in exactly one of `deniedPaths` / `readonlyPaths` / * `readwritePaths`. * - Network: the separate `allowNetwork` policy opens outbound to everything. - * The legacy `allowNetwork` enablement value is treated equivalently. * Domain allow/deny lists are ignored because the SDK's `SandboxConfig` * does not support host-level rules. * @@ -143,7 +145,7 @@ export function buildSandboxConfigForSdk( const enabledRaw = platform === 'win32' ? sandbox[AgentHostSandboxKey.WindowsEnabled] : sandbox[AgentHostSandboxKey.Enabled]; - if (enabledRaw !== AgentSandboxEnabledValue.On && enabledRaw !== AgentSandboxEnabledValue.AllowNetwork) { + if (enabledRaw !== AgentSandboxEnabledValue.On) { return undefined; } @@ -173,7 +175,7 @@ export function buildSandboxConfigForSdk( } } - const allowAllNetwork = enabledRaw === AgentSandboxEnabledValue.AllowNetwork || sandbox[AgentHostSandboxKey.AllowNetwork] === true; + const allowAllNetwork = sandbox[AgentHostSandboxKey.AllowNetwork] === true; return { addCurrentWorkingDirectory: true, allowBypass: true, 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 fad389f33b5b99..d84c1854c68e3d 100644 --- a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts +++ b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts @@ -24,6 +24,7 @@ function sandbox( enabled: AgentSandboxEnabledValue | undefined, fs?: IAgentSandboxFileSystemSetting, hosts?: { allowedHosts?: readonly string[]; blockedHosts?: readonly string[] }, + allowNetwork?: boolean, ): ISandboxConfigValue | undefined { if (!enabled && !fs && !hosts) { return undefined; @@ -46,6 +47,9 @@ function sandbox( if (hosts?.blockedHosts?.length) { cfg[AgentHostSandboxKey.DeniedNetworkDomains] = [...hosts.blockedHosts]; } + if (allowNetwork !== undefined) { + cfg[AgentHostSandboxKey.AllowNetwork] = allowNetwork; + } return cfg; } @@ -107,15 +111,20 @@ suite('buildSandboxConfigForSdk', () => { assert.strictEqual(buildSandboxConfigForSdk('win32', sandbox('win32', AgentSandboxEnabledValue.Off)), undefined); }); + 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('normalizes the legacy `allowNetwork` mode to enabled with outbound network', () => { + 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.AllowNetwork)), expectedSandboxConfig({ allowOutbound: true })); + assert.deepStrictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On, undefined, undefined, true)), expectedSandboxConfig({ allowOutbound: true })); } }); @@ -218,7 +227,7 @@ suite('buildSandboxConfigForSdk', () => { 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, { allowLocalNetwork: false, allowOutbound: true, proxy: 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 5ef68027ba8835..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,14 @@ 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, 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'; @@ -32,17 +33,17 @@ 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, isAgentSandboxEnabledValue } from '../../../../../../platform/sandbox/common/settings.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 { getChatSessionType, isUntitledChatSession } from '../../../common/model/chatUri.js'; import { withChatInputPickerMotion } from '../../widget/input/chatInputPickerActionItem.js'; -import { getPermissionSandboxSettingId } from '../../widget/input/permissionPickerActionItem.js'; import { IAgentHostSessionWorkingDirectoryResolver } from './agentHostSessionWorkingDirectoryResolver.js'; import { IAgentHostNewSessionFolderService } from './agentHostNewSessionFolderService.js'; import { IAgentHostUntitledProvisionalSessionService } from './agentHostUntitledProvisionalSessionService.js'; @@ -121,6 +122,22 @@ export function isAgentHostSandboxToggleItem(property: string, value: string): b 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'); @@ -674,11 +691,11 @@ export class AgentHostChatInputPicker extends Disposable { ); } - private _getSandboxSettingId(): ReturnType { + private _getSandboxSettingId(): ReturnType { const sessionResource = this._widget.viewModel?.sessionResource; const sessionType = sessionResource ? getChatSessionType(sessionResource) : undefined; const customTerminalToolEnabled = this._configurationService.getValue(AgentHostCustomTerminalToolEnabledSettingId) === true; - return getPermissionSandboxSettingId(sessionType, customTerminalToolEnabled); + return getAgentHostSandboxSettingId(sessionType, customTerminalToolEnabled); } private _isSandboxToggleSettingEnabled(): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 0182ac62567c61..9df99585c4f377 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -722,7 +722,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private set _currentSessionType(value: string | undefined) { this._currentSessionTypeObservable.set(value, undefined); } private readonly _currentSessionResourceObservable = observableValue(this, undefined); - private readonly _modelTargetChatSessionType = derived(this, reader => + private readonly _notificationModelTargetChatSessionType = derived(this, reader => this._pendingDelegationTargetObservable.read(reader) ?? this._currentSessionTypeObservable.read(reader) ?? this.getCurrentSessionType() @@ -2243,7 +2243,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Auto-dismiss notifications that requested it. Scope to this input's // session so a message here doesn't hide notifications for other sessions. this.chatInputNotificationService.handleMessageSent({ - sessionType: this._modelTargetChatSessionType.get(), + sessionType: this._notificationModelTargetChatSessionType.get(), sessionResource: this._currentSessionResourceObservable.get(), }); @@ -2747,7 +2747,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // the user creates a session and `sessionTypes`-gated // notifications never render. this._notificationWidget.value = this.instantiationService.createInstance(ChatInputNotificationWidget, { - modelTargetChatSessionType: this._modelTargetChatSessionType, + modelTargetChatSessionType: this._notificationModelTargetChatSessionType, sessionResource: this._currentSessionResourceObservable, openModelPicker: () => this.openModelPicker(), switchToModel: modelIdentifier => this.switchModelByIdentifier(modelIdentifier, /* storeSelection */ true, /* isUserAction */ true), @@ -3571,7 +3571,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } this.permissionWidget?.refresh(); }, - getSandboxSessionType: () => this._modelTargetChatSessionType.get(), + isSandboxToggleApplicable: () => this.getEffectiveSessionType(this.getCurrentSessionResource()) === SessionType.Local, }; const widget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, secondaryPickerOptions); this.permissionWidget = widget; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 2567197d04a648..3a47d9154c9bb0 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -29,8 +29,6 @@ import { URI } from '../../../../../../base/common/uri.js'; import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; import { maybeConfirmElevatedPermissionLevel } from '../../../common/chatPermissionWarnings.js'; import { AgentSandboxEnabledSettingValue, AgentSandboxEnabledValue, AgentSandboxSettingId, isAgentSandboxEnabledValue } from '../../../../../../platform/sandbox/common/settings.js'; -import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId } from '../../../../../../platform/agentHost/common/agentService.js'; -import { AgentHostCustomTerminalToolEnabledSettingId } from '../../../../../../platform/agentHost/common/copilotCliConfig.js'; export interface IExtensionPermissionState { /** Stable identifier for the contributing chat session type, used to namespace action ids. */ @@ -64,10 +62,13 @@ export interface IPermissionPickerDelegate { readonly setExtensionPermission?: (groupId: string, item: IChatSessionProviderOptionItem) => void; readonly getPermissionLevelHover?: (level: ChatPermissionLevel, meta: IPermissionLevelMeta) => string | undefined; /** - * The session type used to select the sandbox setting for the experimental - * "Sandboxing for terminal" toggle. + * Whether the experimental "Sandboxing for terminal" toggle may be shown on + * the Default permissions option. The toggle is specific to the local harness + * (which runs the built-in terminal tool); agent-host harnesses such as + * Copilot CLI and Claude Code do not implement this and never show it. + * Evaluated each time the picker opens so a harness switch is reflected. */ - readonly getSandboxSessionType?: () => string | undefined; + readonly isSandboxToggleApplicable?: () => boolean; } /** Default level set offered when a delegate does not specify {@link IPermissionPickerDelegate.availableLevels}. */ @@ -139,20 +140,8 @@ function sanitizeIdSegment(value: string): string { return value.replace(/[^a-zA-Z0-9_-]/g, '_'); } -type SandboxSettingId = - | AgentSandboxSettingId.AgentSandboxEnabled - | AgentSandboxSettingId.AgentSandboxWindowsEnabled - | typeof AgentHostSdkSandboxEnabledSettingId - | typeof AgentHostSdkSandboxWindowsEnabledSettingId; - -export function getPermissionSandboxSettingId(sessionType: string | undefined, customTerminalToolEnabled: boolean, windows = isWindows): SandboxSettingId | undefined { - if (sessionType === SessionType.Local || (sessionType === SessionType.AgentHostCopilot && customTerminalToolEnabled)) { - return windows ? AgentSandboxSettingId.AgentSandboxWindowsEnabled : AgentSandboxSettingId.AgentSandboxEnabled; - } - if (sessionType === SessionType.AgentHostCopilot) { - return windows ? AgentHostSdkSandboxWindowsEnabledSettingId : AgentHostSdkSandboxEnabledSettingId; - } - return undefined; +function getSandboxEnabledSettingId(): AgentSandboxSettingId.AgentSandboxEnabled | AgentSandboxSettingId.AgentSandboxWindowsEnabled { + return isWindows ? AgentSandboxSettingId.AgentSandboxWindowsEnabled : AgentSandboxSettingId.AgentSandboxEnabled; } export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { @@ -207,12 +196,11 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { } const currentLevel = delegate.currentPermissionLevel.get(); const policyRestricted = isAutoApprovePolicyRestricted(); - const sandboxSettingId = this.getSandboxSettingId(); - const sandboxToggleEnabled = this.isSandboxToggleSettingEnabled() && sandboxSettingId !== undefined; + const sandboxToggleEnabled = this.isSandboxToggleAvailable(); const setSandboxEnabled = async (enableSandbox: boolean) => { const target: AgentSandboxEnabledValue = enableSandbox ? AgentSandboxEnabledValue.On : AgentSandboxEnabledValue.Off; - if (sandboxSettingId && this.isSandboxingEnabled(sandboxSettingId) !== enableSandbox) { - await configurationService.updateValue(sandboxSettingId, target); + if (this.isSandboxingEnabled() !== enableSandbox) { + await configurationService.updateValue(getSandboxEnabledSettingId(), target); } }; const levels = delegate.availableLevels ?? DEFAULT_PERMISSION_LEVELS; @@ -230,7 +218,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { ? { label: localize('permissions.default.sandbox.toggle', "Sandboxing for terminal"), title: localize('permissions.default.sandbox.toggle.title', "Run terminal commands inside a sandbox that restricts file system and network access"), - checked: sandboxSettingId ? this.isSandboxingEnabled(sandboxSettingId) : false, + checked: this.isSandboxingEnabled(), onChange: (checked: boolean) => { void setSandboxEnabled(checked); }, } : undefined; @@ -288,23 +276,14 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); this._register(configurationService.onDidChangeConfiguration(e => { - const sandboxSettingId = this.getSandboxSettingId(); - if ((e.affectsConfiguration(ChatConfiguration.PermissionsSandboxToggleEnabled) - || e.affectsConfiguration(AgentHostCustomTerminalToolEnabledSettingId) - || (sandboxSettingId && e.affectsConfiguration(sandboxSettingId))) && this.element) { + if ((e.affectsConfiguration(getSandboxEnabledSettingId()) || e.affectsConfiguration(ChatConfiguration.PermissionsSandboxToggleEnabled)) && this.element) { this.renderLabel(this.element); } })); } - private getSandboxSettingId(): SandboxSettingId | undefined { - const sessionType = this.delegate.getSandboxSessionType?.(); - const customTerminalToolEnabled = this.configurationService.getValue(AgentHostCustomTerminalToolEnabledSettingId) === true; - return getPermissionSandboxSettingId(sessionType, customTerminalToolEnabled); - } - - private isSandboxingEnabled(settingId: SandboxSettingId): boolean { - const value = this.configurationService.getValue(settingId); + private isSandboxingEnabled(): boolean { + const value = this.configurationService.getValue(getSandboxEnabledSettingId()); return isAgentSandboxEnabledValue(value); } @@ -312,6 +291,15 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { return this.configurationService.getValue(ChatConfiguration.PermissionsSandboxToggleEnabled) === true; } + /** + * Whether the sandbox toggle should surface for the current harness: the + * experimental setting must be on and the delegate must opt in (only the + * local harness does). + */ + private isSandboxToggleAvailable(): boolean { + return this.isSandboxToggleSettingEnabled() && this.delegate.isSandboxToggleApplicable?.() === true; + } + protected override renderLabel(element: HTMLElement): IDisposable | null { this.setAriaLabelAttributes(element); @@ -332,8 +320,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { icon = meta.icon; label = meta.shortLabel; tooltip = this.delegate.getPermissionLevelHover?.(level, meta) ?? meta.description; - const sandboxSettingId = this.getSandboxSettingId(); - if (level === ChatPermissionLevel.Default && this.isSandboxToggleSettingEnabled() && sandboxSettingId && this.isSandboxingEnabled(sandboxSettingId)) { + if (level === ChatPermissionLevel.Default && this.isSandboxToggleAvailable() && this.isSandboxingEnabled()) { label = localize('permissions.defaultSandboxed.label', "Default permissions (sandboxed)"); } } 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 3fd3ea5815c5b7..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, isAgentHostSandboxToggleItem, 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'; @@ -67,6 +70,22 @@ suite('AgentHostChatInputPicker - list options', () => { 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/chat/test/browser/widget/input/permissionPickerActionItem.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/permissionPickerActionItem.test.ts deleted file mode 100644 index 95c610dd8a68f8..00000000000000 --- a/src/vs/workbench/contrib/chat/test/browser/widget/input/permissionPickerActionItem.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { AgentHostSdkSandboxEnabledSettingId, AgentHostSdkSandboxWindowsEnabledSettingId } from '../../../../../../../platform/agentHost/common/agentService.js'; -import { AgentSandboxSettingId } from '../../../../../../../platform/sandbox/common/settings.js'; -import { getPermissionSandboxSettingId } from '../../../../browser/widget/input/permissionPickerActionItem.js'; -import { SessionType } from '../../../../common/chatSessionsService.js'; - -suite('PermissionPickerActionItem', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('resolves the sandbox setting for supported session types', () => { - assert.deepStrictEqual({ - local: getPermissionSandboxSettingId(SessionType.Local, false, false), - localWindows: getPermissionSandboxSettingId(SessionType.Local, false, true), - copilotSdk: getPermissionSandboxSettingId(SessionType.AgentHostCopilot, false, false), - copilotSdkWindows: getPermissionSandboxSettingId(SessionType.AgentHostCopilot, false, true), - copilotCustomTerminal: getPermissionSandboxSettingId(SessionType.AgentHostCopilot, true, false), - claude: getPermissionSandboxSettingId(SessionType.AgentHostClaude, false, false), - }, { - local: AgentSandboxSettingId.AgentSandboxEnabled, - localWindows: AgentSandboxSettingId.AgentSandboxWindowsEnabled, - copilotSdk: AgentHostSdkSandboxEnabledSettingId, - copilotSdkWindows: AgentHostSdkSandboxWindowsEnabledSettingId, - copilotCustomTerminal: AgentSandboxSettingId.AgentSandboxEnabled, - claude: undefined, - }); - }); -}); diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 1486e2b2fd31a5..39d2fba7e8ded1 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -717,7 +717,7 @@ Registry.as(WorkbenchExtensions.ConfigurationMi .registerConfigurationMigrations([{ key: TerminalContribSettingId.AgentSandboxEnabled, migrateFn: (value: unknown, valueAccessor) => { - if (value !== AgentSandboxEnabledValue.AllowNetwork) { + if (value !== 'allowNetwork') { return []; } const configurationKeyValuePairs: ConfigurationKeyValuePairs = [[TerminalContribSettingId.AgentSandboxEnabled, { value: AgentSandboxEnabledValue.On }]]; @@ -729,7 +729,7 @@ Registry.as(WorkbenchExtensions.ConfigurationMi }, { key: TerminalContribSettingId.AgentSandboxWindowsEnabled, migrateFn: (value: unknown, valueAccessor) => { - if (value !== AgentSandboxEnabledValue.AllowNetwork) { + if (value !== 'allowNetwork') { return []; } const configurationKeyValuePairs: ConfigurationKeyValuePairs = [[TerminalContribSettingId.AgentSandboxWindowsEnabled, { value: AgentSandboxEnabledValue.On }]]; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts index 9432f4bdf5310b..7323029e51abad 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/agentHostSandboxForwarder.ts @@ -176,8 +176,6 @@ export class AgentHostSandboxForwarder extends Disposable implements IWorkbenchC * 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. - * - legacy `'allowNetwork'` values are normalized to `'on'` plus the - * existing `allowNetwork: true` policy. */ private _computeDesired(): Record { const customTerminalToolEnabled = this._configurationService.getValue(AgentHostCustomTerminalToolEnabledSettingId) === true; @@ -187,16 +185,13 @@ export class AgentHostSandboxForwarder extends Disposable implements IWorkbenchC } const sdkSandbox = this._configurationService.getValue(AgentHostSdkSandboxEnabledSettingId) ?? AgentSandboxEnabledValue.Off; const windowsSdkSandbox = this._configurationService.getValue(AgentHostSdkSandboxWindowsEnabledSettingId) ?? AgentSandboxEnabledValue.Off; - const sdkSandboxEnabled = sdkSandbox === AgentSandboxEnabledValue.On || sdkSandbox === AgentSandboxEnabledValue.AllowNetwork; - const windowsSdkSandboxEnabled = windowsSdkSandbox === AgentSandboxEnabledValue.On || windowsSdkSandbox === AgentSandboxEnabledValue.AllowNetwork; + const sdkSandboxEnabled = sdkSandbox === AgentSandboxEnabledValue.On; + const windowsSdkSandboxEnabled = windowsSdkSandbox === AgentSandboxEnabledValue.On; if (!sdkSandboxEnabled && !windowsSdkSandboxEnabled) { return {}; } values[AgentHostSandboxKey.Enabled] = sdkSandboxEnabled ? AgentSandboxEnabledValue.On : AgentSandboxEnabledValue.Off; values[AgentHostSandboxKey.WindowsEnabled] = windowsSdkSandboxEnabled ? AgentSandboxEnabledValue.On : AgentSandboxEnabledValue.Off; - if (sdkSandbox === AgentSandboxEnabledValue.AllowNetwork || windowsSdkSandbox === AgentSandboxEnabledValue.AllowNetwork) { - values[AgentHostSandboxKey.AllowNetwork] = true; - } 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 4b6d4509d5d704..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 @@ -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'. }); @@ -525,7 +532,7 @@ suite('AgentHostSandboxForwarder', () => { }]); }); - test('normalizes the legacy SDK `allowNetwork` mode to `on` plus allowNetwork policy', () => { + test('forwards the separate allowNetwork policy when SDK sandboxing is on', () => { const { local, configurationService } = setup(disposables, { [AgentSandboxSettingId.AgentSandboxEnabled]: AgentSandboxEnabledValue.On, [AgentHostCustomTerminalToolEnabledSettingId]: false, @@ -537,12 +544,12 @@ suite('AgentHostSandboxForwarder', () => { })); 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, [{ 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)); From 5375eebb022ee662d6760126dce5ebbb8c85ab99 Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Tue, 11 Aug 2026 09:18:33 -0700 Subject: [PATCH 6/9] sandbox: fixing tests --- .../test/node/copilotAgentSession.test.ts | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index 2692d42cc58bf7..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,16 +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, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), buildSandboxConfigForSdk('linux', sandbox)); assert.deepStrictEqual(mockSession.permissionModeSetCalls, ['off']); }); @@ -3441,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'); @@ -3461,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), ], }); }); @@ -3533,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', @@ -3548,10 +3541,7 @@ suite('CopilotAgentSession', () => { sandbox: mockSession.sandboxConfigUpdates.at(-1), }, { permissionModes: ['off'], - sandbox: { - enabled: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }, + sandbox: buildSandboxConfigForSdk('linux', sandbox), }); }); @@ -3569,17 +3559,15 @@ suite('CopilotAgentSession', () => { }); 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.WindowsEnabled]: AgentSandboxEnabledValue.On } }, + rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox }, platform: 'win32', }); await session.send('hello', undefined, 'turn-1'); - assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { - enabled: true, - userPolicy: { filesystem: {}, network: { allowOutbound: false } }, - }); + assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), buildSandboxConfigForSdk('win32', sandbox)); }); test('per-request sandbox: explicitly disabled when the sandbox setting is off', async () => { From dab4a9bd57c5c8c185fc911d32a12448be2a51ad Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Wed, 12 Aug 2026 00:39:26 -0700 Subject: [PATCH 7/9] update to the sandbox settings --- .vscode/settings.json | 3 +- .../node/copilot/sandboxConfigForSdk.ts | 142 ++++-------------- .../test/node/sandboxConfigForSdk.test.ts | 105 ++++++------- .../chat/browser/chat.shared.contribution.ts | 2 +- 4 files changed, 83 insertions(+), 169 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d69353d3a6902..dd0646f328d276 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -215,5 +215,6 @@ ".github/skills/.local": true, ".agents/skills/.local": true, ".claude/skills/.local": true, - } + }, + "chat.agent.sandbox.enabled": "off" } diff --git a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts index b2841f985ff3d3..ff92467cd00fc6 100644 --- a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts +++ b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { CopilotSession } from '@github/copilot-sdk'; import { AgentSandboxEnabledValue } from '../../../sandbox/common/settings.js'; import { AgentHostSandboxKey, type ISandboxConfigValue } from '../../common/sandboxConfigSchema.js'; @@ -19,93 +20,9 @@ export interface IAgentSandboxFileSystemSetting { denyWrite?: string[]; } -// These interfaces mirror the sandbox configuration contract currently accepted -// by the Copilot SDK. Replace them with the SDK-exported types once those become -// available so this file refers to the SDK contract directly. -export interface SandboxConfig { - /** Whether to auto-add the current working directory to readwritePaths. Default: true. */ - addCurrentWorkingDirectory?: boolean; - /** Whether to auto-grant read access to common developer-tool caches, registries, and toolchains in their default home locations (cargo, go, npm, Maven, and more), plus read-write access to (and, on Unix, up-front creation of) the scratch caches builds write on every run (go-build, ccache, sccache, Gradle caches, Cargo lock/tracker files), so builds work without extra configuration; a relocated CARGO_HOME additionally gets its Cargo lock files granted read-write. Default: true (enabled by default; set to false to opt out). */ - allowDevToolAccess?: boolean; - /** Credential-injection capability flags. */ - auth?: SandboxConfigAuth; - /** Whether sandboxing is enabled for the session. */ - enabled: boolean; - /** User-managed sandbox policy fragment merged into the auto-discovered base policy. */ - userPolicy?: SandboxConfigUserPolicy; -} - -/** User-managed sandbox policy fragment merged into the auto-discovered base policy. */ -export interface SandboxConfigUserPolicy { - /** Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. */ - experimental?: SandboxConfigUserPolicyExperimental; - /** Filesystem rules to merge into the base policy. */ - filesystem?: SandboxConfigUserPolicyFilesystem; - /** Network rules to merge into the base policy. */ - network?: SandboxConfigUserPolicyNetwork; - /** macOS seatbelt options to merge into the base policy. */ - seatbelt?: SandboxConfigUserPolicySeatbelt; -} - -/** Platform-specific experimental policy fields. */ -export interface SandboxConfigUserPolicyExperimental { - /** macOS seatbelt experimental options. */ - seatbelt?: SandboxConfigUserPolicyExperimentalSeatbelt; -} - -/** macOS seatbelt experimental options. */ -export interface SandboxConfigUserPolicyExperimentalSeatbelt { - /** Whether the macOS seatbelt profile may access the keychain. */ - keychainAccess?: boolean; -} - -/** Filesystem rules to merge into the base policy. */ -export interface SandboxConfigUserPolicyFilesystem { - /** Whether to clear the policy when the session exits. */ - clearPolicyOnExit?: boolean; - /** Paths explicitly denied. */ - deniedPaths?: string[]; - /** Paths granted read-only access. */ - readonlyPaths?: string[]; - /** Paths granted read/write access. */ - readwritePaths?: string[]; -} - -/** Network rules to merge into the base policy. */ -export interface SandboxConfigUserPolicyNetwork { - /** Whether traffic to local/loopback addresses is allowed. */ - allowLocalNetwork?: boolean; - /** Whether outbound network traffic is allowed at all. */ - allowOutbound?: boolean; - /** HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. */ - proxy?: SandboxConfigUserPolicyNetworkProxy; -} - -/** HTTP proxy configuration for sandboxed traffic. */ -export interface SandboxConfigUserPolicyNetworkProxy { - /** Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. */ - password?: string; - /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ - url: string; - /** Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. */ - username?: string; -} - -/** macOS seatbelt-specific options. */ -export interface SandboxConfigUserPolicySeatbelt { - /** Whether the macOS seatbelt profile may access the keychain. */ - keychainAccess?: boolean; -} - -/** Credential-injection capability flags applied while the sandbox is enabled. */ -export interface SandboxConfigAuth { - /** Whether to export `GH_TOKEN` so the `gh` CLI authenticates inside the sandbox without the OS keyring the sandbox blocks. Default: false (opt-in). */ - gh?: boolean; - /** Whether to inject git credentials as an `http..extraheader` so authenticated HTTPS git works inside the sandbox without the shell-based credential helper the sandbox blocks. github.com is served by the Copilot token; every other forge (Azure DevOps, GitHub Enterprise Server, GitLab, ...) by a credential the host resolves from the user's own helper before the sandbox is applied. Default: false (opt-in). */ - git?: boolean; -} +type SdkSandboxConfig = NonNullable[0]['sandboxConfig']>; -export type CopilotSandboxConfig = SandboxConfig & { +export type CopilotSandboxConfig = SdkSandboxConfig & { readonly allowBypass?: boolean; }; @@ -154,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(); @@ -175,36 +93,28 @@ export function buildSandboxConfigForSdk( } } - const allowAllNetwork = sandbox[AgentHostSandboxKey.AllowNetwork] === true; + const allowNetwork = sandbox[AgentHostSandboxKey.AllowNetwork]; + const allowBypass = sandbox[AgentHostSandboxKey.AllowUnsandboxedCommands]; return { - addCurrentWorkingDirectory: true, - allowBypass: true, - allowDevToolAccess: true, - auth: { - gh: false, - git: false, - }, - enabled: true, - userPolicy: { - experimental: { - seatbelt: { - keychainAccess: false, + enabled: enabledRaw === AgentSandboxEnabledValue.On, + ...(typeof allowBypass === 'boolean' ? { allowBypass } : {}), + ...(hasFileSystemPolicy || typeof allowNetwork === 'boolean' + ? { + userPolicy: { + ...(hasFileSystemPolicy + ? { + filesystem: { + ...(denied.size ? { deniedPaths: [...denied] } : {}), + ...(readonly.size ? { readonlyPaths: [...readonly] } : {}), + ...(readwrite.size ? { readwritePaths: [...readwrite] } : {}), + }, + } + : {}), + ...(typeof allowNetwork === 'boolean' + ? { network: { allowOutbound: allowNetwork } } + : {}), }, - }, - filesystem: { - clearPolicyOnExit: false, - deniedPaths: [...denied], - readonlyPaths: [...readonly], - readwritePaths: [...readwrite], - }, - network: { - allowLocalNetwork: false, - allowOutbound: allowAllNetwork, - proxy: undefined, - }, - seatbelt: { - keychainAccess: false, - }, - }, + } + : {}), }; } diff --git a/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts b/src/vs/platform/agentHost/test/node/sandboxConfigForSdk.test.ts index d84c1854c68e3d..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 @@ -54,41 +54,38 @@ function sandbox( } 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 { - addCurrentWorkingDirectory: true, - allowBypass: true, - allowDevToolAccess: true, - auth: { - gh: false, - git: false, - }, enabled: true, - userPolicy: { - experimental: { - seatbelt: { - keychainAccess: false, + ...(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 } } + : {}), }, - }, - filesystem: { - clearPolicyOnExit: false, - deniedPaths: options?.deniedPaths ?? [], - readonlyPaths: options?.readonlyPaths ?? [], - readwritePaths: options?.readwritePaths ?? [], - }, - network: { - allowLocalNetwork: false, - allowOutbound: options?.allowOutbound ?? false, - proxy: undefined, - }, - seatbelt: { - keychainAccess: false, - }, - }, + } + : {}), }; } @@ -128,6 +125,22 @@ suite('buildSandboxConfigForSdk', () => { } }); + 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, @@ -152,9 +165,9 @@ suite('buildSandboxConfigForSdk', () => { [AgentHostSandboxKey.MacFileSystem]: { allowWrite: ['/mac'] }, [AgentHostSandboxKey.WindowsFileSystem]: { allowWrite: ['C:\\windows'] }, }; - 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); + 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', () => { @@ -171,8 +184,8 @@ suite('buildSandboxConfigForSdk', () => { })); }); - test('uses defaults for empty filesystem lists', () => { - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, {})), expectedSandboxConfig()); + 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', () => { @@ -182,7 +195,7 @@ suite('buildSandboxConfigForSdk', () => { denyWrite: ['/p'], denyRead: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ deniedPaths: ['/p'] }).userPolicy.filesystem); + 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', () => { @@ -191,7 +204,7 @@ suite('buildSandboxConfigForSdk', () => { allowWrite: ['/p'], denyWrite: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ readonlyPaths: ['/p'] }).userPolicy.filesystem); + 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', () => { @@ -199,7 +212,7 @@ suite('buildSandboxConfigForSdk', () => { allowRead: ['/p'], allowWrite: ['/p'], }; - assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ readwritePaths: ['/p'] }).userPolicy.filesystem); + 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', () => { @@ -210,37 +223,27 @@ suite('buildSandboxConfigForSdk', () => { assert.deepStrictEqual(buildSandboxConfigForSdk('darwin', sandbox('darwin', AgentSandboxEnabledValue.On, fs))?.userPolicy?.filesystem, expectedSandboxConfig({ readwritePaths: ['/work'], readonlyPaths: ['/shared'], - }).userPolicy.filesystem); + }).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, { - allowLocalNetwork: false, - allowOutbound: false, - proxy: undefined, - }, platform); + assert.strictEqual(buildSandboxConfigForSdk(platform, sandbox(platform, AgentSandboxEnabledValue.On, undefined, { allowedHosts: ['github.com'], blockedHosts: ['evil.example'] }))?.userPolicy?.network, undefined, platform); } }); 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.On, undefined, { allowedHosts: ['a.example'], blockedHosts: ['b.example'] }, true))?.userPolicy?.network, { - allowLocalNetwork: false, allowOutbound: true, - proxy: undefined, }, platform); } }); test('ignores empty host lists', () => { - assert.deepStrictEqual(buildSandboxConfigForSdk('linux', sandbox('linux', AgentSandboxEnabledValue.On, undefined, { allowedHosts: [], blockedHosts: [] }))?.userPolicy?.network, { - allowLocalNetwork: false, - allowOutbound: false, - proxy: undefined, - }); + assert.strictEqual(buildSandboxConfigForSdk('linux', sandbox('linux', AgentSandboxEnabledValue.On, undefined, { allowedHosts: [], blockedHosts: [] }))?.userPolicy?.network, undefined); }); }); }); 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 3c6f10f053ad47..45de2dff2148fa 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -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' From 48287a644b53c858d7edfb6876b7747840406ad1 Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Wed, 12 Aug 2026 00:51:21 -0700 Subject: [PATCH 8/9] Remove settings.json from the PR --- .vscode/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index dd0646f328d276..7d69353d3a6902 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -215,6 +215,5 @@ ".github/skills/.local": true, ".agents/skills/.local": true, ".claude/skills/.local": true, - }, - "chat.agent.sandbox.enabled": "off" + } } From 807f69aedd7298c2140dab40a1662f860c7b9c6c Mon Sep 17 00:00:00 2001 From: Dileep Yavanamandha Date: Wed, 12 Aug 2026 01:17:28 -0700 Subject: [PATCH 9/9] Removing legacy settings --- .../node/copilot/sandboxConfigForSdk.ts | 35 +++++++++---------- .../terminal/common/terminalConfiguration.ts | 25 ------------- 2 files changed, 16 insertions(+), 44 deletions(-) diff --git a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts index ff92467cd00fc6..00c523ef39b186 100644 --- a/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts +++ b/src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts @@ -95,26 +95,23 @@ export function buildSandboxConfigForSdk( 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: enabledRaw === AgentSandboxEnabledValue.On, + enabled: true, ...(typeof allowBypass === 'boolean' ? { allowBypass } : {}), - ...(hasFileSystemPolicy || typeof allowNetwork === 'boolean' - ? { - userPolicy: { - ...(hasFileSystemPolicy - ? { - filesystem: { - ...(denied.size ? { deniedPaths: [...denied] } : {}), - ...(readonly.size ? { readonlyPaths: [...readonly] } : {}), - ...(readwrite.size ? { readwritePaths: [...readwrite] } : {}), - }, - } - : {}), - ...(typeof allowNetwork === 'boolean' - ? { network: { allowOutbound: allowNetwork } } - : {}), - }, - } - : {}), + ...(userPolicy ? { userPolicy } : {}), }; } diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 39d2fba7e8ded1..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 !== '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 !== '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 = [];