refactor(sandbox): reorganize Windows sandbox files (#23645)

This commit is contained in:
Gal Zahavi
2026-03-24 07:32:20 -07:00
committed by GitHub
parent 893c7d3880
commit fc18768155
7 changed files with 15 additions and 15 deletions
@@ -10,7 +10,7 @@ import { NoopSandboxManager, sanitizePaths } from './sandboxManager.js';
import { createSandboxManager } from './sandboxManagerFactory.js';
import { LinuxSandboxManager } from '../sandbox/linux/LinuxSandboxManager.js';
import { MacOsSandboxManager } from '../sandbox/macos/MacOsSandboxManager.js';
import { WindowsSandboxManager } from './windowsSandboxManager.js';
import { WindowsSandboxManager } from '../sandbox/windows/WindowsSandboxManager.js';
describe('sanitizePaths', () => {
it('should return undefined if no paths are provided', () => {
@@ -12,7 +12,7 @@ import {
} from './sandboxManager.js';
import { LinuxSandboxManager } from '../sandbox/linux/LinuxSandboxManager.js';
import { MacOsSandboxManager } from '../sandbox/macos/MacOsSandboxManager.js';
import { WindowsSandboxManager } from './windowsSandboxManager.js';
import { WindowsSandboxManager } from '../sandbox/windows/WindowsSandboxManager.js';
import type { SandboxConfig } from '../config/config.js';
import { type SandboxPolicyManager } from '../policy/sandboxPolicyManager.js';
@@ -1,370 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Principal;
using System.IO;
public class GeminiSandbox {
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO {
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public ushort wShowWindow;
public ushort cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION {
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
public Int64 PerProcessUserTimeLimit;
public Int64 PerJobUserTimeLimit;
public uint LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public UIntPtr Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS {
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
[StructLayout(LayoutKind.Sequential)]
public struct SID_AND_ATTRIBUTES {
public IntPtr Sid;
public uint Attributes;
}
[StructLayout(LayoutKind.Sequential)]
public struct TOKEN_MANDATORY_LABEL {
public SID_AND_ATTRIBUTES Label;
}
public enum JobObjectInfoClass {
ExtendedLimitInformation = 9
}
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetCurrentProcess();
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool CreateRestrictedToken(IntPtr ExistingTokenHandle, uint Flags, uint DisableSidCount, IntPtr SidsToDisable, uint DeletePrivilegeCount, IntPtr PrivilegesToDelete, uint RestrictedSidCount, IntPtr SidsToRestrict, out IntPtr NewTokenHandle);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint ResumeThread(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool ConvertStringSidToSid(string StringSid, out IntPtr Sid);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool SetTokenInformation(IntPtr TokenHandle, int TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LocalFree(IntPtr hMem);
public const uint TOKEN_DUPLICATE = 0x0002;
public const uint TOKEN_QUERY = 0x0008;
public const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
public const uint TOKEN_ADJUST_DEFAULT = 0x0080;
public const uint DISABLE_MAX_PRIVILEGE = 0x1;
public const uint CREATE_SUSPENDED = 0x00000004;
public const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
public const uint STARTF_USESTDHANDLES = 0x00000100;
public const int TokenIntegrityLevel = 25;
public const uint SE_GROUP_INTEGRITY = 0x00000020;
public const uint INFINITE = 0xFFFFFFFF;
static int Main(string[] args) {
if (args.Length < 3) {
Console.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]");
Console.WriteLine("Internal commands: __read <path>, __write <path>");
return 1;
}
bool networkAccess = args[0] == "1";
string cwd = args[1];
string command = args[2];
IntPtr hToken = IntPtr.Zero;
IntPtr hRestrictedToken = IntPtr.Zero;
IntPtr hJob = IntPtr.Zero;
IntPtr pSidsToDisable = IntPtr.Zero;
IntPtr pSidsToRestrict = IntPtr.Zero;
IntPtr networkSid = IntPtr.Zero;
IntPtr restrictedSid = IntPtr.Zero;
IntPtr lowIntegritySid = IntPtr.Zero;
try {
// 1. Setup Token
IntPtr hCurrentProcess = GetCurrentProcess();
if (!OpenProcessToken(hCurrentProcess, TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_DEFAULT, out hToken)) {
Console.Error.WriteLine("Failed to open process token");
return 1;
}
uint sidCount = 0;
uint restrictCount = 0;
// "networkAccess == false" implies Strict Sandbox Level 1.
if (!networkAccess) {
if (ConvertStringSidToSid("S-1-5-2", out networkSid)) {
sidCount = 1;
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
pSidsToDisable = Marshal.AllocHGlobal(saaSize);
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
saa.Sid = networkSid;
saa.Attributes = 0;
Marshal.StructureToPtr(saa, pSidsToDisable, false);
}
// 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 (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, sidCount, pSidsToDisable, 0, IntPtr.Zero, restrictCount, pSidsToRestrict, out hRestrictedToken)) {
Console.Error.WriteLine("Failed to create restricted token");
return 1;
}
// 2. Set Integrity Level to Low
if (ConvertStringSidToSid("S-1-16-4096", out lowIntegritySid)) {
TOKEN_MANDATORY_LABEL tml = new TOKEN_MANDATORY_LABEL();
tml.Label.Sid = lowIntegritySid;
tml.Label.Attributes = SE_GROUP_INTEGRITY;
int tmlSize = Marshal.SizeOf(tml);
IntPtr pTml = Marshal.AllocHGlobal(tmlSize);
try {
Marshal.StructureToPtr(tml, pTml, false);
SetTokenInformation(hRestrictedToken, TokenIntegrityLevel, pTml, (uint)tmlSize);
} finally {
Marshal.FreeHGlobal(pTml);
}
}
// 3. Handle Internal Commands or External Process
if (command == "__read") {
string path = args[3];
return RunInImpersonation(hRestrictedToken, () => {
try {
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
using (StreamReader sr = new StreamReader(fs, System.Text.Encoding.UTF8)) {
char[] buffer = new char[4096];
int bytesRead;
while ((bytesRead = sr.Read(buffer, 0, buffer.Length)) > 0) {
Console.Write(buffer, 0, bytesRead);
}
}
return 0;
} catch (Exception e) {
Console.Error.WriteLine(e.Message);
return 1;
}
});
} else if (command == "__write") {
string path = args[3];
return RunInImpersonation(hRestrictedToken, () => {
try {
using (StreamReader reader = new StreamReader(Console.OpenStandardInput(), System.Text.Encoding.UTF8))
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
using (StreamWriter writer = new StreamWriter(fs, System.Text.Encoding.UTF8)) {
char[] buffer = new char[4096];
int bytesRead;
while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0) {
writer.Write(buffer, 0, bytesRead);
}
}
return 0;
} catch (Exception e) {
Console.Error.WriteLine(e.Message);
return 1;
}
});
}
// 4. Setup Job Object for external process
hJob = CreateJobObject(IntPtr.Zero, null);
if (hJob != IntPtr.Zero) {
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
limitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
int limitSize = Marshal.SizeOf(limitInfo);
IntPtr pLimit = Marshal.AllocHGlobal(limitSize);
try {
Marshal.StructureToPtr(limitInfo, pLimit, false);
SetInformationJobObject(hJob, JobObjectInfoClass.ExtendedLimitInformation, pLimit, (uint)limitSize);
} finally {
Marshal.FreeHGlobal(pLimit);
}
}
// 5. Launch Process
STARTUPINFO si = new STARTUPINFO();
si.cb = (uint)Marshal.SizeOf(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = GetStdHandle(-10);
si.hStdOutput = GetStdHandle(-11);
si.hStdError = GetStdHandle(-12);
string commandLine = "";
for (int i = 2; i < args.Length; i++) {
if (i > 2) commandLine += " ";
commandLine += QuoteArgument(args[i]);
}
PROCESS_INFORMATION pi;
if (!CreateProcessAsUser(hRestrictedToken, null, commandLine, IntPtr.Zero, IntPtr.Zero, true, CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT, IntPtr.Zero, cwd, ref si, out pi)) {
Console.Error.WriteLine("Failed to create process. Error: " + Marshal.GetLastWin32Error());
return 1;
}
try {
if (hJob != IntPtr.Zero) {
AssignProcessToJobObject(hJob, pi.hProcess);
}
ResumeThread(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
uint exitCode = 0;
GetExitCodeProcess(pi.hProcess, out exitCode);
return (int)exitCode;
} finally {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
} catch (Exception e) {
Console.Error.WriteLine("Unexpected error: " + e.Message);
return 1;
} finally {
if (hRestrictedToken != IntPtr.Zero) CloseHandle(hRestrictedToken);
if (hToken != IntPtr.Zero) CloseHandle(hToken);
if (hJob != IntPtr.Zero) CloseHandle(hJob);
if (pSidsToDisable != IntPtr.Zero) Marshal.FreeHGlobal(pSidsToDisable);
if (pSidsToRestrict != IntPtr.Zero) Marshal.FreeHGlobal(pSidsToRestrict);
if (networkSid != IntPtr.Zero) LocalFree(networkSid);
if (restrictedSid != IntPtr.Zero) LocalFree(restrictedSid);
if (lowIntegritySid != IntPtr.Zero) LocalFree(lowIntegritySid);
}
}
private static string QuoteArgument(string arg) {
if (string.IsNullOrEmpty(arg)) return "\"\"";
bool hasSpace = arg.IndexOfAny(new char[] { ' ', '\t' }) != -1;
if (!hasSpace && arg.IndexOf('\"') == -1) return arg;
// Windows command line escaping for arguments is complex.
// Rule: Backslashes only need escaping if they precede a double quote or the end of the string.
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append('\"');
for (int i = 0; i < arg.Length; i++) {
int backslashCount = 0;
while (i < arg.Length && arg[i] == '\\') {
backslashCount++;
i++;
}
if (i == arg.Length) {
// Escape backslashes before the closing double quote
sb.Append('\\', backslashCount * 2);
} else if (arg[i] == '\"') {
// Escape backslashes before a literal double quote
sb.Append('\\', backslashCount * 2 + 1);
sb.Append('\"');
} else {
// Backslashes don't need escaping here
sb.Append('\\', backslashCount);
sb.Append(arg[i]);
}
}
sb.Append('\"');
return sb.ToString();
}
private static int RunInImpersonation(IntPtr hToken, Func<int> action) {
using (WindowsIdentity.Impersonate(hToken)) {
return action();
}
}
}
@@ -1,138 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { WindowsSandboxManager } from './windowsSandboxManager.js';
import type { SandboxRequest } from './sandboxManager.js';
import { spawnAsync } from '../utils/shell-utils.js';
vi.mock('../utils/shell-utils.js', () => ({
spawnAsync: vi.fn(),
}));
describe('WindowsSandboxManager', () => {
let manager: WindowsSandboxManager;
let testCwd: string;
beforeEach(() => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
testCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-'));
manager = new WindowsSandboxManager({ workspace: testCwd });
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(testCwd, { recursive: true, force: true });
});
it('should prepare a GeminiSandbox.exe command', async () => {
const req: SandboxRequest = {
command: 'whoami',
args: ['/groups'],
cwd: testCwd,
env: { TEST_VAR: 'test_value' },
policy: {
networkAccess: false,
},
};
const result = await manager.prepareCommand(req);
expect(result.program).toContain('GeminiSandbox.exe');
expect(result.args).toEqual(['0', testCwd, 'whoami', '/groups']);
});
it('should handle networkAccess from config', async () => {
const req: SandboxRequest = {
command: 'whoami',
args: [],
cwd: testCwd,
env: {},
policy: {
networkAccess: true,
},
};
const result = await manager.prepareCommand(req);
expect(result.args[0]).toBe('1');
});
it('should sanitize environment variables', async () => {
const req: SandboxRequest = {
command: 'test',
args: [],
cwd: testCwd,
env: {
API_KEY: 'secret',
PATH: '/usr/bin',
},
policy: {
sanitizationConfig: {
allowedEnvironmentVariables: ['PATH'],
blockedEnvironmentVariables: ['API_KEY'],
enableEnvironmentVariableRedaction: true,
},
},
};
const result = await manager.prepareCommand(req);
expect(result.env['PATH']).toBe('/usr/bin');
expect(result.env['API_KEY']).toBeUndefined();
});
it('should ensure governance files exist', async () => {
const req: SandboxRequest = {
command: 'test',
args: [],
cwd: testCwd,
env: {},
};
await manager.prepareCommand(req);
expect(fs.existsSync(path.join(testCwd, '.gitignore'))).toBe(true);
expect(fs.existsSync(path.join(testCwd, '.geminiignore'))).toBe(true);
expect(fs.existsSync(path.join(testCwd, '.git'))).toBe(true);
expect(fs.lstatSync(path.join(testCwd, '.git')).isDirectory()).toBe(true);
});
it('should grant Low Integrity access to the workspace and allowed paths', async () => {
const allowedPath = path.join(os.tmpdir(), 'gemini-cli-test-allowed');
if (!fs.existsSync(allowedPath)) {
fs.mkdirSync(allowedPath);
}
try {
const req: SandboxRequest = {
command: 'test',
args: [],
cwd: testCwd,
env: {},
policy: {
allowedPaths: [allowedPath],
},
};
await manager.prepareCommand(req);
expect(spawnAsync).toHaveBeenCalledWith('icacls', [
path.resolve(testCwd),
'/setintegritylevel',
'Low',
]);
expect(spawnAsync).toHaveBeenCalledWith('icacls', [
path.resolve(allowedPath),
'/setintegritylevel',
'Low',
]);
} finally {
fs.rmSync(allowedPath, { recursive: true, force: true });
}
});
});
@@ -1,268 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import {
type SandboxManager,
type SandboxRequest,
type SandboxedCommand,
GOVERNANCE_FILES,
type GlobalSandboxOptions,
sanitizePaths,
} from './sandboxManager.js';
import {
sanitizeEnvironment,
getSecureSanitizationConfig,
} 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);
/**
* A SandboxManager implementation for Windows that uses Restricted Tokens,
* Job Objects, and Low Integrity levels for process isolation.
* Uses a native C# helper to bypass PowerShell restrictions.
*/
export class WindowsSandboxManager implements SandboxManager {
private readonly helperPath: string;
private initialized = false;
private readonly lowIntegrityCache = new Set<string>();
constructor(private readonly options: GlobalSandboxOptions) {
this.helperPath = path.resolve(__dirname, 'scripts', 'GeminiSandbox.exe');
}
/**
* Ensures a file or directory exists.
*/
private touch(filePath: string, isDirectory: boolean): void {
try {
// If it exists (even as a broken symlink), do nothing
if (fs.lstatSync(filePath)) return;
} catch {
// Ignore ENOENT
}
if (isDirectory) {
fs.mkdirSync(filePath, { recursive: true });
} else {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.closeSync(fs.openSync(filePath, 'a'));
}
}
private async ensureInitialized(): Promise<void> {
if (this.initialized) return;
if (os.platform() !== 'win32') {
this.initialized = true;
return;
}
try {
if (!fs.existsSync(this.helperPath)) {
debugLogger.log(
`WindowsSandboxManager: Helper not found at ${this.helperPath}. Attempting to compile...`,
);
// If the exe doesn't exist, we try to compile it from the .cs file
const sourcePath = this.helperPath.replace(/\.exe$/, '.cs');
if (fs.existsSync(sourcePath)) {
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
const cscPaths = [
'csc.exe', // Try in PATH first
path.join(
systemRoot,
'Microsoft.NET',
'Framework64',
'v4.0.30319',
'csc.exe',
),
path.join(
systemRoot,
'Microsoft.NET',
'Framework',
'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',
),
];
let compiled = false;
for (const csc of cscPaths) {
try {
debugLogger.log(
`WindowsSandboxManager: Trying to compile using ${csc}...`,
);
// We use spawnAsync but we don't need to capture output
await spawnAsync(csc, ['/out:' + this.helperPath, sourcePath]);
debugLogger.log(
`WindowsSandboxManager: Successfully compiled sandbox helper at ${this.helperPath}`,
);
compiled = true;
break;
} catch (e) {
debugLogger.log(
`WindowsSandboxManager: Failed to compile using ${csc}: ${e instanceof Error ? e.message : String(e)}`,
);
}
}
if (!compiled) {
debugLogger.log(
'WindowsSandboxManager: Failed to compile sandbox helper from any known CSC path.',
);
}
} else {
debugLogger.log(
`WindowsSandboxManager: Source file not found at ${sourcePath}. Cannot compile helper.`,
);
}
} else {
debugLogger.log(
`WindowsSandboxManager: Found helper at ${this.helperPath}`,
);
}
} catch (e) {
debugLogger.log(
'WindowsSandboxManager: Failed to initialize sandbox helper:',
e,
);
}
this.initialized = true;
}
/**
* Prepares a command for sandboxed execution on Windows.
*/
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
await this.ensureInitialized();
const sanitizationConfig = getSecureSanitizationConfig(
req.policy?.sanitizationConfig,
);
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
// 1. Handle filesystem permissions for Low Integrity
// Grant "Low Mandatory Level" write access to the workspace.
await this.grantLowIntegrityAccess(this.options.workspace);
// Grant "Low Mandatory Level" read access to allowedPaths.
const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || [];
for (const allowedPath of allowedPaths) {
await this.grantLowIntegrityAccess(allowedPath);
}
// TODO: handle forbidden paths
// 2. Protected governance files
// These must exist on the host before running the sandbox to prevent
// the sandboxed process from creating them with Low integrity.
// By being created as Medium integrity, they are write-protected from Low processes.
for (const file of GOVERNANCE_FILES) {
const filePath = path.join(this.options.workspace, file.path);
this.touch(filePath, file.isDirectory);
// We resolve real paths to ensure protection for both the symlink and its target.
try {
const realPath = fs.realpathSync(filePath);
if (realPath !== filePath) {
// If it's a symlink, the target is already implicitly protected
// if it's outside the Low integrity workspace (likely Medium).
// If it's inside, we ensure it's not accidentally Low.
}
} catch {
// Ignore realpath errors
}
}
// 3. Construct the helper command
// GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]
const program = this.helperPath;
// If the command starts with __, it's an internal command for the sandbox helper itself.
const args = [
req.policy?.networkAccess ? '1' : '0',
req.cwd,
req.command,
...req.args,
];
return {
program,
args,
env: sanitizedEnv,
};
}
/**
* Grants "Low Mandatory Level" access to a path using icacls.
*/
private async grantLowIntegrityAccess(targetPath: string): Promise<void> {
if (os.platform() !== 'win32') {
return;
}
const resolvedPath = path.resolve(targetPath);
if (this.lowIntegrityCache.has(resolvedPath)) {
return;
}
// Never modify integrity levels for system directories
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
const programFilesX86 =
process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
if (
resolvedPath.toLowerCase().startsWith(systemRoot.toLowerCase()) ||
resolvedPath.toLowerCase().startsWith(programFiles.toLowerCase()) ||
resolvedPath.toLowerCase().startsWith(programFilesX86.toLowerCase())
) {
return;
}
try {
await spawnAsync('icacls', [resolvedPath, '/setintegritylevel', 'Low']);
this.lowIntegrityCache.add(resolvedPath);
} catch (e) {
debugLogger.log(
'WindowsSandboxManager: icacls failed for',
resolvedPath,
e,
);
}
}
}