mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -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'] ??
|
process.env['GEMINI_SANDBOX_IMAGE_DEFAULT'] ??
|
||||||
packageJson?.config?.sandboxImageUri;
|
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 &&
|
return command &&
|
||||||
(image || command === 'sandbox-exec' || command === 'windows-native')
|
(image || command === 'sandbox-exec' || command === 'windows-native')
|
||||||
? { enabled: true, allowedPaths: [], networkAccess: false, command, image }
|
? {
|
||||||
|
enabled: true,
|
||||||
|
allowedPaths,
|
||||||
|
networkAccess,
|
||||||
|
command,
|
||||||
|
image,
|
||||||
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1261,6 +1261,26 @@ const SETTINGS_SCHEMA = {
|
|||||||
`,
|
`,
|
||||||
showInDialog: false,
|
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: {
|
shell: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
label: 'Shell',
|
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') {
|
if (config.command === 'lxc') {
|
||||||
return await start_lxc_sandbox(config, nodeArgs, cliArgs);
|
return await start_lxc_sandbox(config, nodeArgs, cliArgs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:ci": "vitest run",
|
"test:ci": "vitest run",
|
||||||
|
"prepare": "node scripts/compile-windows-sandbox.js",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -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) {
|
if (code === 0) {
|
||||||
resolve(output);
|
resolve(output);
|
||||||
} else {
|
} 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) {
|
if (code === 0) {
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} 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;
|
IntPtr pSidsToDisable = IntPtr.Zero;
|
||||||
uint sidCount = 0;
|
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) {
|
if (!networkAccess) {
|
||||||
IntPtr networkSid;
|
IntPtr networkSid;
|
||||||
if (ConvertStringSidToSid("S-1-5-2", out networkSid)) {
|
if (ConvertStringSidToSid("S-1-5-2", out networkSid)) {
|
||||||
@@ -173,20 +179,21 @@ public class GeminiSandbox {
|
|||||||
saa.Attributes = 0;
|
saa.Attributes = 0;
|
||||||
Marshal.StructureToPtr(saa, pSidsToDisable, false);
|
Marshal.StructureToPtr(saa, pSidsToDisable, false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
IntPtr pSidsToRestrict = IntPtr.Zero;
|
IntPtr restrictedSid;
|
||||||
uint restrictCount = 0;
|
// S-1-5-12 is Restricted Code SID
|
||||||
IntPtr restrictedSid;
|
if (ConvertStringSidToSid("S-1-5-12", out restrictedSid)) {
|
||||||
if (ConvertStringSidToSid("S-1-5-12", out restrictedSid)) {
|
restrictCount = 1;
|
||||||
restrictCount = 1;
|
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
||||||
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
pSidsToRestrict = Marshal.AllocHGlobal(saaSize);
|
||||||
pSidsToRestrict = Marshal.AllocHGlobal(saaSize);
|
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
||||||
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
saa.Sid = restrictedSid;
|
||||||
saa.Sid = restrictedSid;
|
saa.Attributes = 0;
|
||||||
saa.Attributes = 0;
|
Marshal.StructureToPtr(saa, pSidsToRestrict, false);
|
||||||
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)) {
|
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, sidCount, pSidsToDisable, 0, IntPtr.Zero, restrictCount, pSidsToRestrict, out hRestrictedToken)) {
|
||||||
Console.Error.WriteLine("Failed to create restricted token");
|
Console.Error.WriteLine("Failed to create restricted token");
|
||||||
|
|||||||
@@ -326,9 +326,26 @@ export class ShellExecutionService {
|
|||||||
try {
|
try {
|
||||||
const isWindows = os.platform() === 'win32';
|
const isWindows = os.platform() === 'win32';
|
||||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
const { executable, argsPrefix, shell } = getShellConfiguration();
|
||||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
// In Strict Sandbox mode (networkAccess = false), PowerShell will fail to load
|
||||||
const spawnArgs = [...argsPrefix, guardedCommand];
|
// 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 = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
||||||
@@ -456,6 +473,12 @@ export class ShellExecutionService {
|
|||||||
|
|
||||||
let combinedOutput = state.output;
|
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) {
|
if (state.truncated) {
|
||||||
const truncationMessage = `\n[GEMINI_CLI_WARNING: Output truncated. The buffer is limited to ${
|
const truncationMessage = `\n[GEMINI_CLI_WARNING: Output truncated. The buffer is limited to ${
|
||||||
MAX_CHILD_PROCESS_BUFFER_SIZE / (1024 * 1024)
|
MAX_CHILD_PROCESS_BUFFER_SIZE / (1024 * 1024)
|
||||||
@@ -595,8 +618,21 @@ export class ShellExecutionService {
|
|||||||
const rows = shellExecutionConfig.terminalHeight ?? 30;
|
const rows = shellExecutionConfig.terminalHeight ?? 30;
|
||||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
const { executable, argsPrefix, shell } = getShellConfiguration();
|
||||||
|
|
||||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
const isStrictSandbox =
|
||||||
const args = [...argsPrefix, guardedCommand];
|
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 = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -846,6 +882,13 @@ export class ShellExecutionService {
|
|||||||
|
|
||||||
// Store exit info for late subscribers (e.g. backgrounding race condition)
|
// Store exit info for late subscribers (e.g. backgrounding race condition)
|
||||||
this.exitedPtyInfo.set(ptyProcess.pid, { exitCode, signal });
|
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(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
this.exitedPtyInfo.delete(ptyProcess.pid);
|
this.exitedPtyInfo.delete(ptyProcess.pid);
|
||||||
@@ -869,7 +912,7 @@ export class ShellExecutionService {
|
|||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
rawOutput: finalBuffer,
|
rawOutput: finalBuffer,
|
||||||
output: getFullBufferText(headlessTerminal),
|
output: finalOutput,
|
||||||
exitCode,
|
exitCode,
|
||||||
signal: signal ?? null,
|
signal: signal ?? null,
|
||||||
error,
|
error,
|
||||||
|
|||||||
Reference in New Issue
Block a user