mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 19:14:33 -07:00
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { MacOsSandboxManager } from './MacOsSandboxManager.js';
|
|
import { ShellExecutionService } from '../../services/shellExecutionService.js';
|
|
import { getSecureSanitizationConfig } from '../../services/environmentSanitization.js';
|
|
import { type SandboxedCommand } from '../../services/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';
|
|
|
|
/**
|
|
* A simple asynchronous wrapper for execFile that returns the exit status,
|
|
* stdout, and stderr. Unlike spawnSync, this does not block the Node.js
|
|
* event loop, allowing the local HTTP test server to function.
|
|
*/
|
|
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 ?? '',
|
|
};
|
|
}
|
|
}
|
|
|
|
describe.skipIf(os.platform() !== 'darwin')(
|
|
'MacOsSandboxManager Integration',
|
|
() => {
|
|
describe('Basic Execution', () => {
|
|
it('should execute commands within the workspace', async () => {
|
|
const manager = new MacOsSandboxManager({ workspace: process.cwd() });
|
|
const command = await manager.prepareCommand({
|
|
command: 'echo',
|
|
args: ['sandbox test'],
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
});
|
|
|
|
const execResult = await runCommand(command);
|
|
|
|
expect(execResult.status).toBe(0);
|
|
expect(execResult.stdout.trim()).toBe('sandbox test');
|
|
});
|
|
|
|
it('should support interactive pseudo-terminals (node-pty)', async () => {
|
|
const manager = new MacOsSandboxManager({ workspace: process.cwd() });
|
|
const abortController = new AbortController();
|
|
|
|
// Verify that node-pty file descriptors are successfully allocated inside the sandbox
|
|
// by using the bash [ -t 1 ] idiom to check if stdout is a TTY.
|
|
const handle = await ShellExecutionService.execute(
|
|
'bash -c "if [ -t 1 ]; then echo True; else echo False; fi"',
|
|
process.cwd(),
|
|
() => {},
|
|
abortController.signal,
|
|
true,
|
|
{
|
|
sanitizationConfig: getSecureSanitizationConfig(),
|
|
sandboxManager: manager,
|
|
},
|
|
);
|
|
|
|
const result = await handle.result;
|
|
expect(result.error).toBeNull();
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.output).toContain('True');
|
|
});
|
|
});
|
|
|
|
describe('File System Access', () => {
|
|
it('should block file system access outside the workspace', async () => {
|
|
const manager = new MacOsSandboxManager({ workspace: process.cwd() });
|
|
const blockedPath = '/Users/Shared/.gemini_test_sandbox_blocked';
|
|
|
|
const command = await manager.prepareCommand({
|
|
command: 'touch',
|
|
args: [blockedPath],
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
});
|
|
const execResult = await runCommand(command);
|
|
|
|
expect(execResult.status).not.toBe(0);
|
|
expect(execResult.stderr).toContain('Operation not permitted');
|
|
});
|
|
|
|
it('should grant file system access to explicitly allowed paths', async () => {
|
|
// Create a unique temporary directory to prevent artifacts and test flakiness
|
|
const allowedDir = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'gemini-sandbox-test-'),
|
|
);
|
|
|
|
try {
|
|
const manager = new MacOsSandboxManager({
|
|
workspace: process.cwd(),
|
|
});
|
|
const testFile = path.join(allowedDir, 'test.txt');
|
|
|
|
const command = await manager.prepareCommand({
|
|
command: 'touch',
|
|
args: [testFile],
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
policy: {
|
|
allowedPaths: [allowedDir],
|
|
},
|
|
});
|
|
|
|
const execResult = await runCommand(command);
|
|
|
|
expect(execResult.status).toBe(0);
|
|
} finally {
|
|
fs.rmSync(allowedDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Network Access', () => {
|
|
let testServer: http.Server;
|
|
let testServerUrl: string;
|
|
|
|
beforeAll(async () => {
|
|
testServer = http.createServer((_, res) => {
|
|
// Ensure connections are closed immediately to prevent hanging
|
|
res.setHeader('Connection', 'close');
|
|
res.writeHead(200);
|
|
res.end('ok');
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
testServer.on('error', reject);
|
|
testServer.listen(0, '127.0.0.1', () => {
|
|
const address = testServer.address() as import('net').AddressInfo;
|
|
testServerUrl = `http://127.0.0.1:${address.port}`;
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (testServer) {
|
|
await new Promise<void>((resolve) => {
|
|
testServer.close(() => resolve());
|
|
});
|
|
}
|
|
});
|
|
|
|
it('should block network access by default', async () => {
|
|
const manager = new MacOsSandboxManager({ workspace: process.cwd() });
|
|
const command = await manager.prepareCommand({
|
|
command: 'curl',
|
|
args: ['-s', '--connect-timeout', '1', testServerUrl],
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
});
|
|
|
|
const execResult = await runCommand(command);
|
|
|
|
expect(execResult.status).not.toBe(0);
|
|
});
|
|
|
|
it('should grant network access when explicitly allowed', async () => {
|
|
const manager = new MacOsSandboxManager({
|
|
workspace: process.cwd(),
|
|
});
|
|
const command = await manager.prepareCommand({
|
|
command: 'curl',
|
|
args: ['-s', '--connect-timeout', '1', testServerUrl],
|
|
cwd: process.cwd(),
|
|
env: process.env,
|
|
policy: {
|
|
networkAccess: true,
|
|
},
|
|
});
|
|
|
|
const execResult = await runCommand(command);
|
|
|
|
expect(execResult.status).toBe(0);
|
|
expect(execResult.stdout.trim()).toBe('ok');
|
|
});
|
|
});
|
|
},
|
|
);
|