mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-11 05:41:08 -07:00
229 lines
6.8 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|
|
}
|