From 16d91aad2e0430334884ee02878b652fe8d915fd Mon Sep 17 00:00:00 2001 From: mkorwel Date: Wed, 18 Mar 2026 21:16:59 -0700 Subject: [PATCH] feat(windows-sandbox): architectural hardening, spawnAsync refactor, and build fixes --- .geminiignore | 1 + docs/cli/settings.md | 2 + .../services/sandboxedFileSystemService.ts | 4 ++ .../src/services/windowsSandboxManager.ts | 67 +++++++++++-------- schemas/settings.schema.json | 21 +++++- scripts/copy_files.js | 2 +- 6 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 .geminiignore diff --git a/.geminiignore b/.geminiignore new file mode 100644 index 0000000000..e40b6ba36e --- /dev/null +++ b/.geminiignore @@ -0,0 +1 @@ +packages/core/src/services/scripts/*.exe diff --git a/docs/cli/settings.md b/docs/cli/settings.md index eb9ba4158e..efacec8875 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -115,6 +115,8 @@ they appear in the UI. | UI Label | Setting | Description | Default | | -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` | +| Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` | | Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` | | Show Color | `tools.shell.showColor` | Show color in shell output. | `false` | | Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` | diff --git a/packages/core/src/services/sandboxedFileSystemService.ts b/packages/core/src/services/sandboxedFileSystemService.ts index 5c76976851..1f3cbfea84 100644 --- a/packages/core/src/services/sandboxedFileSystemService.ts +++ b/packages/core/src/services/sandboxedFileSystemService.ts @@ -26,6 +26,8 @@ export class SandboxedFileSystemService implements FileSystemService { }); return new Promise((resolve, reject) => { + // Direct spawn is necessary here for streaming large file contents. + const child = spawn(prepared.program, prepared.args, { cwd: this.cwd, env: prepared.env, @@ -65,6 +67,8 @@ export class SandboxedFileSystemService implements FileSystemService { }); return new Promise((resolve, reject) => { + // Direct spawn is necessary here for streaming large file contents. + const child = spawn(prepared.program, prepared.args, { cwd: this.cwd, env: prepared.env, diff --git a/packages/core/src/services/windowsSandboxManager.ts b/packages/core/src/services/windowsSandboxManager.ts index f16181e4d4..b777e9baa4 100644 --- a/packages/core/src/services/windowsSandboxManager.ts +++ b/packages/core/src/services/windowsSandboxManager.ts @@ -5,19 +5,19 @@ */ import fs from 'node:fs'; -import { spawnSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { - type SandboxManager, - type SandboxRequest, - type SandboxedCommand, +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); @@ -38,7 +38,7 @@ export class WindowsSandboxManager implements SandboxManager { this.helperPath = path.resolve(__dirname, 'scripts', 'GeminiSandbox.exe'); } - private ensureInitialized(): void { + private async ensureInitialized(): Promise { if (this.initialized) return; if (this.platform !== 'win32') { this.initialized = true; @@ -67,18 +67,37 @@ export class WindowsSandboxManager implements SandboxManager { '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', + ), ]; for (const csc of cscPaths) { - const result = spawnSync( - csc, - ['/out:' + this.helperPath, sourcePath], - { - stdio: 'ignore', - }, - ); - if (result.status === 0) { + try { + // We use spawnAsync but we don't need to capture output + await spawnAsync(csc, ['/out:' + this.helperPath, sourcePath]); break; + } catch (_e) { + // Try next path } } } @@ -97,7 +116,7 @@ export class WindowsSandboxManager implements SandboxManager { * Prepares a command for sandboxed execution on Windows. */ async prepareCommand(req: SandboxRequest): Promise { - this.ensureInitialized(); + await this.ensureInitialized(); const sanitizationConfig: EnvironmentSanitizationConfig = { allowedEnvironmentVariables: @@ -113,12 +132,12 @@ export class WindowsSandboxManager implements SandboxManager { // 1. Handle filesystem permissions for Low Integrity // Grant "Low Mandatory Level" write access to the CWD. - this.grantLowIntegrityAccess(req.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) { - this.grantLowIntegrityAccess(allowedPath); + await this.grantLowIntegrityAccess(allowedPath); } } @@ -144,7 +163,7 @@ export class WindowsSandboxManager implements SandboxManager { /** * Grants "Low Mandatory Level" access to a path using icacls. */ - private grantLowIntegrityAccess(targetPath: string): void { + private async grantLowIntegrityAccess(targetPath: string): Promise { if (this.platform !== 'win32') { return; } @@ -169,16 +188,8 @@ export class WindowsSandboxManager implements SandboxManager { } try { - const result = spawnSync( - 'icacls', - [resolvedPath, '/setintegritylevel', 'Low'], - { - stdio: 'ignore', - }, - ); - if (result.status === 0) { - this.lowIntegrityCache.add(resolvedPath); - } + await spawnAsync('icacls', [resolvedPath, '/setintegritylevel', 'Low']); + this.lowIntegrityCache.add(resolvedPath); } catch (e) { debugLogger.log( 'WindowsSandboxManager: icacls failed for', diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index f85a39bb35..684dd19e44 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1924,10 +1924,27 @@ "properties": { "sandbox": { "title": "Sandbox", - "description": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\").", - "markdownDescription": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\").\n\n- Category: `Tools`\n- Requires restart: `yes`", + "description": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\", \"windows-native\").", + "markdownDescription": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\", \"windows-native\").\n\n- Category: `Tools`\n- Requires restart: `yes`", "$ref": "#/$defs/BooleanOrStringOrObject" }, + "sandboxAllowedPaths": { + "title": "Sandbox Allowed Paths", + "description": "List of additional paths that the sandbox is allowed to access.", + "markdownDescription": "List of additional paths that the sandbox is allowed to access.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `[]`", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "sandboxNetworkAccess": { + "title": "Sandbox Network Access", + "description": "Whether the sandbox is allowed to access the network.", + "markdownDescription": "Whether the sandbox is allowed to access the network.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `false`", + "default": false, + "type": "boolean" + }, "shell": { "title": "Shell", "description": "Settings for shell execution.", diff --git a/scripts/copy_files.js b/scripts/copy_files.js index fc612fd144..d02070362f 100644 --- a/scripts/copy_files.js +++ b/scripts/copy_files.js @@ -26,7 +26,7 @@ import path from 'node:path'; const sourceDir = path.join('src'); const targetDir = path.join('dist', 'src'); -const extensionsToCopy = ['.md', '.json', '.sb', '.toml']; +const extensionsToCopy = ['.md', '.json', '.sb', '.toml', '.cs', '.exe']; function copyFilesRecursive(source, target) { if (!fs.existsSync(target)) {