mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 10:10:56 -07:00
feat(core): refactor SandboxManager to a stateless architecture and introduce explicit Deny interface (#23141)
This commit is contained in:
@@ -4,24 +4,20 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { LinuxSandboxManager } from './LinuxSandboxManager.js';
|
||||
import type { SandboxRequest } from '../../services/sandboxManager.js';
|
||||
|
||||
describe('LinuxSandboxManager', () => {
|
||||
const workspace = '/home/user/workspace';
|
||||
let manager: LinuxSandboxManager;
|
||||
|
||||
it('correctly outputs bwrap as the program with appropriate isolation flags', async () => {
|
||||
const manager = new LinuxSandboxManager({ workspace });
|
||||
const req: SandboxRequest = {
|
||||
command: 'ls',
|
||||
args: ['-la'],
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
};
|
||||
beforeEach(() => {
|
||||
manager = new LinuxSandboxManager({ workspace });
|
||||
});
|
||||
|
||||
const getBwrapArgs = async (req: SandboxRequest) => {
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
expect(result.program).toBe('sh');
|
||||
expect(result.args[0]).toBe('-c');
|
||||
expect(result.args[1]).toBe(
|
||||
@@ -29,8 +25,17 @@ describe('LinuxSandboxManager', () => {
|
||||
);
|
||||
expect(result.args[2]).toBe('_');
|
||||
expect(result.args[3]).toMatch(/gemini-cli-seccomp-.*\.bpf$/);
|
||||
return result.args.slice(4);
|
||||
};
|
||||
|
||||
it('correctly outputs bwrap as the program with appropriate isolation flags', async () => {
|
||||
const bwrapArgs = await getBwrapArgs({
|
||||
command: 'ls',
|
||||
args: ['-la'],
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
});
|
||||
|
||||
const bwrapArgs = result.args.slice(4);
|
||||
expect(bwrapArgs).toEqual([
|
||||
'--unshare-all',
|
||||
'--new-session',
|
||||
@@ -56,55 +61,48 @@ describe('LinuxSandboxManager', () => {
|
||||
});
|
||||
|
||||
it('maps allowedPaths to bwrap binds', async () => {
|
||||
const manager = new LinuxSandboxManager({
|
||||
workspace,
|
||||
allowedPaths: ['/tmp/cache', '/opt/tools', workspace],
|
||||
});
|
||||
const req: SandboxRequest = {
|
||||
const bwrapArgs = await getBwrapArgs({
|
||||
command: 'node',
|
||||
args: ['script.js'],
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
};
|
||||
policy: {
|
||||
allowedPaths: ['/tmp/cache', '/opt/tools', workspace],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
// Verify the specific bindings were added correctly
|
||||
const bindsIndex = bwrapArgs.indexOf('--seccomp');
|
||||
const binds = bwrapArgs.slice(bwrapArgs.indexOf('--bind'), bindsIndex);
|
||||
|
||||
expect(result.program).toBe('sh');
|
||||
expect(result.args[0]).toBe('-c');
|
||||
expect(result.args[1]).toBe(
|
||||
'bpf_path="$1"; shift; exec bwrap "$@" 9< "$bpf_path"',
|
||||
);
|
||||
expect(result.args[2]).toBe('_');
|
||||
expect(result.args[3]).toMatch(/gemini-cli-seccomp-.*\.bpf$/);
|
||||
|
||||
const bwrapArgs = result.args.slice(4);
|
||||
expect(bwrapArgs).toEqual([
|
||||
'--unshare-all',
|
||||
'--new-session',
|
||||
'--die-with-parent',
|
||||
'--ro-bind',
|
||||
'/',
|
||||
'/',
|
||||
'--dev',
|
||||
'/dev',
|
||||
'--proc',
|
||||
'/proc',
|
||||
'--tmpfs',
|
||||
'/tmp',
|
||||
expect(binds).toEqual([
|
||||
'--bind',
|
||||
workspace,
|
||||
workspace,
|
||||
'--bind',
|
||||
'--bind-try',
|
||||
'/tmp/cache',
|
||||
'/tmp/cache',
|
||||
'--bind',
|
||||
'--bind-try',
|
||||
'/opt/tools',
|
||||
'/opt/tools',
|
||||
'--seccomp',
|
||||
'9',
|
||||
'--',
|
||||
'node',
|
||||
'script.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not bind the workspace twice even if it has a trailing slash in allowedPaths', async () => {
|
||||
const bwrapArgs = await getBwrapArgs({
|
||||
command: 'ls',
|
||||
args: ['-la'],
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
policy: {
|
||||
allowedPaths: [workspace + '/'],
|
||||
},
|
||||
});
|
||||
|
||||
const bindsIndex = bwrapArgs.indexOf('--seccomp');
|
||||
const binds = bwrapArgs.slice(bwrapArgs.indexOf('--bind'), bindsIndex);
|
||||
|
||||
// Should only contain the primary workspace bind, not the second one with a trailing slash
|
||||
expect(binds).toEqual(['--bind', workspace, workspace]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { join } from 'node:path';
|
||||
import { join, normalize } from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import {
|
||||
type SandboxManager,
|
||||
type GlobalSandboxOptions,
|
||||
type SandboxRequest,
|
||||
type SandboxedCommand,
|
||||
sanitizePaths,
|
||||
} from '../../services/sandboxManager.js';
|
||||
import {
|
||||
sanitizeEnvironment,
|
||||
getSecureSanitizationConfig,
|
||||
type EnvironmentSanitizationConfig,
|
||||
} from '../../services/environmentSanitization.js';
|
||||
|
||||
let cachedBpfPath: string | undefined;
|
||||
@@ -76,28 +77,15 @@ function getSeccompBpfPath(): string {
|
||||
return bpfPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring the LinuxSandboxManager.
|
||||
*/
|
||||
export interface LinuxSandboxOptions {
|
||||
/** The primary workspace path to bind into the sandbox. */
|
||||
workspace: string;
|
||||
/** Additional paths to bind into the sandbox. */
|
||||
allowedPaths?: string[];
|
||||
/** Optional base sanitization config. */
|
||||
sanitizationConfig?: EnvironmentSanitizationConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* A SandboxManager implementation for Linux that uses Bubblewrap (bwrap).
|
||||
*/
|
||||
export class LinuxSandboxManager implements SandboxManager {
|
||||
constructor(private readonly options: LinuxSandboxOptions) {}
|
||||
constructor(private readonly options: GlobalSandboxOptions) {}
|
||||
|
||||
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
|
||||
const sanitizationConfig = getSecureSanitizationConfig(
|
||||
req.config?.sanitizationConfig,
|
||||
this.options.sanitizationConfig,
|
||||
req.policy?.sanitizationConfig,
|
||||
);
|
||||
|
||||
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
|
||||
@@ -121,13 +109,20 @@ export class LinuxSandboxManager implements SandboxManager {
|
||||
this.options.workspace,
|
||||
];
|
||||
|
||||
const allowedPaths = this.options.allowedPaths ?? [];
|
||||
for (const path of allowedPaths) {
|
||||
if (path !== this.options.workspace) {
|
||||
bwrapArgs.push('--bind', path, path);
|
||||
const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || [];
|
||||
const normalizedWorkspace = normalize(this.options.workspace).replace(
|
||||
/\/$/,
|
||||
'',
|
||||
);
|
||||
for (const allowedPath of allowedPaths) {
|
||||
const normalizedAllowedPath = normalize(allowedPath).replace(/\/$/, '');
|
||||
if (normalizedAllowedPath !== normalizedWorkspace) {
|
||||
bwrapArgs.push('--bind-try', allowedPath, allowedPath);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle forbidden paths
|
||||
|
||||
const bpfPath = getSeccompBpfPath();
|
||||
|
||||
bwrapArgs.push('--seccomp', '9');
|
||||
|
||||
Reference in New Issue
Block a user