mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-19 01:30:42 -07:00
feat(sandbox): resolve custom seatbelt profiles from $HOME/.gemini first (#25427)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
@@ -8,8 +8,13 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { spawn, exec, execFile, execSync } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { start_sandbox } from './sandbox.js';
|
||||
import { FatalSandboxError, type SandboxConfig } from '@google/gemini-cli-core';
|
||||
import {
|
||||
FatalSandboxError,
|
||||
homedir,
|
||||
type SandboxConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockSandboxConfig } from '@google/gemini-cli-test-utils';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
@@ -133,6 +138,7 @@ describe('sandbox', () => {
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
process.argv = originalArgv;
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe('start_sandbox', () => {
|
||||
@@ -171,6 +177,105 @@ describe('sandbox', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve custom seatbelt profile from user home directory', async () => {
|
||||
vi.mocked(os.platform).mockReturnValue('darwin');
|
||||
vi.stubEnv('SEATBELT_PROFILE', 'custom-test');
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) =>
|
||||
String(p).includes(
|
||||
path.join(homedir(), '.gemini', 'sandbox-macos-custom-test.sb'),
|
||||
),
|
||||
);
|
||||
const config: SandboxConfig = createMockSandboxConfig({
|
||||
command: 'sandbox-exec',
|
||||
image: 'some-image',
|
||||
});
|
||||
|
||||
interface MockProcess extends EventEmitter {
|
||||
stdout: EventEmitter;
|
||||
stderr: EventEmitter;
|
||||
}
|
||||
const mockSpawnProcess = new EventEmitter() as MockProcess;
|
||||
mockSpawnProcess.stdout = new EventEmitter();
|
||||
mockSpawnProcess.stderr = new EventEmitter();
|
||||
vi.mocked(spawn).mockReturnValue(
|
||||
mockSpawnProcess as unknown as ReturnType<typeof spawn>,
|
||||
);
|
||||
|
||||
const promise = start_sandbox(config, [], undefined, ['arg1']);
|
||||
|
||||
setTimeout(() => {
|
||||
mockSpawnProcess.emit('close', 0);
|
||||
}, 10);
|
||||
|
||||
await expect(promise).resolves.toBe(0);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'sandbox-exec',
|
||||
expect.any(Array),
|
||||
expect.objectContaining({ stdio: 'inherit' }),
|
||||
);
|
||||
const spawnArgs = vi.mocked(spawn).mock.calls[0]?.[1];
|
||||
expect(spawnArgs).toEqual(
|
||||
expect.arrayContaining(['-f', expect.any(String)]),
|
||||
);
|
||||
const profileArg = spawnArgs?.[spawnArgs.indexOf('-f') + 1];
|
||||
expect(profileArg).toEqual(
|
||||
expect.stringContaining(
|
||||
path.join(homedir(), '.gemini', 'sandbox-macos-custom-test.sb'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to project .gemini directory when user profile is missing', async () => {
|
||||
vi.mocked(os.platform).mockReturnValue('darwin');
|
||||
vi.stubEnv('SEATBELT_PROFILE', 'custom-test');
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
||||
const s = String(p);
|
||||
return (
|
||||
s.includes(path.join('.gemini', 'sandbox-macos-custom-test.sb')) &&
|
||||
!s.includes(path.join(homedir(), '.gemini'))
|
||||
);
|
||||
});
|
||||
const config: SandboxConfig = createMockSandboxConfig({
|
||||
command: 'sandbox-exec',
|
||||
image: 'some-image',
|
||||
});
|
||||
|
||||
interface MockProcess extends EventEmitter {
|
||||
stdout: EventEmitter;
|
||||
stderr: EventEmitter;
|
||||
}
|
||||
const mockSpawnProcess = new EventEmitter() as MockProcess;
|
||||
mockSpawnProcess.stdout = new EventEmitter();
|
||||
mockSpawnProcess.stderr = new EventEmitter();
|
||||
vi.mocked(spawn).mockReturnValue(
|
||||
mockSpawnProcess as unknown as ReturnType<typeof spawn>,
|
||||
);
|
||||
|
||||
const promise = start_sandbox(config, [], undefined, ['arg1']);
|
||||
|
||||
setTimeout(() => {
|
||||
mockSpawnProcess.emit('close', 0);
|
||||
}, 10);
|
||||
|
||||
await expect(promise).resolves.toBe(0);
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'sandbox-exec',
|
||||
expect.any(Array),
|
||||
expect.objectContaining({ stdio: 'inherit' }),
|
||||
);
|
||||
const spawnArgs = vi.mocked(spawn).mock.calls[0]?.[1];
|
||||
expect(spawnArgs).toEqual(
|
||||
expect.arrayContaining(['-f', expect.any(String)]),
|
||||
);
|
||||
const profileArg = spawnArgs?.[spawnArgs.indexOf('-f') + 1];
|
||||
expect(profileArg).toEqual(
|
||||
expect.stringContaining(
|
||||
path.join('.gemini', 'sandbox-macos-custom-test.sb'),
|
||||
),
|
||||
);
|
||||
expect(profileArg).not.toContain(homedir());
|
||||
});
|
||||
|
||||
it('should throw FatalSandboxError if seatbelt profile is missing', async () => {
|
||||
vi.mocked(os.platform).mockReturnValue('darwin');
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
@@ -68,9 +68,17 @@ export async function start_sandbox(
|
||||
let profileFile = fileURLToPath(
|
||||
new URL(`sandbox-macos-${profile}.sb`, import.meta.url),
|
||||
);
|
||||
// if profile name is not recognized, then look for file under project settings directory
|
||||
// if profile name is not recognized, look in user-level ~/.gemini first,
|
||||
// then fall back to project-level .gemini. path.basename() strips any
|
||||
// directory separators to prevent path traversal via SEATBELT_PROFILE.
|
||||
if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
|
||||
profileFile = path.join(GEMINI_DIR, `sandbox-macos-${profile}.sb`);
|
||||
const safeProfile = path.basename(profile);
|
||||
const fileName = `sandbox-macos-${safeProfile}.sb`;
|
||||
const userProfileFile = path.join(homedir(), GEMINI_DIR, fileName);
|
||||
const projectProfileFile = path.join(GEMINI_DIR, fileName);
|
||||
profileFile = fs.existsSync(userProfileFile)
|
||||
? userProfileFile
|
||||
: projectProfileFile;
|
||||
}
|
||||
if (!fs.existsSync(profileFile)) {
|
||||
throw new FatalSandboxError(
|
||||
|
||||
Reference in New Issue
Block a user