mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat(core): implement progressive elevation and AI error awareness for Windows sandbox
This commit is contained in:
@@ -43,8 +43,14 @@ function isSandboxCommand(
|
||||
function getSandboxCommand(
|
||||
sandbox?: boolean | string | null,
|
||||
): SandboxConfig['command'] | '' {
|
||||
// If the SANDBOX env var is set, we're already inside the sandbox.
|
||||
if (process.env['SANDBOX']) {
|
||||
// If the SANDBOX env var is set, we're already inside a container-based sandbox.
|
||||
// For native sandboxing (windows-native, sandbox-exec), we still need the command
|
||||
// to be active in the child process to restrict tool calls.
|
||||
if (
|
||||
process.env['SANDBOX'] &&
|
||||
process.env['SANDBOX'] !== 'windows-native' &&
|
||||
process.env['SANDBOX'] !== 'sandbox-exec'
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -143,10 +149,15 @@ export async function loadSandboxConfig(
|
||||
const allowedPaths =
|
||||
allowedPathsEnv ?? settings.tools?.sandboxAllowedPaths ?? [];
|
||||
|
||||
const enabled =
|
||||
(sandboxOption !== undefined && sandboxOption !== false) ||
|
||||
command === 'windows-native' ||
|
||||
command === 'sandbox-exec';
|
||||
|
||||
return command &&
|
||||
(image || command === 'sandbox-exec' || command === 'windows-native')
|
||||
? {
|
||||
enabled: true,
|
||||
enabled,
|
||||
allowedPaths,
|
||||
networkAccess,
|
||||
command,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "node ../../scripts/build_package.js",
|
||||
"build": "node scripts/compile-windows-sandbox.js && node ../../scripts/build_package.js",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
|
||||
@@ -17,12 +17,13 @@ const __dirname = path.dirname(__filename);
|
||||
* Compiles the GeminiSandbox C# helper on Windows.
|
||||
* This is used to provide native restricted token sandboxing.
|
||||
*/
|
||||
function compileWindowsSandbox(): void {
|
||||
function compileWindowsSandbox() {
|
||||
if (os.platform() !== 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
const helperPath = path.resolve(__dirname, '../src/services/scripts/GeminiSandbox.exe');
|
||||
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)) {
|
||||
@@ -30,6 +31,14 @@ function compileWindowsSandbox(): void {
|
||||
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 = [
|
||||
@@ -45,14 +54,25 @@ function compileWindowsSandbox(): void {
|
||||
}
|
||||
|
||||
console.log(`Compiling native Windows sandbox helper...`);
|
||||
const result = spawnSync(csc, [`/out:${helperPath}`, '/optimize', sourcePath], {
|
||||
// Compile to src
|
||||
let result = spawnSync(csc, [`/out:${srcHelperPath}`, '/optimize', sourcePath], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error('Failed to compile Windows sandbox helper.');
|
||||
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.log('Successfully compiled GeminiSandbox.exe');
|
||||
console.error('Failed to compile Windows sandbox helper.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -974,13 +974,32 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.useAlternateBuffer = params.useAlternateBuffer ?? false;
|
||||
this.enableInteractiveShell = params.enableInteractiveShell ?? false;
|
||||
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true;
|
||||
|
||||
if (
|
||||
os.platform() === 'win32' &&
|
||||
(this.sandbox?.enabled || this.sandbox?.command === 'windows-native')
|
||||
) {
|
||||
this._sandboxManager = new WindowsSandboxManager();
|
||||
} else {
|
||||
this._sandboxManager = new NoopSandboxManager();
|
||||
}
|
||||
|
||||
if (this.sandbox?.enabled && this._sandboxManager) {
|
||||
this.fileSystemService = new SandboxedFileSystemService(
|
||||
this._sandboxManager,
|
||||
this.cwd,
|
||||
);
|
||||
} else {
|
||||
this.fileSystemService = new StandardFileSystemService();
|
||||
}
|
||||
|
||||
this.shellExecutionConfig = {
|
||||
terminalWidth: params.shellExecutionConfig?.terminalWidth ?? 80,
|
||||
terminalHeight: params.shellExecutionConfig?.terminalHeight ?? 24,
|
||||
showColor: params.shellExecutionConfig?.showColor ?? false,
|
||||
pager: params.shellExecutionConfig?.pager ?? 'cat',
|
||||
sanitizationConfig: this.sanitizationConfig,
|
||||
sandboxManager: this.sandboxManager,
|
||||
sandboxManager: this._sandboxManager,
|
||||
sandboxConfig: this.sandbox,
|
||||
};
|
||||
this.truncateToolOutputThreshold =
|
||||
@@ -1097,25 +1116,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
}
|
||||
this._geminiClient = new GeminiClient(this);
|
||||
if (
|
||||
os.platform() === 'win32' &&
|
||||
(this.sandbox?.enabled || this.sandbox?.command === 'windows-native')
|
||||
) {
|
||||
this._sandboxManager = new WindowsSandboxManager();
|
||||
} else {
|
||||
this._sandboxManager = new NoopSandboxManager();
|
||||
}
|
||||
|
||||
if (this.sandbox?.enabled && this._sandboxManager) {
|
||||
this.fileSystemService = new SandboxedFileSystemService(
|
||||
this._sandboxManager,
|
||||
this.cwd,
|
||||
);
|
||||
} else {
|
||||
this.fileSystemService = new StandardFileSystemService();
|
||||
}
|
||||
|
||||
this.shellExecutionConfig.sandboxManager = this._sandboxManager;
|
||||
this.modelRouterService = new ModelRouterService(this);
|
||||
|
||||
// HACK: The settings loading logic doesn't currently merge the default
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { WindowsSandboxManager } from './windowsSandboxManager.js';
|
||||
import { SandboxRequest } from './sandboxManager.js';
|
||||
import type { SandboxRequest } from './sandboxManager.js';
|
||||
import * as os from 'node:os';
|
||||
|
||||
describe('WindowsSandboxManager', () => {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import fs from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
type SandboxManager,
|
||||
@@ -84,6 +83,20 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
// Grant "Low Mandatory Level" write access to the CWD.
|
||||
this.grantLowIntegrityAccess(req.cwd);
|
||||
|
||||
// Whitelist essential system paths for DLL loading and basic tool execution.
|
||||
// This is required when using the Restricted Code SID (S-1-5-12).
|
||||
const systemPaths = [
|
||||
process.env['SystemRoot'] || 'C:\\Windows',
|
||||
process.env['ProgramFiles'] || 'C:\\Program Files',
|
||||
process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)',
|
||||
];
|
||||
|
||||
for (const sysPath of systemPaths) {
|
||||
if (fs.existsSync(sysPath)) {
|
||||
this.grantLowIntegrityAccess(sysPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Grant "Low Mandatory Level" read access to allowedPaths.
|
||||
if (req.config?.allowedPaths) {
|
||||
for (const allowedPath of req.config.allowedPaths) {
|
||||
|
||||
Reference in New Issue
Block a user