From 1633cd88ac4990137de47408cea7f8d896d833fd Mon Sep 17 00:00:00 2001 From: mkorwel Date: Mon, 9 Mar 2026 22:46:13 -0700 Subject: [PATCH] feat(core): implement progressive elevation and AI error awareness for Windows sandbox --- packages/cli/src/config/sandboxConfig.ts | 17 ++++++-- packages/core/package.json | 2 +- .../core/scripts/compile-windows-sandbox.js | 32 ++++++++++++--- packages/core/src/config/config.ts | 40 +++++++++---------- .../services/windowsSandboxManager.test.ts | 4 +- .../src/services/windowsSandboxManager.ts | 15 ++++++- 6 files changed, 77 insertions(+), 33 deletions(-) diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts index a37d8341c2..5eb0f3d004 100644 --- a/packages/cli/src/config/sandboxConfig.ts +++ b/packages/cli/src/config/sandboxConfig.ts @@ -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, diff --git a/packages/core/package.json b/packages/core/package.json index 59cd2181bc..02422b7b54 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/scripts/compile-windows-sandbox.js b/packages/core/scripts/compile-windows-sandbox.js index d7aae88db9..4389817162 100644 --- a/packages/core/scripts/compile-windows-sandbox.js +++ b/packages/core/scripts/compile-windows-sandbox.js @@ -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.'); } } diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 2ce34e7723..c7048c1bcd 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -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 diff --git a/packages/core/src/services/windowsSandboxManager.test.ts b/packages/core/src/services/windowsSandboxManager.test.ts index 864c1f0587..ebcce77d5c 100644 --- a/packages/core/src/services/windowsSandboxManager.test.ts +++ b/packages/core/src/services/windowsSandboxManager.test.ts @@ -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', () => { diff --git a/packages/core/src/services/windowsSandboxManager.ts b/packages/core/src/services/windowsSandboxManager.ts index b0f0dfe2b2..0c3918eda6 100644 --- a/packages/core/src/services/windowsSandboxManager.ts +++ b/packages/core/src/services/windowsSandboxManager.ts @@ -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) {