Files
gemini-cli/packages/core/src/services/windowsSandboxManager.ts

229 lines
6.8 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type {
SandboxManager,
SandboxRequest,
SandboxedCommand,
} from './sandboxManager.js';
import {
sanitizeEnvironment,
type EnvironmentSanitizationConfig,
} from './environmentSanitization.js';
import { debugLogger } from '../utils/debugLogger.js';
import { spawnAsync } from '../utils/shell-utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* A SandboxManager implementation for Windows that uses Restricted Tokens,
* Job Objects, and Low Integrity levels for process isolation.
* Uses a native C# helper to bypass PowerShell restrictions.
*/
export class WindowsSandboxManager implements SandboxManager {
private readonly helperPath: string;
private readonly platform: string;
private initialized = false;
private readonly lowIntegrityCache = new Set<string>();
constructor(platform: string = process.platform) {
this.platform = platform;
this.helperPath = path.resolve(__dirname, 'scripts', 'GeminiSandbox.exe');
}
private async ensureInitialized(): Promise<void> {
if (this.initialized) return;
if (this.platform !== 'win32') {
this.initialized = true;
return;
}
try {
if (!fs.existsSync(this.helperPath)) {
debugLogger.log(
`WindowsSandboxManager: Helper not found at ${this.helperPath}. Attempting to compile...`,
);
// If the exe doesn't exist, we try to compile it from the .cs file
const sourcePath = this.helperPath.replace(/\.exe$/, '.cs');
if (fs.existsSync(sourcePath)) {
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',
),
// Added newer framework paths
path.join(
systemRoot,
'Microsoft.NET',
'Framework64',
'v4.8',
'csc.exe',
),
path.join(
systemRoot,
'Microsoft.NET',
'Framework',
'v4.8',
'csc.exe',
),
path.join(
systemRoot,
'Microsoft.NET',
'Framework64',
'v3.5',
'csc.exe',
),
];
let compiled = false;
for (const csc of cscPaths) {
try {
debugLogger.log(
`WindowsSandboxManager: Trying to compile using ${csc}...`,
);
// We use spawnAsync but we don't need to capture output
await spawnAsync(csc, ['/out:' + this.helperPath, sourcePath]);
debugLogger.log(
`WindowsSandboxManager: Successfully compiled sandbox helper at ${this.helperPath}`,
);
compiled = true;
break;
} catch (e) {
debugLogger.log(
`WindowsSandboxManager: Failed to compile using ${csc}: ${e instanceof Error ? e.message : String(e)}`,
);
}
}
if (!compiled) {
debugLogger.log(
'WindowsSandboxManager: Failed to compile sandbox helper from any known CSC path.',
);
}
} else {
debugLogger.log(
`WindowsSandboxManager: Source file not found at ${sourcePath}. Cannot compile helper.`,
);
}
} else {
debugLogger.log(
`WindowsSandboxManager: Found helper at ${this.helperPath}`,
);
}
} catch (e) {
debugLogger.log(
'WindowsSandboxManager: Failed to initialize sandbox helper:',
e,
);
}
this.initialized = true;
}
/**
* Prepares a command for sandboxed execution on Windows.
*/
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
await this.ensureInitialized();
const sanitizationConfig: EnvironmentSanitizationConfig = {
allowedEnvironmentVariables:
req.config?.sanitizationConfig?.allowedEnvironmentVariables ?? [],
blockedEnvironmentVariables:
req.config?.sanitizationConfig?.blockedEnvironmentVariables ?? [],
enableEnvironmentVariableRedaction:
req.config?.sanitizationConfig?.enableEnvironmentVariableRedaction ??
true,
};
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
// 1. Handle filesystem permissions for Low Integrity
// Grant "Low Mandatory Level" write access to the CWD.
await this.grantLowIntegrityAccess(req.cwd);
// Grant "Low Mandatory Level" read access to allowedPaths.
if (req.config?.allowedPaths) {
for (const allowedPath of req.config.allowedPaths) {
await this.grantLowIntegrityAccess(allowedPath);
}
}
// 2. Construct the helper command
// GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]
const program = this.helperPath;
// If the command starts with __, it's an internal command for the sandbox helper itself.
const args = [
req.config?.networkAccess ? '1' : '0',
req.cwd,
req.command,
...req.args,
];
return {
program,
args,
env: sanitizedEnv,
};
}
/**
* Grants "Low Mandatory Level" access to a path using icacls.
*/
private async grantLowIntegrityAccess(targetPath: string): Promise<void> {
if (this.platform !== 'win32') {
return;
}
const resolvedPath = path.resolve(targetPath);
if (this.lowIntegrityCache.has(resolvedPath)) {
return;
}
// Never modify integrity levels for system directories
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
const programFilesX86 =
process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
if (
resolvedPath.toLowerCase().startsWith(systemRoot.toLowerCase()) ||
resolvedPath.toLowerCase().startsWith(programFiles.toLowerCase()) ||
resolvedPath.toLowerCase().startsWith(programFilesX86.toLowerCase())
) {
return;
}
try {
await spawnAsync('icacls', [resolvedPath, '/setintegritylevel', 'Low']);
this.lowIntegrityCache.add(resolvedPath);
} catch (e) {
debugLogger.log(
'WindowsSandboxManager: icacls failed for',
resolvedPath,
e,
);
}
}
}