Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .geminiignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages/core/src/services/scripts/*.exe
20 changes: 19 additions & 1 deletion docs/cli/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,25 @@ Cross-platform sandboxing with complete process isolation.
**Note**: Requires building the sandbox image locally or using a published image
from your organization's registry.

### 3. gVisor / runsc (Linux only)
### 3. Windows Native Sandbox (Windows only)

... **Troubleshooting and Side Effects:**

The Windows Native sandbox uses the `icacls` command to set a "Low Mandatory
Level" on files and directories it needs to write to.

- **Persistence**: These integrity level changes are persistent on the
filesystem. Even after the sandbox session ends, files created or modified by
the sandbox will retain their "Low" integrity level.
- **Manual Reset**: If you need to reset the integrity level of a file or
directory, you can use:
```powershell
icacls "C:\path\to\dir" /setintegritylevel Medium
```
- **System Folders**: The sandbox manager automatically skips setting integrity
levels on system folders (like `C:\Windows`) for safety.

### 4. gVisor / runsc (Linux only)

Strongest isolation available: runs containers inside a user-space kernel via
[gVisor](https://github.com/google/gvisor). gVisor intercepts all container
Expand Down
2 changes: 2 additions & 0 deletions docs/cli/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ they appear in the UI.

| UI Label | Setting | Description | Default |
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` |
| Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` |
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
Expand Down
13 changes: 12 additions & 1 deletion docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1105,10 +1105,21 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Legacy full-process sandbox execution environment. Set to a
boolean to enable or disable the sandbox, provide a string path to a sandbox
profile, or specify an explicit sandbox command (e.g., "docker", "podman",
"lxc").
"lxc", "windows-native").
- **Default:** `undefined`
- **Requires restart:** Yes

- **`tools.sandboxAllowedPaths`** (array):
- **Description:** List of additional paths that the sandbox is allowed to
access.
- **Default:** `[]`
- **Requires restart:** Yes

- **`tools.sandboxNetworkAccess`** (boolean):
- **Description:** Whether the sandbox is allowed to access the network.
- **Default:** `false`
- **Requires restart:** Yes

- **`tools.shell.enableInteractiveShell`** (boolean):
- **Description:** Use node-pty for an interactive shell experience. Fallback
to child_process still applies.
Expand Down
7 changes: 6 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,12 @@ export default tseslint.config(
},
},
{
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'],
files: [
'./scripts/**/*.js',
'packages/*/scripts/**/*.js',
'esbuild.config.js',
'packages/core/scripts/**/*.{js,mjs}',
],
languageOptions: {
globals: {
...globals.node,
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,19 @@ export async function loadCliConfig(
? defaultModel
: specifiedModel || defaultModel;
const sandboxConfig = await loadSandboxConfig(settings, argv);
if (sandboxConfig) {
const existingPaths = sandboxConfig.allowedPaths || [];
if (settings.tools.sandboxAllowedPaths?.length) {
sandboxConfig.allowedPaths = [
...new Set([...existingPaths, ...settings.tools.sandboxAllowedPaths]),
];
}
if (settings.tools.sandboxNetworkAccess !== undefined) {
sandboxConfig.networkAccess =
sandboxConfig.networkAccess || settings.tools.sandboxNetworkAccess;
}
}

const screenReader =
argv.screenReader !== undefined
? argv.screenReader
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/config/sandboxConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ describe('loadSandboxConfig', () => {
sandbox: {
enabled: true,
command: 'podman',
allowedPaths: [],
networkAccess: false,
},
},
},
Expand All @@ -353,6 +355,8 @@ describe('loadSandboxConfig', () => {
sandbox: {
enabled: true,
image: 'custom/image',
allowedPaths: [],
networkAccess: false,
},
},
},
Expand All @@ -367,6 +371,8 @@ describe('loadSandboxConfig', () => {
tools: {
sandbox: {
enabled: false,
allowedPaths: [],
networkAccess: false,
},
},
},
Expand All @@ -382,6 +388,7 @@ describe('loadSandboxConfig', () => {
sandbox: {
enabled: true,
allowedPaths: ['/settings-path'],
networkAccess: false,
},
},
},
Expand Down
19 changes: 16 additions & 3 deletions packages/cli/src/config/sandboxConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const VALID_SANDBOX_COMMANDS = [
'sandbox-exec',
'runsc',
'lxc',
'windows-native',
];

function isSandboxCommand(
Expand Down Expand Up @@ -75,8 +76,15 @@ function getSandboxCommand(
'gVisor (runsc) sandboxing is only supported on Linux',
);
}
// confirm that specified command exists
if (!commandExists.sync(sandbox)) {
// windows-native is only supported on Windows
if (sandbox === 'windows-native' && os.platform() !== 'win32') {
throw new FatalSandboxError(
'Windows native sandboxing is only supported on Windows',
);
}

// confirm that specified command exists (unless it's built-in)
if (sandbox !== 'windows-native' && !commandExists.sync(sandbox)) {
throw new FatalSandboxError(
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
);
Expand Down Expand Up @@ -149,7 +157,12 @@ export async function loadSandboxConfig(
customImage ??
packageJson?.config?.sandboxImageUri;

return command && image
const isNative =
command === 'windows-native' ||
command === 'sandbox-exec' ||
command === 'lxc';

return command && (image || isNative)
? { enabled: true, allowedPaths, networkAccess, command, image }
: undefined;
}
22 changes: 21 additions & 1 deletion packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,10 +1344,30 @@ const SETTINGS_SCHEMA = {
description: oneLine`
Legacy full-process sandbox execution environment.
Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile,
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc", "windows-native").
`,
showInDialog: false,
},
sandboxAllowedPaths: {
type: 'array',
label: 'Sandbox Allowed Paths',
category: 'Tools',
requiresRestart: true,
default: [] as string[],
description:
'List of additional paths that the sandbox is allowed to access.',
showInDialog: true,
items: { type: 'string' },
},
sandboxNetworkAccess: {
type: 'boolean',
label: 'Sandbox Network Access',
category: 'Tools',
requiresRestart: true,
default: false,
description: 'Whether the sandbox is allowed to access the network.',
showInDialog: true,
},
shell: {
type: 'object',
label: 'Shell',
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/ui/hooks/slashCommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,9 +505,7 @@ export const useSlashCommandProcessor = (
const props = result.props as Record<string, unknown>;
if (
!props ||
// eslint-disable-next-line no-restricted-syntax
typeof props['name'] !== 'string' ||
// eslint-disable-next-line no-restricted-syntax
typeof props['displayName'] !== 'string' ||
!props['definition']
) {
Expand Down
121 changes: 121 additions & 0 deletions packages/core/scripts/compile-windows-sandbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/* eslint-env node */

import { spawnSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
* Compiles the GeminiSandbox C# helper on Windows.
* This is used to provide native restricted token sandboxing.
*/
function compileWindowsSandbox() {
if (os.platform() !== 'win32') {
return;
}

const srcHelperPath = path.resolve(
__dirname,
'../src/services/scripts/GeminiSandbox.exe',
);
const distHelperPath = path.resolve(
__dirname,
'../dist/src/services/scripts/GeminiSandbox.exe',
);
const sourcePath = path.resolve(
__dirname,
'../src/services/scripts/GeminiSandbox.cs',
);

if (!fs.existsSync(sourcePath)) {
console.error(`Sandbox source not found at ${sourcePath}`);
return;
}

// Ensure directories exist
[srcHelperPath, distHelperPath].forEach((p) => {
const dir = path.dirname(p);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});

// Find csc.exe (C# Compiler) which is built into Windows .NET Framework
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
const cscPaths = [
'csc.exe', // Try in PATH first
path.join(
systemRoot,
'Microsoft.NET',
'Framework64',
'v4.0.30319',
'csc.exe',
),
path.join(
systemRoot,
'Microsoft.NET',
'Framework',
'v4.0.30319',
'csc.exe',
),
];

let csc = undefined;
for (const p of cscPaths) {
if (p === 'csc.exe') {
const result = spawnSync('where', ['csc.exe'], { stdio: 'ignore' });
if (result.status === 0) {
csc = 'csc.exe';
break;
}
} else if (fs.existsSync(p)) {
csc = p;
break;
}
}

if (!csc) {
console.warn(
'Windows C# compiler (csc.exe) not found. Native sandboxing will attempt to compile on first run.',
);
return;
}

console.log(`Compiling native Windows sandbox helper...`);
// Compile to src
let result = spawnSync(
csc,
[`/out:${srcHelperPath}`, '/optimize', sourcePath],
{
stdio: 'inherit',
},
);

if (result.status === 0) {
console.log('Successfully compiled GeminiSandbox.exe to src');
// Copy to dist if dist exists
const distDir = path.resolve(__dirname, '../dist');
if (fs.existsSync(distDir)) {
const distScriptsDir = path.dirname(distHelperPath);
if (!fs.existsSync(distScriptsDir)) {
fs.mkdirSync(distScriptsDir, { recursive: true });
}
fs.copyFileSync(srcHelperPath, distHelperPath);
console.log('Successfully copied GeminiSandbox.exe to dist');
}
} else {
console.error('Failed to compile Windows sandbox helper.');
}
}

compileWindowsSandbox();
Loading
Loading