feat(core): implement progressive elevation and AI error awareness for Windows sandbox

This commit is contained in:
mkorwel
2026-03-09 22:46:13 -07:00
parent 5c0b0f98ec
commit 1633cd88ac
6 changed files with 77 additions and 33 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -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.');
}
}

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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) {