chore(core): add build script for Windows sandbox helper

This commit is contained in:
mkorwel
2026-03-09 19:55:47 -07:00
parent 1cb703b405
commit 5c0b0f98ec
8 changed files with 190 additions and 20 deletions
@@ -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,