Skip to content

Commit c9a3369

Browse files
authored
feat(core): implement native Windows sandboxing (#21807)
1 parent 06a7873 commit c9a3369

23 files changed

+1365
-149
lines changed

.geminiignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
packages/core/src/services/scripts/*.exe

docs/cli/sandbox.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,25 @@ Cross-platform sandboxing with complete process isolation.
5050
**Note**: Requires building the sandbox image locally or using a published image
5151
from your organization's registry.
5252

53-
### 3. gVisor / runsc (Linux only)
53+
### 3. Windows Native Sandbox (Windows only)
54+
55+
... **Troubleshooting and Side Effects:**
56+
57+
The Windows Native sandbox uses the `icacls` command to set a "Low Mandatory
58+
Level" on files and directories it needs to write to.
59+
60+
- **Persistence**: These integrity level changes are persistent on the
61+
filesystem. Even after the sandbox session ends, files created or modified by
62+
the sandbox will retain their "Low" integrity level.
63+
- **Manual Reset**: If you need to reset the integrity level of a file or
64+
directory, you can use:
65+
```powershell
66+
icacls "C:\path\to\dir" /setintegritylevel Medium
67+
```
68+
- **System Folders**: The sandbox manager automatically skips setting integrity
69+
levels on system folders (like `C:\Windows`) for safety.
70+
71+
### 4. gVisor / runsc (Linux only)
5472

5573
Strongest isolation available: runs containers inside a user-space kernel via
5674
[gVisor](https://github.com/google/gvisor). gVisor intercepts all container

docs/cli/settings.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ they appear in the UI.
117117

118118
| UI Label | Setting | Description | Default |
119119
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
120+
| Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` |
121+
| Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` |
120122
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
121123
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
122124
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |

docs/reference/configuration.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1276,10 +1276,21 @@ their corresponding top-level category object in your `settings.json` file.
12761276
- **Description:** Legacy full-process sandbox execution environment. Set to a
12771277
boolean to enable or disable the sandbox, provide a string path to a sandbox
12781278
profile, or specify an explicit sandbox command (e.g., "docker", "podman",
1279-
"lxc").
1279+
"lxc", "windows-native").
12801280
- **Default:** `undefined`
12811281
- **Requires restart:** Yes
12821282

1283+
- **`tools.sandboxAllowedPaths`** (array):
1284+
- **Description:** List of additional paths that the sandbox is allowed to
1285+
access.
1286+
- **Default:** `[]`
1287+
- **Requires restart:** Yes
1288+
1289+
- **`tools.sandboxNetworkAccess`** (boolean):
1290+
- **Description:** Whether the sandbox is allowed to access the network.
1291+
- **Default:** `false`
1292+
- **Requires restart:** Yes
1293+
12831294
- **`tools.shell.enableInteractiveShell`** (boolean):
12841295
- **Description:** Use node-pty for an interactive shell experience. Fallback
12851296
to child_process still applies.

eslint.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,12 @@ export default tseslint.config(
319319
},
320320
},
321321
{
322-
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'],
322+
files: [
323+
'./scripts/**/*.js',
324+
'packages/*/scripts/**/*.js',
325+
'esbuild.config.js',
326+
'packages/core/scripts/**/*.{js,mjs}',
327+
],
323328
languageOptions: {
324329
globals: {
325330
...globals.node,

packages/cli/src/config/config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,19 @@ export async function loadCliConfig(
702702
? defaultModel
703703
: specifiedModel || defaultModel;
704704
const sandboxConfig = await loadSandboxConfig(settings, argv);
705+
if (sandboxConfig) {
706+
const existingPaths = sandboxConfig.allowedPaths || [];
707+
if (settings.tools.sandboxAllowedPaths?.length) {
708+
sandboxConfig.allowedPaths = [
709+
...new Set([...existingPaths, ...settings.tools.sandboxAllowedPaths]),
710+
];
711+
}
712+
if (settings.tools.sandboxNetworkAccess !== undefined) {
713+
sandboxConfig.networkAccess =
714+
sandboxConfig.networkAccess || settings.tools.sandboxNetworkAccess;
715+
}
716+
}
717+
705718
const screenReader =
706719
argv.screenReader !== undefined
707720
? argv.screenReader

packages/cli/src/config/sandboxConfig.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,8 @@ describe('loadSandboxConfig', () => {
338338
sandbox: {
339339
enabled: true,
340340
command: 'podman',
341+
allowedPaths: [],
342+
networkAccess: false,
341343
},
342344
},
343345
},
@@ -353,6 +355,8 @@ describe('loadSandboxConfig', () => {
353355
sandbox: {
354356
enabled: true,
355357
image: 'custom/image',
358+
allowedPaths: [],
359+
networkAccess: false,
356360
},
357361
},
358362
},
@@ -367,6 +371,8 @@ describe('loadSandboxConfig', () => {
367371
tools: {
368372
sandbox: {
369373
enabled: false,
374+
allowedPaths: [],
375+
networkAccess: false,
370376
},
371377
},
372378
},
@@ -382,6 +388,7 @@ describe('loadSandboxConfig', () => {
382388
sandbox: {
383389
enabled: true,
384390
allowedPaths: ['/settings-path'],
391+
networkAccess: false,
385392
},
386393
},
387394
},

packages/cli/src/config/sandboxConfig.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const VALID_SANDBOX_COMMANDS = [
2929
'sandbox-exec',
3030
'runsc',
3131
'lxc',
32+
'windows-native',
3233
];
3334

3435
function isSandboxCommand(
@@ -75,8 +76,15 @@ function getSandboxCommand(
7576
'gVisor (runsc) sandboxing is only supported on Linux',
7677
);
7778
}
78-
// confirm that specified command exists
79-
if (!commandExists.sync(sandbox)) {
79+
// windows-native is only supported on Windows
80+
if (sandbox === 'windows-native' && os.platform() !== 'win32') {
81+
throw new FatalSandboxError(
82+
'Windows native sandboxing is only supported on Windows',
83+
);
84+
}
85+
86+
// confirm that specified command exists (unless it's built-in)
87+
if (sandbox !== 'windows-native' && !commandExists.sync(sandbox)) {
8088
throw new FatalSandboxError(
8189
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
8290
);
@@ -149,7 +157,12 @@ export async function loadSandboxConfig(
149157
customImage ??
150158
packageJson?.config?.sandboxImageUri;
151159

152-
return command && image
160+
const isNative =
161+
command === 'windows-native' ||
162+
command === 'sandbox-exec' ||
163+
command === 'lxc';
164+
165+
return command && (image || isNative)
153166
? { enabled: true, allowedPaths, networkAccess, command, image }
154167
: undefined;
155168
}

packages/cli/src/config/settingsSchema.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,10 +1358,30 @@ const SETTINGS_SCHEMA = {
13581358
description: oneLine`
13591359
Legacy full-process sandbox execution environment.
13601360
Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile,
1361-
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
1361+
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc", "windows-native").
13621362
`,
13631363
showInDialog: false,
13641364
},
1365+
sandboxAllowedPaths: {
1366+
type: 'array',
1367+
label: 'Sandbox Allowed Paths',
1368+
category: 'Tools',
1369+
requiresRestart: true,
1370+
default: [] as string[],
1371+
description:
1372+
'List of additional paths that the sandbox is allowed to access.',
1373+
showInDialog: true,
1374+
items: { type: 'string' },
1375+
},
1376+
sandboxNetworkAccess: {
1377+
type: 'boolean',
1378+
label: 'Sandbox Network Access',
1379+
category: 'Tools',
1380+
requiresRestart: true,
1381+
default: false,
1382+
description: 'Whether the sandbox is allowed to access the network.',
1383+
showInDialog: true,
1384+
},
13651385
shell: {
13661386
type: 'object',
13671387
label: 'Shell',
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/* eslint-env node */
8+
9+
import { spawnSync } from 'node:child_process';
10+
import path from 'node:path';
11+
import fs from 'node:fs';
12+
import os from 'node:os';
13+
import { fileURLToPath } from 'node:url';
14+
15+
const __filename = fileURLToPath(import.meta.url);
16+
const __dirname = path.dirname(__filename);
17+
18+
/**
19+
* Compiles the GeminiSandbox C# helper on Windows.
20+
* This is used to provide native restricted token sandboxing.
21+
*/
22+
function compileWindowsSandbox() {
23+
if (os.platform() !== 'win32') {
24+
return;
25+
}
26+
27+
const srcHelperPath = path.resolve(
28+
__dirname,
29+
'../src/services/scripts/GeminiSandbox.exe',
30+
);
31+
const distHelperPath = path.resolve(
32+
__dirname,
33+
'../dist/src/services/scripts/GeminiSandbox.exe',
34+
);
35+
const sourcePath = path.resolve(
36+
__dirname,
37+
'../src/services/scripts/GeminiSandbox.cs',
38+
);
39+
40+
if (!fs.existsSync(sourcePath)) {
41+
console.error(`Sandbox source not found at ${sourcePath}`);
42+
return;
43+
}
44+
45+
// Ensure directories exist
46+
[srcHelperPath, distHelperPath].forEach((p) => {
47+
const dir = path.dirname(p);
48+
if (!fs.existsSync(dir)) {
49+
fs.mkdirSync(dir, { recursive: true });
50+
}
51+
});
52+
53+
// Find csc.exe (C# Compiler) which is built into Windows .NET Framework
54+
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
55+
const cscPaths = [
56+
'csc.exe', // Try in PATH first
57+
path.join(
58+
systemRoot,
59+
'Microsoft.NET',
60+
'Framework64',
61+
'v4.0.30319',
62+
'csc.exe',
63+
),
64+
path.join(
65+
systemRoot,
66+
'Microsoft.NET',
67+
'Framework',
68+
'v4.0.30319',
69+
'csc.exe',
70+
),
71+
];
72+
73+
let csc = undefined;
74+
for (const p of cscPaths) {
75+
if (p === 'csc.exe') {
76+
const result = spawnSync('where', ['csc.exe'], { stdio: 'ignore' });
77+
if (result.status === 0) {
78+
csc = 'csc.exe';
79+
break;
80+
}
81+
} else if (fs.existsSync(p)) {
82+
csc = p;
83+
break;
84+
}
85+
}
86+
87+
if (!csc) {
88+
console.warn(
89+
'Windows C# compiler (csc.exe) not found. Native sandboxing will attempt to compile on first run.',
90+
);
91+
return;
92+
}
93+
94+
console.log(`Compiling native Windows sandbox helper...`);
95+
// Compile to src
96+
let result = spawnSync(
97+
csc,
98+
[`/out:${srcHelperPath}`, '/optimize', sourcePath],
99+
{
100+
stdio: 'inherit',
101+
},
102+
);
103+
104+
if (result.status === 0) {
105+
console.log('Successfully compiled GeminiSandbox.exe to src');
106+
// Copy to dist if dist exists
107+
const distDir = path.resolve(__dirname, '../dist');
108+
if (fs.existsSync(distDir)) {
109+
const distScriptsDir = path.dirname(distHelperPath);
110+
if (!fs.existsSync(distScriptsDir)) {
111+
fs.mkdirSync(distScriptsDir, { recursive: true });
112+
}
113+
fs.copyFileSync(srcHelperPath, distHelperPath);
114+
console.log('Successfully copied GeminiSandbox.exe to dist');
115+
}
116+
} else {
117+
console.error('Failed to compile Windows sandbox helper.');
118+
}
119+
}
120+
121+
compileWindowsSandbox();

0 commit comments

Comments
 (0)