feat(core): refactor SandboxManager to a stateless architecture and introduce explicit Deny interface (#23141)

This commit is contained in:
Emily Hedlund
2026-03-23 11:43:58 -04:00
committed by GitHub
parent 99e5164c82
commit cdf077da56
13 changed files with 444 additions and 388 deletions
@@ -6,15 +6,18 @@
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import type {
SandboxManager,
SandboxRequest,
SandboxedCommand,
import {
type SandboxManager,
type SandboxRequest,
type SandboxedCommand,
type GlobalSandboxOptions,
sanitizePaths,
} from './sandboxManager.js';
import {
sanitizeEnvironment,
type EnvironmentSanitizationConfig,
getSecureSanitizationConfig,
} from './environmentSanitization.js';
import { debugLogger } from '../utils/debugLogger.js';
import { spawnAsync } from '../utils/shell-utils.js';
@@ -29,18 +32,16 @@ const __dirname = path.dirname(__filename);
*/
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;
constructor(private readonly options: GlobalSandboxOptions) {
this.helperPath = path.resolve(__dirname, 'scripts', 'GeminiSandbox.exe');
}
private async ensureInitialized(): Promise<void> {
if (this.initialized) return;
if (this.platform !== 'win32') {
if (os.platform() !== 'win32') {
this.initialized = true;
return;
}
@@ -145,36 +146,31 @@ export class WindowsSandboxManager implements SandboxManager {
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 sanitizationConfig = getSecureSanitizationConfig(
req.policy?.sanitizationConfig,
);
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" write access to the workspace.
await this.grantLowIntegrityAccess(this.options.workspace);
// Grant "Low Mandatory Level" read access to allowedPaths.
if (req.config?.allowedPaths) {
for (const allowedPath of req.config.allowedPaths) {
await this.grantLowIntegrityAccess(allowedPath);
}
const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || [];
for (const allowedPath of allowedPaths) {
await this.grantLowIntegrityAccess(allowedPath);
}
// TODO: handle forbidden paths
// 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.policy?.networkAccess ? '1' : '0',
req.cwd,
req.command,
...req.args,
@@ -191,7 +187,7 @@ export class WindowsSandboxManager implements SandboxManager {
* Grants "Low Mandatory Level" access to a path using icacls.
*/
private async grantLowIntegrityAccess(targetPath: string): Promise<void> {
if (this.platform !== 'win32') {
if (os.platform() !== 'win32') {
return;
}