/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { createSandboxManager } from './sandboxManagerFactory.js'; import { ShellExecutionService } from './shellExecutionService.js'; import { getSecureSanitizationConfig } from './environmentSanitization.js'; import { type SandboxManager, type SandboxedCommand, } from './sandboxManager.js'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import os from 'node:os'; import fs from 'node:fs'; import path from 'node:path'; import http from 'node:http'; /** * Cross-platform command wrappers using Node.js inline scripts. * Ensures consistent execution behavior and reliable exit codes across * different host operating systems and restricted sandbox environments. */ const Platform = { isWindows: os.platform() === 'win32', isMac: os.platform() === 'darwin', /** Returns a command to create an empty file. */ touch(filePath: string) { return { command: process.execPath, args: [ '-e', `require("node:fs").writeFileSync(${JSON.stringify(filePath)}, "")`, ], }; }, /** Returns a command to read a file's content. */ cat(filePath: string) { return { command: process.execPath, args: [ '-e', `console.log(require("node:fs").readFileSync(${JSON.stringify(filePath)}, "utf8"))`, ], }; }, /** Returns a command to echo a string. */ echo(text: string) { return { command: process.execPath, args: ['-e', `console.log(${JSON.stringify(text)})`], }; }, /** Returns a command to perform a network request. */ curl(url: string) { return { command: process.execPath, args: [ '-e', `require("node:http").get(${JSON.stringify(url)}, (res) => { res.on("data", (d) => process.stdout.write(d)); res.on("end", () => process.exit(0)); }).on("error", () => process.exit(1));`, ], }; }, /** Returns a command that checks if the current terminal is interactive. */ isPty() { // ShellExecutionService.execute expects a raw shell string return `"${process.execPath}" -e "console.log(process.stdout.isTTY ? 'True' : 'False')"`; }, /** Returns a path that is strictly outside the workspace and likely blocked. */ getExternalBlockedPath() { return this.isWindows ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/Users/Shared/.gemini_test_blocked'; }, }; async function runCommand(command: SandboxedCommand) { try { const { stdout, stderr } = await promisify(execFile)( command.program, command.args, { cwd: command.cwd, env: command.env, encoding: 'utf-8', }, ); return { status: 0, stdout, stderr }; } catch (error: unknown) { const err = error as { code?: number; stdout?: string; stderr?: string }; return { status: err.code ?? 1, stdout: err.stdout ?? '', stderr: err.stderr ?? '', }; } } /** * Asserts the result of a sandboxed command execution, and provides detailed * diagnostics on failure. */ function assertResult( result: { status: number; stdout: string; stderr: string }, command: SandboxedCommand, expected: 'success' | 'failure', ) { const isSuccess = result.status === 0; const shouldBeSuccess = expected === 'success'; if (isSuccess === shouldBeSuccess) { if (shouldBeSuccess) { expect(result.status).toBe(0); } else { expect(result.status).not.toBe(0); } return; } const commandLine = `${command.program} ${command.args.join(' ')}`; const message = `Command ${ shouldBeSuccess ? 'failed' : 'succeeded' } unexpectedly. Command: ${commandLine} CWD: ${command.cwd || 'N/A'} Status: ${result.status} (expected ${expected})${ result.stdout ? `\nStdout: ${result.stdout.trim()}` : '' }${result.stderr ? `\nStderr: ${result.stderr.trim()}` : ''}`; throw new Error(message); } describe('SandboxManager Integration', () => { const tempDirectories: string[] = []; /** * Creates a temporary directory. * - macOS: Created in process.cwd() to avoid the seatbelt profile's global os.tmpdir() whitelist. * - Win/Linux: Created in os.tmpdir() because enforcing sandbox restrictions inside a large directory can be very slow. */ function createTempDir(prefix = 'gemini-sandbox-test-'): string { const baseDir = Platform.isMac ? path.join(process.cwd(), `.${prefix}`) : path.join(os.tmpdir(), prefix); const dir = fs.mkdtempSync(baseDir); tempDirectories.push(dir); return dir; } let workspace: string; let manager: SandboxManager; beforeAll(() => { workspace = createTempDir('workspace-'); manager = createSandboxManager({ enabled: true }, { workspace }); }); afterAll(() => { for (const dir of tempDirectories) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { // Best-effort cleanup } } }); describe('Basic Execution', () => { it('executes commands within the workspace', async () => { const { command, args } = Platform.echo('sandbox test'); const sandboxed = await manager.prepareCommand({ command, args, cwd: workspace, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'success'); expect(result.stdout.trim()).toBe('sandbox test'); }); // The Windows sandbox wrapper (GeminiSandbox.exe) uses standard pipes // for I/O interception, which breaks ConPTY pseudo-terminal inheritance. it.skipIf(Platform.isWindows)( 'supports interactive pseudo-terminals (node-pty)', async () => { const handle = await ShellExecutionService.execute( Platform.isPty(), workspace, () => {}, new AbortController().signal, true, { sanitizationConfig: getSecureSanitizationConfig(), sandboxManager: manager, }, ); const result = await handle.result; expect(result.exitCode).toBe(0); expect(result.output).toContain('True'); }, ); }); describe('File System Access', () => { it('blocks access outside the workspace', async () => { const blockedPath = Platform.getExternalBlockedPath(); const { command, args } = Platform.touch(blockedPath); const sandboxed = await manager.prepareCommand({ command, args, cwd: workspace, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'failure'); }); it('allows dynamic expansion of permissions after a failure', async () => { const tempDir = createTempDir('expansion-'); const testFile = path.join(tempDir, 'test.txt'); const { command, args } = Platform.touch(testFile); // First attempt: fails due to sandbox restrictions const sandboxed1 = await manager.prepareCommand({ command, args, cwd: workspace, env: process.env, }); const result1 = await runCommand(sandboxed1); assertResult(result1, sandboxed1, 'failure'); expect(fs.existsSync(testFile)).toBe(false); // Second attempt: succeeds with additional permissions const sandboxed2 = await manager.prepareCommand({ command, args, cwd: workspace, env: process.env, policy: { allowedPaths: [tempDir] }, }); const result2 = await runCommand(sandboxed2); assertResult(result2, sandboxed2, 'success'); expect(fs.existsSync(testFile)).toBe(true); }); it('grants access to explicitly allowed paths', async () => { const allowedDir = createTempDir('allowed-'); const testFile = path.join(allowedDir, 'test.txt'); const { command, args } = Platform.touch(testFile); const sandboxed = await manager.prepareCommand({ command, args, cwd: workspace, env: process.env, policy: { allowedPaths: [allowedDir] }, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'success'); expect(fs.existsSync(testFile)).toBe(true); }); it('blocks write access to forbidden paths within the workspace', async () => { const tempWorkspace = createTempDir('workspace-'); const forbiddenDir = path.join(tempWorkspace, 'forbidden'); const testFile = path.join(forbiddenDir, 'test.txt'); fs.mkdirSync(forbiddenDir); const osManager = createSandboxManager( { enabled: true }, { workspace: tempWorkspace, forbiddenPaths: async () => [forbiddenDir], }, ); const { command, args } = Platform.touch(testFile); const sandboxed = await osManager.prepareCommand({ command, args, cwd: tempWorkspace, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'failure'); }); // Windows icacls does not reliably block read-up access for Low Integrity // processes, so we skip read-specific assertions on Windows. The internal // tool architecture prevents read bypasses via the C# wrapper and __read. it.skipIf(Platform.isWindows)( 'blocks read access to forbidden paths within the workspace', async () => { const tempWorkspace = createTempDir('workspace-'); const forbiddenDir = path.join(tempWorkspace, 'forbidden'); const testFile = path.join(forbiddenDir, 'test.txt'); fs.mkdirSync(forbiddenDir); fs.writeFileSync(testFile, 'secret data'); const osManager = createSandboxManager( { enabled: true }, { workspace: tempWorkspace, forbiddenPaths: async () => [forbiddenDir], }, ); const { command, args } = Platform.cat(testFile); const sandboxed = await osManager.prepareCommand({ command, args, cwd: tempWorkspace, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'failure'); }, ); it('blocks access to files inside forbidden directories recursively', async () => { const tempWorkspace = createTempDir('workspace-'); const forbiddenDir = path.join(tempWorkspace, 'forbidden'); const nestedDir = path.join(forbiddenDir, 'nested'); const nestedFile = path.join(nestedDir, 'test.txt'); // Create the base forbidden directory first so the manager can restrict access to it. fs.mkdirSync(forbiddenDir); const osManager = createSandboxManager( { enabled: true }, { workspace: tempWorkspace, forbiddenPaths: async () => [forbiddenDir], }, ); // Execute a dummy command so the manager initializes its restrictions. const dummyCommand = await osManager.prepareCommand({ ...Platform.echo('init'), cwd: tempWorkspace, env: process.env, }); await runCommand(dummyCommand); // Now create the nested items. They will inherit the sandbox restrictions from their parent. fs.mkdirSync(nestedDir, { recursive: true }); fs.writeFileSync(nestedFile, 'secret'); const { command, args } = Platform.touch(nestedFile); const sandboxed = await osManager.prepareCommand({ command, args, cwd: tempWorkspace, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'failure'); }); it('prioritizes forbiddenPaths over allowedPaths', async () => { const tempWorkspace = createTempDir('workspace-'); const conflictDir = path.join(tempWorkspace, 'conflict'); const testFile = path.join(conflictDir, 'test.txt'); fs.mkdirSync(conflictDir); const osManager = createSandboxManager( { enabled: true }, { workspace: tempWorkspace, forbiddenPaths: async () => [conflictDir], }, ); const { command, args } = Platform.touch(testFile); const sandboxed = await osManager.prepareCommand({ command, args, cwd: tempWorkspace, env: process.env, policy: { allowedPaths: [conflictDir], }, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'failure'); }); it('gracefully ignores non-existent paths in allowedPaths and forbiddenPaths', async () => { const tempWorkspace = createTempDir('workspace-'); const nonExistentPath = path.join(tempWorkspace, 'does-not-exist'); const osManager = createSandboxManager( { enabled: true }, { workspace: tempWorkspace, forbiddenPaths: async () => [nonExistentPath], }, ); const { command, args } = Platform.echo('survived'); const sandboxed = await osManager.prepareCommand({ command, args, cwd: tempWorkspace, env: process.env, policy: { allowedPaths: [nonExistentPath], }, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'success'); expect(result.stdout.trim()).toBe('survived'); }); it('prevents creation of non-existent forbidden paths', async () => { const tempWorkspace = createTempDir('workspace-'); const nonExistentFile = path.join(tempWorkspace, 'never-created.txt'); const osManager = createSandboxManager( { enabled: true }, { workspace: tempWorkspace, forbiddenPaths: async () => [nonExistentFile], }, ); // We use touch to attempt creation of the file const { command: cmdTouch, args: argsTouch } = Platform.touch(nonExistentFile); const sandboxedCmd = await osManager.prepareCommand({ command: cmdTouch, args: argsTouch, cwd: tempWorkspace, env: process.env, }); // Execute the command, we expect it to fail (permission denied or read-only file system) const result = await runCommand(sandboxedCmd); assertResult(result, sandboxedCmd, 'failure'); expect(fs.existsSync(nonExistentFile)).toBe(false); }); it('blocks access to both a symlink and its target when the symlink is forbidden', async () => { const tempWorkspace = createTempDir('workspace-'); const targetFile = path.join(tempWorkspace, 'target.txt'); const symlinkFile = path.join(tempWorkspace, 'link.txt'); fs.writeFileSync(targetFile, 'secret data'); fs.symlinkSync(targetFile, symlinkFile); const osManager = createSandboxManager( { enabled: true }, { workspace: tempWorkspace, forbiddenPaths: async () => [symlinkFile], }, ); // Attempt to write to the target file directly const { command: cmdTarget, args: argsTarget } = Platform.touch(targetFile); const commandTarget = await osManager.prepareCommand({ command: cmdTarget, args: argsTarget, cwd: tempWorkspace, env: process.env, }); const resultTarget = await runCommand(commandTarget); assertResult(resultTarget, commandTarget, 'failure'); // Attempt to write via the symlink const { command: cmdLink, args: argsLink } = Platform.touch(symlinkFile); const commandLink = await osManager.prepareCommand({ command: cmdLink, args: argsLink, cwd: tempWorkspace, env: process.env, }); const resultLink = await runCommand(commandLink); assertResult(resultLink, commandLink, 'failure'); }); }); describe('Git Worktree Support', () => { it('allows access to git common directory in a worktree', async () => { const mainRepo = createTempDir('main-repo-'); const worktreeDir = createTempDir('worktree-'); const mainGitDir = path.join(mainRepo, '.git'); fs.mkdirSync(mainGitDir, { recursive: true }); fs.writeFileSync( path.join(mainGitDir, 'config'), '[core]\n\trepositoryformatversion = 0\n', ); const worktreeGitDir = path.join( mainGitDir, 'worktrees', 'test-worktree', ); fs.mkdirSync(worktreeGitDir, { recursive: true }); // Create the .git file in the worktree directory pointing to the worktree git dir fs.writeFileSync( path.join(worktreeDir, '.git'), `gitdir: ${worktreeGitDir}\n`, ); // Create the backlink from worktree git dir to the worktree's .git file const backlinkPath = path.join(worktreeGitDir, 'gitdir'); fs.writeFileSync(backlinkPath, path.join(worktreeDir, '.git')); // Create a file in the worktree git dir that we want to access const secretFile = path.join(worktreeGitDir, 'secret.txt'); fs.writeFileSync(secretFile, 'git-secret'); const osManager = createSandboxManager( { enabled: true }, { workspace: worktreeDir }, ); const { command, args } = Platform.cat(secretFile); const sandboxed = await osManager.prepareCommand({ command, args, cwd: worktreeDir, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'success'); expect(result.stdout.trim()).toBe('git-secret'); }); it('blocks write access to git common directory in a worktree', async () => { const mainRepo = createTempDir('main-repo-'); const worktreeDir = createTempDir('worktree-'); const mainGitDir = path.join(mainRepo, '.git'); fs.mkdirSync(mainGitDir, { recursive: true }); const worktreeGitDir = path.join( mainGitDir, 'worktrees', 'test-worktree', ); fs.mkdirSync(worktreeGitDir, { recursive: true }); fs.writeFileSync( path.join(worktreeDir, '.git'), `gitdir: ${worktreeGitDir}\n`, ); fs.writeFileSync( path.join(worktreeGitDir, 'gitdir'), path.join(worktreeDir, '.git'), ); const targetFile = path.join(worktreeGitDir, 'secret.txt'); const osManager = createSandboxManager( { enabled: true }, // Use YOLO mode to ensure the workspace is fully writable, but git worktrees should still be read-only { workspace: worktreeDir, modeConfig: { yolo: true } }, ); const { command, args } = Platform.touch(targetFile); const sandboxed = await osManager.prepareCommand({ command, args, cwd: worktreeDir, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'failure'); expect(fs.existsSync(targetFile)).toBe(false); }); }); describe('Network Access', () => { let server: http.Server; let url: string; beforeAll(async () => { server = http.createServer((_, res) => { res.setHeader('Connection', 'close'); res.writeHead(200); res.end('ok'); }); await new Promise((resolve, reject) => { server.on('error', reject); server.listen(0, '127.0.0.1', () => { const addr = server.address() as import('net').AddressInfo; url = `http://127.0.0.1:${addr.port}`; resolve(); }); }); }); afterAll(async () => { if (server) await new Promise((res) => server.close(() => res())); }); // Windows Job Object rate limits exempt loopback (127.0.0.1) traffic, // so this test cannot verify loopback blocking on Windows. it.skipIf(Platform.isWindows)( 'blocks network access by default', async () => { const { command, args } = Platform.curl(url); const sandboxed = await manager.prepareCommand({ command, args, cwd: workspace, env: process.env, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'failure'); }, ); it('grants network access when explicitly allowed', async () => { const { command, args } = Platform.curl(url); const sandboxed = await manager.prepareCommand({ command, args, cwd: workspace, env: process.env, policy: { networkAccess: true }, }); const result = await runCommand(sandboxed); assertResult(result, sandboxed, 'success'); if (!Platform.isWindows) { expect(result.stdout.trim()).toBe('ok'); } }); }); });