feat(windows-sandbox): architectural hardening, spawnAsync refactor, and build fixes

This commit is contained in:
mkorwel
2026-03-18 21:16:59 -07:00
parent 2b666506da
commit 16d91aad2e
6 changed files with 66 additions and 31 deletions
+1
View File
@@ -0,0 +1 @@
packages/core/src/services/scripts/*.exe
+2
View File
@@ -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` |
@@ -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,
@@ -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<void> {
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<SandboxedCommand> {
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<void> {
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',
+19 -2
View File
@@ -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.",
+1 -1
View File
@@ -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)) {