mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-15 16:41:11 -07:00
chore(core): add build script for Windows sandbox helper
This commit is contained in:
@@ -131,8 +131,26 @@ export async function loadSandboxConfig(
|
||||
process.env['GEMINI_SANDBOX_IMAGE_DEFAULT'] ??
|
||||
packageJson?.config?.sandboxImageUri;
|
||||
|
||||
const networkAccess =
|
||||
process.env['GEMINI_SANDBOX_NETWORK'] === 'true' ||
|
||||
settings.tools?.sandboxNetworkAccess === true;
|
||||
|
||||
const allowedPathsEnv = process.env['GEMINI_SANDBOX_ALLOWED_PATHS']
|
||||
?.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 0);
|
||||
|
||||
const allowedPaths =
|
||||
allowedPathsEnv ?? settings.tools?.sandboxAllowedPaths ?? [];
|
||||
|
||||
return command &&
|
||||
(image || command === 'sandbox-exec' || command === 'windows-native')
|
||||
? { enabled: true, allowedPaths: [], networkAccess: false, command, image }
|
||||
? {
|
||||
enabled: true,
|
||||
allowedPaths,
|
||||
networkAccess,
|
||||
command,
|
||||
image,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -1261,6 +1261,26 @@ const SETTINGS_SCHEMA = {
|
||||
`,
|
||||
showInDialog: false,
|
||||
},
|
||||
sandboxNetworkAccess: {
|
||||
type: 'boolean',
|
||||
label: 'Sandbox Network Access',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Whether the sandbox has outbound network access.',
|
||||
showInDialog: true,
|
||||
},
|
||||
sandboxAllowedPaths: {
|
||||
type: 'array',
|
||||
label: 'Sandbox Allowed Paths',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: [] as string[],
|
||||
description: 'Additional host paths to allow the sandbox to access.',
|
||||
showInDialog: true,
|
||||
items: { type: 'string' },
|
||||
mergeStrategy: MergeStrategy.UNION,
|
||||
},
|
||||
shell: {
|
||||
type: 'object',
|
||||
label: 'Shell',
|
||||
|
||||
@@ -211,6 +211,28 @@ export async function start_sandbox(
|
||||
});
|
||||
}
|
||||
|
||||
if (config.command === 'windows-native') {
|
||||
debugLogger.log('using native windows sandboxing ...');
|
||||
// process.argv is [node, script, ...args]
|
||||
// We want to skip the first element (node) when calling spawn(process.execPath, ...)
|
||||
const finalArgv = cliArgs.slice(1);
|
||||
|
||||
const child = spawn(process.execPath, finalArgv, {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
SANDBOX: 'windows-native',
|
||||
},
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => {
|
||||
resolve(code ?? 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (config.command === 'lxc') {
|
||||
return await start_lxc_sandbox(config, nodeArgs, cliArgs);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:ci": "vitest run",
|
||||
"prepare": "node scripts/compile-windows-sandbox.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"files": [
|
||||
|
||||
59
packages/core/scripts/compile-windows-sandbox.js
Normal file
59
packages/core/scripts/compile-windows-sandbox.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* Compiles the GeminiSandbox C# helper on Windows.
|
||||
* This is used to provide native restricted token sandboxing.
|
||||
*/
|
||||
function compileWindowsSandbox(): void {
|
||||
if (os.platform() !== 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
const helperPath = path.resolve(__dirname, '../src/services/scripts/GeminiSandbox.exe');
|
||||
const sourcePath = path.resolve(__dirname, '../src/services/scripts/GeminiSandbox.cs');
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
console.error(`Sandbox source not found at ${sourcePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find csc.exe (C# Compiler) which is built into Windows .NET Framework
|
||||
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
|
||||
const cscPaths = [
|
||||
path.join(systemRoot, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe'),
|
||||
path.join(systemRoot, 'Microsoft.NET', 'Framework', 'v4.0.30319', 'csc.exe'),
|
||||
];
|
||||
|
||||
const csc = cscPaths.find(p => fs.existsSync(p));
|
||||
|
||||
if (!csc) {
|
||||
console.warn('Windows C# compiler (csc.exe) not found. Native sandboxing will attempt to compile on first run.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Compiling native Windows sandbox helper...`);
|
||||
const result = spawnSync(csc, [`/out:${helperPath}`, '/optimize', sourcePath], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error('Failed to compile Windows sandbox helper.');
|
||||
} else {
|
||||
console.log('Successfully compiled GeminiSandbox.exe');
|
||||
}
|
||||
}
|
||||
|
||||
compileWindowsSandbox();
|
||||
@@ -46,7 +46,7 @@ export class SandboxedFileSystemService implements FileSystemService {
|
||||
if (code === 0) {
|
||||
resolve(output);
|
||||
} else {
|
||||
reject(new Error(`Failed to read file via sandbox: ${error || 'Unknown error'}`));
|
||||
reject(new Error(`Sandbox Error: Command failed with exit code ${code}. ${error ? 'Details: ' + error : ''}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -78,7 +78,7 @@ export class SandboxedFileSystemService implements FileSystemService {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Failed to write file via sandbox: ${error || 'Unknown error'}`));
|
||||
reject(new Error(`Sandbox Error: Command failed with exit code ${code}. ${error ? 'Details: ' + error : ''}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -162,6 +162,12 @@ public class GeminiSandbox {
|
||||
IntPtr pSidsToDisable = IntPtr.Zero;
|
||||
uint sidCount = 0;
|
||||
|
||||
IntPtr pSidsToRestrict = IntPtr.Zero;
|
||||
uint restrictCount = 0;
|
||||
|
||||
// "networkAccess == false" implies Strict Sandbox Level 1.
|
||||
// In Strict mode, we strip the Network SID and apply the Restricted Code SID.
|
||||
// This blocks network access and restricts file reads, but requires cmd.exe.
|
||||
if (!networkAccess) {
|
||||
IntPtr networkSid;
|
||||
if (ConvertStringSidToSid("S-1-5-2", out networkSid)) {
|
||||
@@ -173,20 +179,21 @@ public class GeminiSandbox {
|
||||
saa.Attributes = 0;
|
||||
Marshal.StructureToPtr(saa, pSidsToDisable, false);
|
||||
}
|
||||
}
|
||||
|
||||
IntPtr pSidsToRestrict = IntPtr.Zero;
|
||||
uint restrictCount = 0;
|
||||
IntPtr restrictedSid;
|
||||
if (ConvertStringSidToSid("S-1-5-12", out restrictedSid)) {
|
||||
restrictCount = 1;
|
||||
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
||||
pSidsToRestrict = Marshal.AllocHGlobal(saaSize);
|
||||
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
||||
saa.Sid = restrictedSid;
|
||||
saa.Attributes = 0;
|
||||
Marshal.StructureToPtr(saa, pSidsToRestrict, false);
|
||||
IntPtr restrictedSid;
|
||||
// S-1-5-12 is Restricted Code SID
|
||||
if (ConvertStringSidToSid("S-1-5-12", out restrictedSid)) {
|
||||
restrictCount = 1;
|
||||
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
||||
pSidsToRestrict = Marshal.AllocHGlobal(saaSize);
|
||||
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
||||
saa.Sid = restrictedSid;
|
||||
saa.Attributes = 0;
|
||||
Marshal.StructureToPtr(saa, pSidsToRestrict, false);
|
||||
}
|
||||
}
|
||||
// If networkAccess == true, we are in Elevated mode (Level 2).
|
||||
// We only strip privileges (DISABLE_MAX_PRIVILEGE), allowing network and powershell.
|
||||
|
||||
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, sidCount, pSidsToDisable, 0, IntPtr.Zero, restrictCount, pSidsToRestrict, out hRestrictedToken)) {
|
||||
Console.Error.WriteLine("Failed to create restricted token");
|
||||
|
||||
@@ -326,9 +326,26 @@ export class ShellExecutionService {
|
||||
try {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
||||
const spawnArgs = [...argsPrefix, guardedCommand];
|
||||
// In Strict Sandbox mode (networkAccess = false), PowerShell will fail to load
|
||||
// its required system DLLs due to the Restricted Code SID.
|
||||
// We must fallback to cmd.exe for strict sandboxing.
|
||||
const isStrictSandbox =
|
||||
shellExecutionConfig.sandboxConfig?.enabled &&
|
||||
shellExecutionConfig.sandboxConfig?.command === 'windows-native' &&
|
||||
!shellExecutionConfig.sandboxConfig?.networkAccess;
|
||||
|
||||
const finalShell = isStrictSandbox ? 'cmd' : shell;
|
||||
let finalArgsPrefix: string[] = [];
|
||||
if (finalShell === 'cmd') {
|
||||
finalArgsPrefix = ['/c'];
|
||||
} else {
|
||||
finalArgsPrefix = argsPrefix;
|
||||
}
|
||||
|
||||
// We still use the original executable logic (e.g. searching for git)
|
||||
// but the guard command formatting is based on the final shell.
|
||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, finalShell as ShellType);
|
||||
const spawnArgs = [...finalArgsPrefix, guardedCommand];
|
||||
const env = {
|
||||
...process.env,
|
||||
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
||||
@@ -456,6 +473,12 @@ export class ShellExecutionService {
|
||||
|
||||
let combinedOutput = state.output;
|
||||
|
||||
const isSandboxError = code === 3221225781 || code === -1073741515; // 0xC0000135
|
||||
if (isSandboxError && shellExecutionConfig.sandboxConfig?.enabled) {
|
||||
const sandboxMessage = `\\n[GEMINI_CLI_SANDBOX_ERROR: Command execution was blocked by the native Windows sandbox. This typically means the command attempted an unauthorized network request or file access.]`;
|
||||
combinedOutput += sandboxMessage;
|
||||
}
|
||||
|
||||
if (state.truncated) {
|
||||
const truncationMessage = `\n[GEMINI_CLI_WARNING: Output truncated. The buffer is limited to ${
|
||||
MAX_CHILD_PROCESS_BUFFER_SIZE / (1024 * 1024)
|
||||
@@ -595,8 +618,21 @@ export class ShellExecutionService {
|
||||
const rows = shellExecutionConfig.terminalHeight ?? 30;
|
||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
||||
|
||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
||||
const args = [...argsPrefix, guardedCommand];
|
||||
const isStrictSandbox =
|
||||
shellExecutionConfig.sandboxConfig?.enabled &&
|
||||
shellExecutionConfig.sandboxConfig?.command === 'windows-native' &&
|
||||
!shellExecutionConfig.sandboxConfig?.networkAccess;
|
||||
|
||||
const finalShell = isStrictSandbox ? 'cmd' : shell;
|
||||
let finalArgsPrefix: string[] = [];
|
||||
if (finalShell === 'cmd') {
|
||||
finalArgsPrefix = ['/c'];
|
||||
} else {
|
||||
finalArgsPrefix = argsPrefix;
|
||||
}
|
||||
|
||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, finalShell as ShellType);
|
||||
const args = [...finalArgsPrefix, guardedCommand];
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
@@ -846,6 +882,13 @@ export class ShellExecutionService {
|
||||
|
||||
// Store exit info for late subscribers (e.g. backgrounding race condition)
|
||||
this.exitedPtyInfo.set(ptyProcess.pid, { exitCode, signal });
|
||||
|
||||
const isSandboxError = exitCode === 3221225781 || exitCode === -1073741515; // 0xC0000135
|
||||
let finalOutput = getFullBufferText(headlessTerminal);
|
||||
if (isSandboxError && shellExecutionConfig.sandboxConfig?.enabled) {
|
||||
finalOutput += `\n[GEMINI_CLI_SANDBOX_ERROR: Command execution was blocked by the native Windows sandbox. This typically means the command attempted an unauthorized network request or file access.]`;
|
||||
}
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
this.exitedPtyInfo.delete(ptyProcess.pid);
|
||||
@@ -869,7 +912,7 @@ export class ShellExecutionService {
|
||||
|
||||
resolve({
|
||||
rawOutput: finalBuffer,
|
||||
output: getFullBufferText(headlessTerminal),
|
||||
output: finalOutput,
|
||||
exitCode,
|
||||
signal: signal ?? null,
|
||||
error,
|
||||
|
||||
Reference in New Issue
Block a user