feat(windows-sandbox): address review comments, fix shell integration, and harden security

This commit is contained in:
mkorwel
2026-03-18 11:51:19 -07:00
parent 4848908b91
commit f08fad9b87
8 changed files with 434 additions and 233 deletions
+1 -1
View File
@@ -1010,7 +1010,7 @@ export class Config implements McpContext, AgentLoopContext {
this._sandboxManager = new NoopSandboxManager();
}
if (this.sandbox?.enabled && this._sandboxManager) {
if (!(this._sandboxManager instanceof NoopSandboxManager)) {
this.fileSystemService = new SandboxedFileSystemService(
this._sandboxManager,
this.cwd,
@@ -126,6 +126,9 @@ public class GeminiSandbox {
[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;
@@ -150,166 +153,207 @@ public class GeminiSandbox {
string cwd = args[1];
string command = args[2];
// 1. Setup Token
IntPtr hCurrentProcess = GetCurrentProcess();
IntPtr hToken;
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;
}
IntPtr hRestrictedToken;
IntPtr hToken = IntPtr.Zero;
IntPtr hRestrictedToken = IntPtr.Zero;
IntPtr hJob = IntPtr.Zero;
IntPtr pSidsToDisable = IntPtr.Zero;
uint sidCount = 0;
IntPtr pSidsToRestrict = IntPtr.Zero;
uint restrictCount = 0;
IntPtr networkSid = IntPtr.Zero;
IntPtr restrictedSid = IntPtr.Zero;
IntPtr lowIntegritySid = IntPtr.Zero;
// "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)) {
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);
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;
}
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);
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 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");
return 1;
}
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
IntPtr lowIntegritySid;
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);
Marshal.StructureToPtr(tml, pTml, false);
SetTokenInformation(hRestrictedToken, TokenIntegrityLevel, pTml, (uint)tmlSize);
Marshal.FreeHGlobal(pTml);
}
// 3. Handle Internal Commands or External Process
if (command == "__read") {
string path = args[3];
return RunInImpersonation(hRestrictedToken, () => {
// 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 {
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
using (StreamReader sr = new StreamReader(fs)) {
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;
Marshal.StructureToPtr(tml, pTml, false);
SetTokenInformation(hRestrictedToken, TokenIntegrityLevel, pTml, (uint)tmlSize);
} finally {
Marshal.FreeHGlobal(pTml);
}
});
} else if (command == "__write") {
string path = args[3];
return RunInImpersonation(hRestrictedToken, () => {
}
// 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)) {
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()))
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
using (StreamWriter writer = new StreamWriter(fs)) {
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 {
using (StreamReader reader = new StreamReader(Console.OpenStandardInput()))
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
using (StreamWriter writer = new StreamWriter(fs)) {
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;
Marshal.StructureToPtr(limitInfo, pLimit, false);
SetInformationJobObject(hJob, JobObjectInfoClass.ExtendedLimitInformation, pLimit, (uint)limitSize);
} finally {
Marshal.FreeHGlobal(pLimit);
}
});
}
}
// 4. Setup Job Object for external process
IntPtr 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);
Marshal.StructureToPtr(limitInfo, pLimit, false);
SetInformationJobObject(hJob, JobObjectInfoClass.ExtendedLimitInformation, pLimit, (uint)limitSize);
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);
// 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]);
}
List<string> quotedArgs = new List<string>();
for (int i = 2; i < args.Length; i++) {
quotedArgs.Add(QuoteArgument(args[i]));
}
string commandLine = string.Join(" ", quotedArgs.ToArray());
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;
}
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());
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);
}
if (hJob != IntPtr.Zero) {
AssignProcessToJobObject(hJob, pi.hProcess);
}
ResumeThread(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
uint exitCode = 0;
GetExitCodeProcess(pi.hProcess, out exitCode);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(hRestrictedToken);
CloseHandle(hToken);
if (hJob != IntPtr.Zero) CloseHandle(hJob);
return (int)exitCode;
}
private static string QuoteArgument(string arg) {
if (string.IsNullOrEmpty(arg)) return "\"\"";
if (arg.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '\"' }) == -1) return arg;
string escaped = arg.Replace("\"", "\\\"");
return "\"" + escaped + "\"";
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) {
@@ -280,37 +280,112 @@ export class ShellExecutionService {
const sandboxManager =
shellExecutionConfig.sandboxManager ?? new NoopSandboxManager();
// Strict sandbox on Windows (network disabled) requires cmd.exe
// 1. Determine Shell Configuration
const isWindows = os.platform() === 'win32';
const isStrictSandbox =
os.platform() === 'win32' &&
isWindows &&
shellExecutionConfig.sandboxConfig?.enabled &&
shellExecutionConfig.sandboxConfig?.command === 'windows-native' &&
!shellExecutionConfig.sandboxConfig?.networkAccess;
const { env: sanitizedEnv } = await sandboxManager.prepareCommand({
command: commandToExecute,
args: [],
env: process.env,
let { executable, argsPrefix, shell } = getShellConfiguration();
if (isStrictSandbox) {
shell = 'cmd';
argsPrefix = ['/c'];
executable = 'cmd.exe';
}
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
const spawnArgs = [...argsPrefix, guardedCommand];
// 2. Prepare Environment
const gitConfigKeys: string[] = [];
if (!shouldUseNodePty) {
for (const key in process.env) {
if (key.startsWith('GIT_CONFIG_')) {
gitConfigKeys.push(key);
}
}
}
const sanitizationConfig = {
...shellExecutionConfig.sanitizationConfig,
allowedEnvironmentVariables: [
...(shellExecutionConfig.sanitizationConfig
.allowedEnvironmentVariables || []),
...gitConfigKeys,
],
};
const sanitizedEnv = sanitizeEnvironment(process.env, sanitizationConfig);
const baseEnv: Record<string, string | undefined> = {
...sanitizedEnv,
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
TERM: 'xterm-256color',
PAGER: shellExecutionConfig.pager ?? 'cat',
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
};
if (!shouldUseNodePty) {
// Ensure all GIT_CONFIG_* variables are preserved even if they were redacted
for (const key of gitConfigKeys) {
baseEnv[key] = process.env[key];
}
const gitConfigCount = parseInt(baseEnv['GIT_CONFIG_COUNT'] || '0', 10);
const newKey = `GIT_CONFIG_KEY_${gitConfigCount}`;
const newValue = `GIT_CONFIG_VALUE_${gitConfigCount}`;
// Ensure these new keys are allowed through sanitization
sanitizationConfig.allowedEnvironmentVariables.push(
'GIT_CONFIG_COUNT',
newKey,
newValue,
);
Object.assign(baseEnv, {
GIT_TERMINAL_PROMPT: '0',
GIT_ASKPASS: '',
SSH_ASKPASS: '',
GH_PROMPT_DISABLED: '1',
GCM_INTERACTIVE: 'never',
DISPLAY: '',
DBUS_SESSION_BUS_ADDRESS: '',
GIT_CONFIG_COUNT: (gitConfigCount + 1).toString(),
[newKey]: 'credential.helper',
[newValue]: '',
});
}
// 3. Prepare Sandboxed Command
const sandboxedCommand = await sandboxManager.prepareCommand({
command: executable,
args: spawnArgs,
env: baseEnv,
cwd,
config: {
...shellExecutionConfig,
...(shellExecutionConfig.sandboxConfig || {}),
sanitizationConfig,
},
});
// 4. Execute
if (shouldUseNodePty) {
const ptyInfo = await getPty();
if (ptyInfo) {
try {
return await this.executeWithPty(
commandToExecute,
sandboxedCommand.program,
sandboxedCommand.args,
cwd,
onOutputEvent,
abortSignal,
shellExecutionConfig,
ptyInfo,
sanitizedEnv,
isStrictSandbox,
sandboxedCommand.env,
);
} catch (_e) {
// Fallback to child_process
@@ -319,13 +394,12 @@ export class ShellExecutionService {
}
return this.childProcessFallback(
commandToExecute,
sandboxedCommand.program,
sandboxedCommand.args,
cwd,
onOutputEvent,
abortSignal,
shellExecutionConfig.sanitizationConfig,
shouldUseNodePty,
isStrictSandbox,
sandboxedCommand.env,
);
}
@@ -360,69 +434,15 @@ export class ShellExecutionService {
}
private static childProcessFallback(
commandToExecute: string,
executable: string,
spawnArgs: string[],
cwd: string,
onOutputEvent: (event: ShellOutputEvent) => void,
abortSignal: AbortSignal,
sanitizationConfig: EnvironmentSanitizationConfig,
isInteractive: boolean,
isStrictSandbox?: boolean,
env: Record<string, string | undefined>,
): ShellExecutionHandle {
try {
const isWindows = os.platform() === 'win32';
let { executable, argsPrefix, shell } = getShellConfiguration();
if (isStrictSandbox) {
shell = 'cmd';
argsPrefix = ['/c'];
executable = 'cmd.exe';
}
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
const spawnArgs = [...argsPrefix, guardedCommand];
// Specifically allow GIT_CONFIG_* variables to pass through sanitization
// in non-interactive mode so we can safely append our overrides.
const gitConfigKeys = !isInteractive
? Object.keys(process.env).filter((k) => k.startsWith('GIT_CONFIG_'))
: [];
const sanitizedEnv = sanitizeEnvironment(process.env, {
...sanitizationConfig,
allowedEnvironmentVariables: [
...(sanitizationConfig.allowedEnvironmentVariables || []),
...gitConfigKeys,
],
});
const env: NodeJS.ProcessEnv = {
...sanitizedEnv,
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
TERM: 'xterm-256color',
PAGER: 'cat',
GIT_PAGER: 'cat',
};
if (!isInteractive) {
const gitConfigCount = parseInt(
sanitizedEnv['GIT_CONFIG_COUNT'] || '0',
10,
);
Object.assign(env, {
// Disable interactive prompts and session-linked credential helpers
// in non-interactive mode to prevent hangs in detached process groups.
GIT_TERMINAL_PROMPT: '0',
GIT_ASKPASS: '',
SSH_ASKPASS: '',
GH_PROMPT_DISABLED: '1',
GCM_INTERACTIVE: 'never',
DISPLAY: '',
DBUS_SESSION_BUS_ADDRESS: '',
GIT_CONFIG_COUNT: (gitConfigCount + 1).toString(),
[`GIT_CONFIG_KEY_${gitConfigCount}`]: 'credential.helper',
[`GIT_CONFIG_VALUE_${gitConfigCount}`]: '',
});
}
const child = cpSpawn(executable, spawnArgs, {
cwd,
@@ -701,14 +721,14 @@ export class ShellExecutionService {
}
private static async executeWithPty(
commandToExecute: string,
executable: string,
args: string[],
cwd: string,
onOutputEvent: (event: ShellOutputEvent) => void,
abortSignal: AbortSignal,
shellExecutionConfig: ShellExecutionConfig,
ptyInfo: PtyImplementation,
sanitizedEnv: Record<string, string | undefined>,
isStrictSandbox?: boolean,
env: Record<string, string | undefined>,
): Promise<ShellExecutionHandle> {
if (!ptyInfo) {
// This should not happen, but as a safeguard...
@@ -719,13 +739,6 @@ export class ShellExecutionService {
try {
const cols = shellExecutionConfig.terminalWidth ?? 80;
const rows = shellExecutionConfig.terminalHeight ?? 30;
let { executable, argsPrefix, shell } = getShellConfiguration();
if (isStrictSandbox) {
shell = 'cmd';
argsPrefix = ['/c'];
executable = 'cmd.exe';
}
const resolvedExecutable = await resolveExecutable(executable);
if (!resolvedExecutable) {
@@ -734,9 +747,6 @@ export class ShellExecutionService {
);
}
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
const args = [...argsPrefix, guardedCommand];
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const ptyProcess = ptyInfo.module.spawn(executable, args, {
cwd,
@@ -744,14 +754,13 @@ export class ShellExecutionService {
cols,
rows,
env: {
...sanitizedEnv,
...env,
GEMINI_CLI: '1',
TERM: 'xterm-256color',
PAGER: shellExecutionConfig.pager ?? 'cat',
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
},
handleFlowControl: true,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
spawnedPty = ptyProcess as IPty;
const ptyPid = Number(ptyProcess.pid);
@@ -30,6 +30,7 @@ export class WindowsSandboxManager implements SandboxManager {
private readonly helperPath: string;
private readonly platform: string;
private initialized = false;
private readonly lowIntegrityCache = new Set<string>();
constructor(platform: string = process.platform) {
this.platform = platform;
@@ -139,10 +140,37 @@ export class WindowsSandboxManager implements SandboxManager {
if (this.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 {
spawnSync('icacls', [targetPath, '/setintegritylevel', 'Low'], {
stdio: 'ignore',
});
const result = spawnSync(
'icacls',
[resolvedPath, '/setintegritylevel', 'Low'],
{
stdio: 'ignore',
},
);
if (result.status === 0) {
this.lowIntegrityCache.add(resolvedPath);
}
} catch (_e) {
// Best effort
}