Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,32 @@ 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
* commands and reads `chat.agent.sandbox.enabled` directly.
*/
export const AgentHostSdkSandboxEnabledSettingId = 'chat.agentHost.sdkSandbox.enabled';

/**
* Configuration key that controls the sandbox mode for the Copilot SDK's
* built-in shell tool on Windows. This is independent of
* {@link AgentHostSdkSandboxEnabledSettingId} so Windows support can be rolled
* out separately. Supported values are `'off'` and `'on'`; the default is
* `'off'`.
*/
export const AgentHostSdkSandboxWindowsEnabledSettingId = 'chat.agentHost.sdkSandbox.enabledWindows';

/**
* Selects whether the regular workbench surfaces Codex from the agent host
* instead of the OpenAI extension.
Expand Down
6 changes: 3 additions & 3 deletions src/vs/platform/agentHost/common/sandboxConfigSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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',
Expand Down
68 changes: 28 additions & 40 deletions src/vs/platform/agentHost/node/copilot/sandboxConfigForSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@ import type { CopilotSession } from '@github/copilot-sdk';
import { AgentSandboxEnabledValue } from '../../../sandbox/common/settings.js';
import { AgentHostSandboxKey, type ISandboxConfigValue } from '../../common/sandboxConfigSchema.js';

/**
* Whether the SDK sandbox is supported on Windows. Not enabled yet, so the
* builders bail out early on `win32`; the Windows handling is kept so support
* can be turned on by flipping this flag once the runtime is ready. Typed as
* `boolean` (not the `false` literal) so the Windows branches are not flagged
* as unreachable by control-flow narrowing.
*/
const WINDOWS_SANDBOX_SUPPORTED: boolean = false;

/**
* Per-platform filesystem rule bundle accepted under each `fileSystem.<os>`
* sub-key (`AgentHostSandboxKey.LinuxFileSystem` etc.) in the AgentHost root
Expand Down Expand Up @@ -52,16 +43,13 @@ export type CopilotSandboxConfig = SdkSandboxConfig & {
* - Path precedence: `denyRead` > `denyWrite` > `allowWrite` > `allowRead`.
* Each path appears in exactly one of `deniedPaths` / `readonlyPaths` /
* `readwritePaths`.
* - Network: `allowNetwork` opens outbound to everything and drops the
* allow/deny lists. Otherwise the allow/deny lists open outbound when
* set so they're actually enforced; host lists are currently disabled on
* all platforms (fail closed) because the runtime does not yet enforce
* them reliably everywhere.
* - Network: the separate `allowNetwork` policy opens outbound to everything.
* Domain allow/deny lists are ignored because the SDK's `SandboxConfig`
* does not support host-level rules.
*
* Windows is not supported yet, so this bails out early and returns `undefined`
* there. The Windows handling below is intentionally kept (and exercised when
* {@link WINDOWS_SANDBOX_SUPPORTED} is flipped) so support can be turned on once
* the runtime is ready.
* Windows uses its platform-specific enablement and filesystem settings. It
* does not fall back to the shared enablement setting so Windows rollout is
* controlled independently.
Comment thread
dileepyavan marked this conversation as resolved.
*/
export function buildSandboxConfigForSdk(
platform: NodeJS.Platform,
Expand All @@ -71,16 +59,10 @@ export function buildSandboxConfigForSdk(
return undefined;
}

// Typed as `boolean` (not the `false` literal) so the Windows branches below
// are not flagged as unreachable by control-flow narrowing.
if (platform === 'win32' && !WINDOWS_SANDBOX_SUPPORTED) {
return undefined;
}

const enabledRaw = platform === 'win32' && sandbox[AgentHostSandboxKey.WindowsEnabled] !== undefined
const enabledRaw = platform === 'win32'
? sandbox[AgentHostSandboxKey.WindowsEnabled]
: sandbox[AgentHostSandboxKey.Enabled];
if (enabledRaw !== AgentSandboxEnabledValue.On && enabledRaw !== AgentSandboxEnabledValue.AllowNetwork) {
if (enabledRaw !== AgentSandboxEnabledValue.On) {
return undefined;
}

Expand All @@ -89,7 +71,8 @@ export function buildSandboxConfigForSdk(
: platform === 'darwin'
? sandbox[AgentHostSandboxKey.MacFileSystem]
: sandbox[AgentHostSandboxKey.LinuxFileSystem];
const fs = (fsRaw && typeof fsRaw === 'object') ? fsRaw as IAgentSandboxFileSystemSetting : {};
const hasFileSystemPolicy = fsRaw !== undefined && typeof fsRaw === 'object';
const fs = hasFileSystemPolicy ? fsRaw as IAgentSandboxFileSystemSetting : {};

const denied = new Set<string>(fs.denyRead ?? []);
const readonly = new Set<string>();
Expand All @@ -110,20 +93,25 @@ export function buildSandboxConfigForSdk(
}
}

const legacyAllowAllNetwork = enabledRaw === AgentSandboxEnabledValue.AllowNetwork;
const allowAllNetwork = legacyAllowAllNetwork || (enabledRaw === AgentSandboxEnabledValue.On && sandbox[AgentHostSandboxKey.AllowNetwork] === true);
const allowNetwork = sandbox[AgentHostSandboxKey.AllowNetwork];
const allowBypass = sandbox[AgentHostSandboxKey.AllowUnsandboxedCommands];
const filesystem = hasFileSystemPolicy
? {
...(denied.size ? { deniedPaths: [...denied] } : {}),
...(readonly.size ? { readonlyPaths: [...readonly] } : {}),
...(readwrite.size ? { readwritePaths: [...readwrite] } : {}),
}
: undefined;
const network = typeof allowNetwork === 'boolean' ? { allowOutbound: allowNetwork } : undefined;
const userPolicy = filesystem || network
? {
...(filesystem ? { filesystem } : {}),
...(network ? { network } : {}),
}
: undefined;
return {
enabled: true,
allowBypass: true,
userPolicy: {
filesystem: {
...(readwrite.size ? { readwritePaths: [...readwrite] } : {}),
...(readonly.size ? { readonlyPaths: [...readonly] } : {}),
...(denied.size ? { deniedPaths: [...denied] } : {}),
},
network: {
allowOutbound: allowAllNetwork,
},
},
...(typeof allowBypass === 'boolean' ? { allowBypass } : {}),
...(userPolicy ? { userPolicy } : {}),
};
}
41 changes: 15 additions & 26 deletions src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -3045,17 +3046,14 @@ suite('CopilotAgentSession', () => {
});

test('per-request sandbox: applies the configured policy under default permissions', async () => {
const sandbox = { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On };
const { session, mockSession } = await createAgentSession(disposables, {
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } },
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox },
});

