diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 13db1f0be3..d5e8436936 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -756,7 +756,7 @@ their corresponding top-level category object in your `settings.json` file. #### `tools` -- **`tools.sandbox`** (string): +- **`tools.sandbox`** (boolean | string | object): - **Description:** Sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., "docker", "podman", "lxc"). diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index b82a5d98a0..dbd2ec64e3 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -7,9 +7,9 @@ import { exec, execFile, - execFileSync, execSync, spawn, + spawnSync, type ChildProcess, } from 'node:child_process'; import path from 'node:path'; @@ -876,10 +876,10 @@ async function start_lxc_sandbox( const removeDevices = () => { for (const deviceName of devicesToRemove) { try { - execFileSync( + spawnSync( 'lxc', ['config', 'device', 'remove', containerName, deviceName], - { timeout: 2000 }, + { timeout: 1000, killSignal: 'SIGKILL', stdio: 'ignore' }, ); } catch { // Best-effort cleanup; ignore errors on exit. diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 56828f5a17..6e179eca11 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -469,6 +469,15 @@ export const ConfigSchema = z.object({ .optional(), image: z.string().optional(), }) + .superRefine((data, ctx) => { + if (data.enabled && !data.command) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Sandbox command is required when sandbox is enabled', + path: ['command'], + }); + } + }) .optional(), }); @@ -839,19 +848,7 @@ export class Config implements McpContext, AgentLoopContext { this.embeddingModel = params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL; this.fileSystemService = new StandardFileSystemService(); - this.sandbox = params.sandbox - ? { - enabled: params.sandbox.enabled ?? false, - allowedPaths: params.sandbox.allowedPaths ?? [], - networkAccess: params.sandbox.networkAccess ?? false, - command: params.sandbox.command, - image: params.sandbox.image, - } - : { - enabled: false, - allowedPaths: [], - networkAccess: false, - }; + this.sandbox = params.sandbox; this.targetDir = path.resolve(params.targetDir); this.folderTrust = params.folderTrust ?? false; this.workspaceContext = new WorkspaceContext(this.targetDir, []); diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index d92f395706..054940bb71 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -26,10 +26,8 @@ import { serializeTerminalToObject, type AnsiOutput, } from '../utils/terminalSerializer.js'; -import { - sanitizeEnvironment, - type EnvironmentSanitizationConfig, -} from './environmentSanitization.js'; +import { sanitizeEnvironment, type EnvironmentSanitizationConfig } from './environmentSanitization.js'; +import { NoopSandboxManager } from './sandboxManager.js'; import { killProcessGroup } from '../utils/process-utils.js'; const { Terminal } = pkg; @@ -326,6 +324,15 @@ export class ShellExecutionService { shouldUseNodePty: boolean, shellExecutionConfig: ShellExecutionConfig, ): Promise { + const sandboxManager = new NoopSandboxManager(); + const { env: sanitizedEnv } = await sandboxManager.prepareCommand({ + command: commandToExecute, + args: [], + env: process.env, + cwd, + config: shellExecutionConfig, + }); + if (shouldUseNodePty) { const ptyInfo = await getPty(); if (ptyInfo) { @@ -337,6 +344,7 @@ export class ShellExecutionService { abortSignal, shellExecutionConfig, ptyInfo, + sanitizedEnv, ); } catch (_e) { // Fallback to child_process @@ -695,6 +703,7 @@ export class ShellExecutionService { abortSignal: AbortSignal, shellExecutionConfig: ShellExecutionConfig, ptyInfo: PtyImplementation, + sanitizedEnv: Record, ): Promise { if (!ptyInfo) { // This should not happen, but as a safeguard... @@ -724,10 +733,7 @@ export class ShellExecutionService { cols, rows, env: { - ...sanitizeEnvironment( - process.env, - shellExecutionConfig.sanitizationConfig, - ), + ...sanitizedEnv, GEMINI_CLI: '1', TERM: 'xterm-256color', PAGER: shellExecutionConfig.pager ?? 'cat',