Download packages/core/src/sandbox/windows/WindowsSandboxManager.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/sandbox/windows/WindowsSandboxManager.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/sandbox/windows/WindowsSandboxManager.ts
-
curl -L -o WindowsSandboxManager.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/sandbox/windows/WindowsSandboxManager.ts
16.1 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import fs from 'node:fs'; | |
| import path, { join } from 'node:path'; | |
| import os from 'node:os'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { | |
| type SandboxManager, | |
| type SandboxRequest, | |
| type SandboxedCommand, | |
| GOVERNANCE_FILES, | |
| findSecretFiles, | |
| type GlobalSandboxOptions, | |
| type SandboxPermissions, | |
| type ParsedSandboxDenial, | |
| resolveSandboxPaths, | |
| } from '../../services/sandboxManager.js'; | |
| import type { ShellExecutionResult } from '../../services/shellExecutionService.js'; | |
| import { | |
| sanitizeEnvironment, | |
| getSecureSanitizationConfig, | |
| } from '../../services/environmentSanitization.js'; | |
| import { debugLogger } from '../../utils/debugLogger.js'; | |
| import { | |
| spawnAsync, | |
| getCommandName, | |
| initializeShellParsers, | |
| getCommandRoots, | |
| stripShellWrapper, | |
| } from '../../utils/shell-utils.js'; | |
| import { | |
| isKnownSafeCommand, | |
| isDangerousCommand, | |
| isStrictlyApproved, | |
| } from './commandSafety.js'; | |
| import { verifySandboxOverrides } from '../utils/commandUtils.js'; | |
| import { parseWindowsSandboxDenials } from './windowsSandboxDenialUtils.js'; | |
| import { isErrnoException } from '../utils/fsUtils.js'; | |
| import { | |
| isSubpath, | |
| resolveToRealPath, | |
| assertValidPathString, | |
| } from '../../utils/paths.js'; | |
| import { | |
| type SandboxDenialCache, | |
| createSandboxDenialCache, | |
| } from '../utils/sandboxDenialUtils.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 { | |
| static readonly HELPER_EXE = 'GeminiSandbox.exe'; | |
| private readonly helperPath: string; | |
| private readonly denialCache: SandboxDenialCache = createSandboxDenialCache(); | |
| private static helperCompiled = false; | |
| private governanceFilesInitialized = false; | |
| constructor(private readonly options: GlobalSandboxOptions) { | |
| this.helperPath = path.resolve(__dirname, WindowsSandboxManager.HELPER_EXE); | |
| } | |
| isKnownSafeCommand(args: string[], cwd?: string): boolean { | |
| const effectiveCwd = cwd ?? this.options.workspace; | |
| return isKnownSafeCommand(args, effectiveCwd, this.options.workspace); | |
| } | |
| isDangerousCommand(args: string[], cwd?: string): boolean { | |
| const effectiveCwd = cwd ?? this.options.workspace; | |
| return isDangerousCommand(args, effectiveCwd, this.options.workspace); | |
| } | |
| parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined { | |
| return parseWindowsSandboxDenials(result, this.denialCache); | |
| } | |
| getWorkspace(): string { | |
| return this.options.workspace; | |
| } | |
| getOptions(): GlobalSandboxOptions { | |
| return this.options; | |
| } | |
| private ensureGovernanceFilesExist(workspace: string): void { | |
| if (this.governanceFilesInitialized) return; | |
| // These must exist on the host before running the sandbox to ensure they are protected. | |
| for (const file of GOVERNANCE_FILES) { | |
| const filePath = join(workspace, file.path); | |
| touch(filePath, file.isDirectory); | |
| } | |
| this.governanceFilesInitialized = true; | |
| } | |
| private async ensureHelperCompiled(): Promise<void> { | |
| if (WindowsSandboxManager.helperCompiled || os.platform() !== 'win32') { | |
| 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, | |
| ); | |
| } | |
| WindowsSandboxManager.helperCompiled = true; | |
| } | |
| /** | |
| * Prepares a command for sandboxed execution on Windows. | |
| */ | |
| async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> { | |
| await this.ensureHelperCompiled(); | |
| const sanitizationConfig = getSecureSanitizationConfig( | |
| req.policy?.sanitizationConfig, | |
| ); | |
| const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig); | |
| const isReadonlyMode = this.options.modeConfig?.readonly ?? true; | |
| const allowOverrides = this.options.modeConfig?.allowOverrides ?? true; | |
| // Reject override attempts in plan mode | |
| verifySandboxOverrides(allowOverrides, req.policy); | |
| const command = req.command; | |
| const args = req.args; | |
| // Native commands __read and __write are passed directly to GeminiSandbox.exe | |
| const isYolo = this.options.modeConfig?.yolo ?? false; | |
| // Fetch persistent approvals for this command | |
| const commandName = await getCommandName(command, args); | |
| const persistentPermissions = allowOverrides | |
| ? this.options.policyManager?.getCommandPermissions(commandName) | |
| : undefined; | |
| // Merge all permissions | |
| const mergedAdditional: SandboxPermissions = { | |
| fileSystem: { | |
| read: [ | |
| ...(persistentPermissions?.fileSystem?.read ?? []), | |
| ...(req.policy?.additionalPermissions?.fileSystem?.read ?? []), | |
| ], | |
| write: [ | |
| ...(persistentPermissions?.fileSystem?.write ?? []), | |
| ...(req.policy?.additionalPermissions?.fileSystem?.write ?? []), | |
| ], | |
| }, | |
| network: | |
| isYolo || | |
| persistentPermissions?.network || | |
| req.policy?.additionalPermissions?.network || | |
| false, | |
| }; | |
| if (req.command === '__read' && req.args[0]) { | |
| mergedAdditional.fileSystem!.read!.push(req.args[0]); | |
| } else if (req.command === '__write' && req.args[0]) { | |
| mergedAdditional.fileSystem!.write!.push(req.args[0]); | |
| } | |
| const defaultNetwork = | |
| this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false; | |
| const networkAccess = defaultNetwork || mergedAdditional.network; | |
| await initializeShellParsers(); | |
| const fullCmd = [command, ...args].join(' '); | |
| const stripped = stripShellWrapper(fullCmd); | |
| const roots = getCommandRoots(stripped).filter( | |
| (r) => r !== 'shopt' && r !== 'set', | |
| ); | |
| const isGitCommand = roots.includes('git'); | |
| const resolvedPaths = await resolveSandboxPaths( | |
| this.options, | |
| req, | |
| mergedAdditional, | |
| ); | |
| this.ensureGovernanceFilesExist(resolvedPaths.workspace.resolved); | |
| // 1. Collect all forbidden paths. | |
| // We start with explicitly forbidden paths from the options and request. | |
| const forbiddenManifest = new Set( | |
| resolvedPaths.forbidden.map((p) => resolveToRealPath(p)), | |
| ); | |
| // On Windows, we explicitly deny access to secret files for Low Integrity processes. | |
| // We scan common search directories (workspace, allowed paths) for secrets. | |
| const searchDirs = new Set([ | |
| resolvedPaths.workspace.resolved, | |
| ...resolvedPaths.policyAllowed, | |
| ...resolvedPaths.globalIncludes, | |
| ]); | |
| const secretFilesPromises = Array.from(searchDirs).map(async (dir) => { | |
| try { | |
| // We use maxDepth 3 to catch common nested secrets while keeping performance high. | |
| const secretFiles = await findSecretFiles(dir, 3); | |
| for (const secretFile of secretFiles) { | |
| forbiddenManifest.add(resolveToRealPath(secretFile)); | |
| } | |
| } catch (e) { | |
| debugLogger.log( | |
| `WindowsSandboxManager: Failed to find secret files in ${dir}`, | |
| e, | |
| ); | |
| } | |
| }); | |
| await Promise.all(secretFilesPromises); | |
| // 2. Track paths that will be granted write access. | |
| // 'allowedManifest' contains resolved paths for the C# helper to apply ACLs. | |
| // 'inheritanceRoots' contains both original and resolved paths for Node.js sub-path validation. | |
| const allowedManifest = new Set<string>(); | |
| const inheritanceRoots = new Set<string>(); | |
| const addWritableRoot = (p: string) => { | |
| const resolved = resolveToRealPath(p); | |
| // Track both versions for inheritance checks to be robust against symlinks. | |
| inheritanceRoots.add(p); | |
| inheritanceRoots.add(resolved); | |
| // Never grant access to system directories or explicitly forbidden paths. | |
| if (this.isSystemDirectory(resolved)) return; | |
| if (forbiddenManifest.has(resolved)) return; | |
| // Explicitly reject UNC paths to prevent credential theft/SSRF, | |
| // but allow local extended-length and device paths. | |
| if ( | |
| resolved.startsWith('\\\\') && | |
| !resolved.startsWith('\\\\?\\') && | |
| !resolved.startsWith('\\\\.\\') | |
| ) { | |
| debugLogger.log( | |
| 'WindowsSandboxManager: Rejecting UNC path for allowed manifest:', | |
| resolved, | |
| ); | |
| return; | |
| } | |
| allowedManifest.add(resolved); | |
| }; | |
| // 3. Populate writable roots from various sources. | |
| // A. Workspace access | |
| const isApproved = allowOverrides | |
| ? await isStrictlyApproved( | |
| command, | |
| args, | |
| this.options.modeConfig?.approvedTools, | |
| req.cwd, | |
| this.options.workspace, | |
| ) | |
| : false; | |
| const workspaceWrite = !isReadonlyMode || isApproved || isYolo; | |
| if (workspaceWrite) { | |
| addWritableRoot(resolvedPaths.workspace.resolved); | |
| // If the workspace is writable and we're running a git command, | |
| // automatically allow write access to the .git directory. | |
| if (isGitCommand) { | |
| const gitDir = path.join(resolvedPaths.workspace.resolved, '.git'); | |
| addWritableRoot(gitDir); | |
| } | |
| } | |
| // B. Globally included directories | |
| for (const includeDir of resolvedPaths.globalIncludes) { | |
| addWritableRoot(includeDir); | |
| } | |
| // C. Explicitly allowed paths from the request policy | |
| for (const allowedPath of resolvedPaths.policyAllowed) { | |
| try { | |
| await fs.promises.access(allowedPath, fs.constants.F_OK); | |
| } catch { | |
| throw new Error( | |
| `Sandbox request rejected: Allowed path does not exist: ${allowedPath}. ` + | |
| 'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.', | |
| ); | |
| } | |
| addWritableRoot(allowedPath); | |
| } | |
| // D. Additional write paths (e.g. from internal __write command) | |
| for (const writePath of resolvedPaths.policyWrite) { | |
| try { | |
| await fs.promises.access(writePath, fs.constants.F_OK); | |
| addWritableRoot(writePath); | |
| continue; | |
| } catch { | |
| // If the file doesn't exist, it's only allowed if it resides within a granted root. | |
| const isInherited = Array.from(inheritanceRoots).some((root) => | |
| isSubpath(root, writePath), | |
| ); | |
| if (!isInherited) { | |
| throw new Error( | |
| `Sandbox request rejected: Additional write path does not exist and its parent directory is not allowed: ${writePath}. ` + | |
| 'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.', | |
| ); | |
| } | |
| } | |
| } | |
| // Support git worktrees/submodules; read-only to prevent malicious hook/config modification (RCE). | |
| // Read access is inherited; skip addWritableRoot to ensure write protection. | |
| if (resolvedPaths.gitWorktree) { | |
| // No-op for read access on Windows. | |
| } | |
| // 5. Generate Manifests | |
| const tempDir = await fs.promises.mkdtemp( | |
| path.join(os.tmpdir(), 'gemini-cli-sandbox-'), | |
| ); | |
| const forbiddenManifestPath = path.join(tempDir, 'forbidden.txt'); | |
| await fs.promises.writeFile( | |
| forbiddenManifestPath, | |
| Array.from(forbiddenManifest).join('\n'), | |
| ); | |
| const allowedManifestPath = path.join(tempDir, 'allowed.txt'); | |
| await fs.promises.writeFile( | |
| allowedManifestPath, | |
| Array.from(allowedManifest).join('\n'), | |
| ); | |
| // 6. Construct the helper command | |
| const program = this.helperPath; | |
| const finalArgs = [ | |
| networkAccess ? '1' : '0', | |
| req.cwd, | |
| '--forbidden-manifest', | |
| forbiddenManifestPath, | |
| '--allowed-manifest', | |
| allowedManifestPath, | |
| command, | |
| ...args, | |
| ]; | |
| const finalEnv = { ...sanitizedEnv }; | |
| return { | |
| program, | |
| args: finalArgs, | |
| env: finalEnv, | |
| cwd: req.cwd, | |
| cleanup: () => { | |
| try { | |
| fs.rmSync(tempDir, { recursive: true, force: true }); | |
| } catch { | |
| // Ignore errors | |
| } | |
| }, | |
| }; | |
| } | |
| private isSystemDirectory(resolvedPath: string): boolean { | |
| 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)'; | |
| return ( | |
| resolvedPath.toLowerCase().startsWith(systemRoot.toLowerCase()) || | |
| resolvedPath.toLowerCase().startsWith(programFiles.toLowerCase()) || | |
| resolvedPath.toLowerCase().startsWith(programFilesX86.toLowerCase()) | |
| ); | |
| } | |
| } | |
| /** | |
| * Ensures a file or directory exists. | |
| */ | |
| function touch(filePath: string, isDirectory: boolean): void { | |
| assertValidPathString(filePath); | |
| try { | |
| // If it exists (even as a broken symlink), do nothing | |
| fs.lstatSync(filePath); | |
| return; | |
| } catch (e: unknown) { | |
| if (isErrnoException(e) && e.code !== 'ENOENT') { | |
| throw e; | |
| } | |
| } | |
| if (isDirectory) { | |
| fs.mkdirSync(filePath, { recursive: true }); | |
| } else { | |
| const dir = path.dirname(filePath); | |
| if (!fs.existsSync(dir)) { | |
| fs.mkdirSync(dir, { recursive: true }); | |
| } | |
| fs.closeSync(fs.openSync(filePath, 'a')); | |
| } | |
| } | |