await session.send('hello', undefined, 'turn-1');

assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), {
enabled: true,
allowBypass: true,
userPolicy: { filesystem: {}, network: { allowOutbound: false } },
});
assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), buildSandboxConfigForSdk('linux', sandbox));
assert.deepStrictEqual(mockSession.permissionModeSetCalls, ['off']);
});

Expand Down Expand Up @@ -3442,8 +3440,9 @@ suite('CopilotAgentSession', () => {
});

test('syncs sandbox when the session approval level changes', async () => {
const sandbox = { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On };
const { session, mockSession, setConfigValue, fireSessionConfigChange } = await createAgentSession(disposables, {
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } },
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox },
configValues: { [SessionConfigKey.AutoApprove]: 'default' },
});
await session.send('hello', undefined, 'turn-1');
Expand All @@ -3462,17 +3461,9 @@ suite('CopilotAgentSession', () => {
}, {
permissionModes: ['off', 'on', 'off'],
sandboxConfigs: [
{
enabled: true,
allowBypass: true,
userPolicy: { filesystem: {}, network: { allowOutbound: false } },
},
buildSandboxConfigForSdk('linux', sandbox),
{ enabled: false },
{
enabled: true,
allowBypass: true,
userPolicy: { filesystem: {}, network: { allowOutbound: false } },
},
buildSandboxConfigForSdk('linux', sandbox),
],
});
});
Expand Down Expand Up @@ -3534,8 +3525,9 @@ suite('CopilotAgentSession', () => {
});

test('per-request permissions: Autopilot with Ask When Needed keeps SDK approval mode off', async () => {
const sandbox = { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On };
const { session, mockSession } = await createAgentSession(disposables, {
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } },
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox },
configValues: {
[SessionConfigKey.Mode]: 'autopilot',
[SessionConfigKey.AutoApprove]: 'default',
Expand All @@ -3549,11 +3541,7 @@ suite('CopilotAgentSession', () => {
sandbox: mockSession.sandboxConfigUpdates.at(-1),
}, {
permissionModes: ['off'],
sandbox: {
enabled: true,
allowBypass: true,
userPolicy: { filesystem: {}, network: { allowOutbound: false } },
},
sandbox: buildSandboxConfigForSdk('linux', sandbox),
});
});

Expand All @@ -3570,15 +3558,16 @@ suite('CopilotAgentSession', () => {
assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { enabled: false });
});

test('per-request sandbox: explicitly disabled on Windows', async () => {
test('per-request sandbox: applies the configured policy on Windows', async () => {
const sandbox = { [AgentHostSandboxKey.WindowsEnabled]: AgentSandboxEnabledValue.On };
const { session, mockSession } = await createAgentSession(disposables, {
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: { [AgentHostSandboxKey.Enabled]: AgentSandboxEnabledValue.On } },
rootValues: { [AgentHostSandboxConfigKey.Sandbox]: sandbox },
platform: 'win32',
});

await session.send('hello', undefined, 'turn-1');

assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), { enabled: false });
assert.deepStrictEqual(mockSession.sandboxConfigUpdates.at(-1), buildSandboxConfigForSdk('win32', sandbox));
});

test('per-request sandbox: explicitly disabled when the sandbox setting is off', async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/agentHost/test/node/copilotShellTools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading