mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 20:14:44 -07:00
feat(core): integrate SandboxManager to sandbox all process-spawning tools
- Integrate `SandboxManager` into `Config` and `AgentLoopContext`.
- Refactor `ShellExecutionService` to use sandboxing for PTY and child process spawns.
- Update `GrepTool`, `ShellTool`, and `ToolRegistry` to execute commands via `SandboxManager`.
- Ensure consistent environment sanitization in `spawnAsync` and `execStreaming` utilities.
- Address PR review feedback and fix compilation/lint errors:
- Respect user redaction settings in `NoopSandboxManager`.
- Use idiomatic `async/await` in `GrepTool` availability checks.
- Update license headers to 2026.
- Fix cross-package build errors and exports.
- Resolve all TypeScript and ESLint warnings/errors.
- Update `sandboxConfig.test.ts` to match new `SandboxConfig` schema.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -64,7 +64,9 @@ export class NoopSandboxManager implements SandboxManager {
|
||||
req.config?.sanitizationConfig?.allowedEnvironmentVariables ?? [],
|
||||
blockedEnvironmentVariables:
|
||||
req.config?.sanitizationConfig?.blockedEnvironmentVariables ?? [],
|
||||
enableEnvironmentVariableRedaction: true, // Forced for safety
|
||||
enableEnvironmentVariableRedaction:
|
||||
req.config?.sanitizationConfig?.enableEnvironmentVariableRedaction ??
|
||||
true,
|
||||
};
|
||||
|
||||
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type ShellOutputEvent,
|
||||
type ShellExecutionConfig,
|
||||
} from './shellExecutionService.js';
|
||||
import { NoopSandboxManager } from './sandboxManager.js';
|
||||
import type { AnsiOutput, AnsiToken } from '../utils/terminalSerializer.js';
|
||||
|
||||
// Hoisted Mocks
|
||||
@@ -103,6 +104,7 @@ const shellExecutionConfig: ShellExecutionConfig = {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
},
|
||||
sandboxManager: new NoopSandboxManager(),
|
||||
};
|
||||
|
||||
const createMockSerializeTerminalToObjectReturnValue = (
|
||||
@@ -578,6 +580,7 @@ describe('ShellExecutionService', () => {
|
||||
new AbortController().signal,
|
||||
true,
|
||||
{
|
||||
...shellExecutionConfig,
|
||||
sanitizationConfig: {
|
||||
enableEnvironmentVariableRedaction: true,
|
||||
allowedEnvironmentVariables: [],
|
||||
@@ -1148,6 +1151,7 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
abortController.signal,
|
||||
true,
|
||||
{
|
||||
...shellExecutionConfig,
|
||||
sanitizationConfig: {
|
||||
enableEnvironmentVariableRedaction: true,
|
||||
allowedEnvironmentVariables: [],
|
||||
@@ -1360,6 +1364,7 @@ describe('ShellExecutionService execution method selection', () => {
|
||||
abortController.signal,
|
||||
false, // shouldUseNodePty
|
||||
{
|
||||
...shellExecutionConfig,
|
||||
sanitizationConfig: {
|
||||
enableEnvironmentVariableRedaction: true,
|
||||
allowedEnvironmentVariables: [],
|
||||
@@ -1507,6 +1512,7 @@ describe('ShellExecutionService environment variables', () => {
|
||||
new AbortController().signal,
|
||||
true,
|
||||
{
|
||||
...shellExecutionConfig,
|
||||
sanitizationConfig: {
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
allowedEnvironmentVariables: [],
|
||||
@@ -1566,6 +1572,7 @@ describe('ShellExecutionService environment variables', () => {
|
||||
new AbortController().signal,
|
||||
true,
|
||||
{
|
||||
...shellExecutionConfig,
|
||||
sanitizationConfig: {
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
allowedEnvironmentVariables: [],
|
||||
@@ -1632,4 +1639,56 @@ describe('ShellExecutionService environment variables', () => {
|
||||
mockChildProcess.emit('close', 0, null);
|
||||
await new Promise(process.nextTick);
|
||||
});
|
||||
|
||||
it('should call prepareCommand on sandboxManager when provided', async () => {
|
||||
const mockSandboxManager = {
|
||||
prepareCommand: vi.fn().mockResolvedValue({
|
||||
program: 'sandboxed-bash',
|
||||
args: ['-c', 'ls'],
|
||||
env: { SANDBOXED: 'true' },
|
||||
}),
|
||||
};
|
||||
|
||||
const configWithSandbox: ShellExecutionConfig = {
|
||||
...shellExecutionConfig,
|
||||
sandboxManager: mockSandboxManager,
|
||||
};
|
||||
|
||||
mockResolveExecutable.mockResolvedValue('/bin/bash/resolved');
|
||||
const mockChild = new EventEmitter() as unknown as ChildProcess;
|
||||
mockChild.stdout = new EventEmitter() as unknown as Readable;
|
||||
mockChild.stderr = new EventEmitter() as unknown as Readable;
|
||||
Object.assign(mockChild, { pid: 123 });
|
||||
mockCpSpawn.mockReturnValue(mockChild);
|
||||
|
||||
const handle = await ShellExecutionService.execute(
|
||||
'ls',
|
||||
'/test/cwd',
|
||||
() => {},
|
||||
new AbortController().signal,
|
||||
false, // child_process path
|
||||
configWithSandbox,
|
||||
);
|
||||
|
||||
expect(mockResolveExecutable).toHaveBeenCalledWith(expect.any(String));
|
||||
expect(mockSandboxManager.prepareCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
command: '/bin/bash/resolved',
|
||||
args: expect.arrayContaining([expect.stringContaining('ls')]),
|
||||
cwd: '/test/cwd',
|
||||
}),
|
||||
);
|
||||
expect(mockCpSpawn).toHaveBeenCalledWith(
|
||||
'sandboxed-bash',
|
||||
['-c', 'ls'],
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({ SANDBOXED: 'true' }),
|
||||
}),
|
||||
);
|
||||
|
||||
// Clean up
|
||||
mockChild.emit('exit', 0, null);
|
||||
mockChild.emit('close', 0, null);
|
||||
await handle.result;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -22,10 +22,8 @@ import {
|
||||
serializeTerminalToObject,
|
||||
type AnsiOutput,
|
||||
} from '../utils/terminalSerializer.js';
|
||||
import {
|
||||
sanitizeEnvironment,
|
||||
type EnvironmentSanitizationConfig,
|
||||
} from './environmentSanitization.js';
|
||||
import { type EnvironmentSanitizationConfig } from './environmentSanitization.js';
|
||||
import { type SandboxManager } from './sandboxManager.js';
|
||||
import { killProcessGroup } from '../utils/process-utils.js';
|
||||
const { Terminal } = pkg;
|
||||
|
||||
@@ -102,6 +100,7 @@ export interface ShellExecutionConfig {
|
||||
defaultFg?: string;
|
||||
defaultBg?: string;
|
||||
sanitizationConfig: EnvironmentSanitizationConfig;
|
||||
sandboxManager: SandboxManager;
|
||||
// Used for testing
|
||||
disableDynamicLineTrimming?: boolean;
|
||||
scrollback?: number;
|
||||
@@ -251,7 +250,7 @@ export class ShellExecutionService {
|
||||
cwd,
|
||||
onOutputEvent,
|
||||
abortSignal,
|
||||
shellExecutionConfig.sanitizationConfig,
|
||||
shellExecutionConfig,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -292,33 +291,68 @@ export class ShellExecutionService {
|
||||
}
|
||||
}
|
||||
|
||||
private static childProcessFallback(
|
||||
private static async prepareExecution(
|
||||
executable: string,
|
||||
args: string[],
|
||||
cwd: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
shellExecutionConfig: ShellExecutionConfig,
|
||||
): Promise<{ program: string; args: string[]; env: NodeJS.ProcessEnv }> {
|
||||
const resolvedExecutable =
|
||||
(await resolveExecutable(executable)) ?? executable;
|
||||
|
||||
return shellExecutionConfig.sandboxManager.prepareCommand({
|
||||
command: resolvedExecutable,
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
config: {
|
||||
sanitizationConfig: shellExecutionConfig.sanitizationConfig,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private static async childProcessFallback(
|
||||
commandToExecute: string,
|
||||
cwd: string,
|
||||
onOutputEvent: (event: ShellOutputEvent) => void,
|
||||
abortSignal: AbortSignal,
|
||||
sanitizationConfig: EnvironmentSanitizationConfig,
|
||||
): ShellExecutionHandle {
|
||||
shellExecutionConfig: ShellExecutionConfig,
|
||||
): Promise<ShellExecutionHandle> {
|
||||
try {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
||||
const spawnArgs = [...argsPrefix, guardedCommand];
|
||||
|
||||
const child = cpSpawn(executable, spawnArgs, {
|
||||
const env = {
|
||||
...process.env,
|
||||
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
||||
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
|
||||
TERM: 'xterm-256color',
|
||||
PAGER: 'cat',
|
||||
GIT_PAGER: 'cat',
|
||||
};
|
||||
|
||||
const {
|
||||
program: finalExecutable,
|
||||
args: finalArgs,
|
||||
env: finalEnv,
|
||||
} = await this.prepareExecution(
|
||||
executable,
|
||||
spawnArgs,
|
||||
cwd,
|
||||
env,
|
||||
shellExecutionConfig,
|
||||
);
|
||||
|
||||
const child = cpSpawn(finalExecutable, finalArgs, {
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsVerbatimArguments: isWindows ? false : undefined,
|
||||
shell: false,
|
||||
detached: !isWindows,
|
||||
env: {
|
||||
...sanitizeEnvironment(process.env, sanitizationConfig),
|
||||
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
||||
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
|
||||
TERM: 'xterm-256color',
|
||||
PAGER: 'cat',
|
||||
GIT_PAGER: 'cat',
|
||||
},
|
||||
env: finalEnv,
|
||||
});
|
||||
|
||||
const state = {
|
||||
@@ -557,32 +591,36 @@ export class ShellExecutionService {
|
||||
const rows = shellExecutionConfig.terminalHeight ?? 30;
|
||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
||||
|
||||
const resolvedExecutable = await resolveExecutable(executable);
|
||||
if (!resolvedExecutable) {
|
||||
throw new Error(
|
||||
`Shell executable "${executable}" not found in PATH or at absolute location. Please ensure the shell is installed and available in your environment.`,
|
||||
);
|
||||
}
|
||||
|
||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
||||
const args = [...argsPrefix, guardedCommand];
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
GEMINI_CLI: '1',
|
||||
TERM: 'xterm-256color',
|
||||
PAGER: shellExecutionConfig.pager ?? 'cat',
|
||||
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
|
||||
};
|
||||
|
||||
const {
|
||||
program: finalExecutable,
|
||||
args: finalArgs,
|
||||
env: finalEnv,
|
||||
} = await this.prepareExecution(
|
||||
executable,
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
shellExecutionConfig,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const ptyProcess = ptyInfo.module.spawn(executable, args, {
|
||||
const ptyProcess = ptyInfo.module.spawn(finalExecutable, finalArgs, {
|
||||
cwd,
|
||||
name: 'xterm-256color',
|
||||
cols,
|
||||
rows,
|
||||
env: {
|
||||
...sanitizeEnvironment(
|
||||
process.env,
|
||||
shellExecutionConfig.sanitizationConfig,
|
||||
),
|
||||
GEMINI_CLI: '1',
|
||||
TERM: 'xterm-256color',
|
||||
PAGER: shellExecutionConfig.pager ?? 'cat',
|
||||
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
|
||||
},
|
||||
env: finalEnv,
|
||||
handleFlowControl: true,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user