diff --git a/packages/core/src/services/scripts/GeminiSandbox.cs b/packages/core/src/services/scripts/GeminiSandbox.cs index 51c57a86b9..2f1bd26cd0 100644 --- a/packages/core/src/services/scripts/GeminiSandbox.cs +++ b/packages/core/src/services/scripts/GeminiSandbox.cs @@ -273,7 +273,12 @@ public class GeminiSandbox { si.hStdOutput = GetStdHandle(-11); si.hStdError = GetStdHandle(-12); - string commandLine = string.Join(" ", args, 2, args.Length - 2); + List quotedArgs = new List(); + 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()); @@ -299,6 +304,14 @@ public class GeminiSandbox { 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 + "\""; + } + private static int RunInImpersonation(IntPtr hToken, Func action) { using (WindowsIdentity.Impersonate(hToken)) { return action(); diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index 77d74d2761..d7d30dd44d 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -279,6 +279,14 @@ export class ShellExecutionService { ): Promise { const sandboxManager = shellExecutionConfig.sandboxManager ?? new NoopSandboxManager(); + + // Strict sandbox on Windows (network disabled) requires cmd.exe + const isStrictSandbox = + os.platform() === 'win32' && + shellExecutionConfig.sandboxConfig?.enabled && + shellExecutionConfig.sandboxConfig?.command === 'windows-native' && + !shellExecutionConfig.sandboxConfig?.networkAccess; + const { env: sanitizedEnv } = await sandboxManager.prepareCommand({ command: commandToExecute, args: [], @@ -302,6 +310,7 @@ export class ShellExecutionService { shellExecutionConfig, ptyInfo, sanitizedEnv, + isStrictSandbox, ); } catch (_e) { // Fallback to child_process @@ -316,6 +325,7 @@ export class ShellExecutionService { abortSignal, shellExecutionConfig.sanitizationConfig, shouldUseNodePty, + isStrictSandbox, ); } @@ -356,10 +366,18 @@ export class ShellExecutionService { abortSignal: AbortSignal, sanitizationConfig: EnvironmentSanitizationConfig, isInteractive: boolean, + isStrictSandbox?: boolean, ): ShellExecutionHandle { try { const isWindows = os.platform() === 'win32'; - const { executable, argsPrefix, shell } = getShellConfiguration(); + let { executable, argsPrefix, shell } = getShellConfiguration(); + + if (isStrictSandbox) { + shell = 'cmd'; + argsPrefix = ['/c']; + executable = 'cmd.exe'; + } + const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell); const spawnArgs = [...argsPrefix, guardedCommand]; @@ -690,6 +708,7 @@ export class ShellExecutionService { shellExecutionConfig: ShellExecutionConfig, ptyInfo: PtyImplementation, sanitizedEnv: Record, + isStrictSandbox?: boolean, ): Promise { if (!ptyInfo) { // This should not happen, but as a safeguard... @@ -700,7 +719,13 @@ export class ShellExecutionService { try { const cols = shellExecutionConfig.terminalWidth ?? 80; const rows = shellExecutionConfig.terminalHeight ?? 30; - const { executable, argsPrefix, shell } = getShellConfiguration(); + let { executable, argsPrefix, shell } = getShellConfiguration(); + + if (isStrictSandbox) { + shell = 'cmd'; + argsPrefix = ['/c']; + executable = 'cmd.exe'; + } const resolvedExecutable = await resolveExecutable(executable); if (!resolvedExecutable) {