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:
galz10
2026-03-09 14:57:45 -07:00
parent 863a0aa01e
commit 002a57efeb
25 changed files with 533 additions and 107 deletions
@@ -20,7 +20,12 @@ import {
import { createExtension } from '../test-utils/createExtension.js';
import { ExtensionManager } from './extension-manager.js';
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { GEMINI_DIR, type Config, tmpdir } from '@google/gemini-cli-core';
import {
GEMINI_DIR,
type Config,
tmpdir,
NoopSandboxManager,
} from '@google/gemini-cli-core';
import { createTestMergedSettings, SettingScope } from './settings.js';
describe('ExtensionManager theme loading', () => {
@@ -117,6 +122,7 @@ describe('ExtensionManager theme loading', () => {
terminalHeight: 24,
showColor: false,
pager: 'cat',
sandboxManager: new NoopSandboxManager(),
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
+90 -12
View File
@@ -90,7 +90,13 @@ describe('loadSandboxConfig', () => {
process.env['GEMINI_SANDBOX'] = 'docker';
mockedCommandExistsSync.mockReturnValue(true);
const config = await loadSandboxConfig({}, {});
expect(config).toEqual({ command: 'docker', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'docker',
image: 'default/image',
});
expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker');
});
@@ -113,7 +119,13 @@ describe('loadSandboxConfig', () => {
process.env['GEMINI_SANDBOX'] = 'lxc';
mockedCommandExistsSync.mockReturnValue(true);
const config = await loadSandboxConfig({}, {});
expect(config).toEqual({ command: 'lxc', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'lxc',
image: 'default/image',
});
expect(mockedCommandExistsSync).toHaveBeenCalledWith('lxc');
});
@@ -134,6 +146,9 @@ describe('loadSandboxConfig', () => {
);
const config = await loadSandboxConfig({}, { sandbox: true });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'sandbox-exec',
image: 'default/image',
});
@@ -144,6 +159,9 @@ describe('loadSandboxConfig', () => {
mockedCommandExistsSync.mockReturnValue(true); // all commands exist
const config = await loadSandboxConfig({}, { sandbox: true });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'sandbox-exec',
image: 'default/image',
});
@@ -153,14 +171,26 @@ describe('loadSandboxConfig', () => {
mockedOsPlatform.mockReturnValue('linux');
mockedCommandExistsSync.mockImplementation((cmd) => cmd === 'docker');
const config = await loadSandboxConfig({ tools: { sandbox: true } }, {});
expect(config).toEqual({ command: 'docker', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'docker',
image: 'default/image',
});
});
it('should use podman if available and docker is not', async () => {
mockedOsPlatform.mockReturnValue('linux');
mockedCommandExistsSync.mockImplementation((cmd) => cmd === 'podman');
const config = await loadSandboxConfig({}, { sandbox: true });
expect(config).toEqual({ command: 'podman', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'podman',
image: 'default/image',
});
});
it('should throw if sandbox: true but no command is found', async () => {
@@ -177,7 +207,13 @@ describe('loadSandboxConfig', () => {
it('should use the specified command if it exists', async () => {
mockedCommandExistsSync.mockReturnValue(true);
const config = await loadSandboxConfig({}, { sandbox: 'podman' });
expect(config).toEqual({ command: 'podman', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'podman',
image: 'default/image',
});
expect(mockedCommandExistsSync).toHaveBeenCalledWith('podman');
});
@@ -205,14 +241,26 @@ describe('loadSandboxConfig', () => {
process.env['GEMINI_SANDBOX'] = 'docker';
mockedCommandExistsSync.mockReturnValue(true);
const config = await loadSandboxConfig({}, {});
expect(config).toEqual({ command: 'docker', image: 'env/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'docker',
image: 'env/image',
});
});
it('should use image from package.json if env var is not set', async () => {
process.env['GEMINI_SANDBOX'] = 'docker';
mockedCommandExistsSync.mockReturnValue(true);
const config = await loadSandboxConfig({}, {});
expect(config).toEqual({ command: 'docker', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'docker',
image: 'default/image',
});
});
it('should return undefined if command is found but no image is configured', async () => {
@@ -234,7 +282,13 @@ describe('loadSandboxConfig', () => {
'should enable sandbox for value: %s',
async (value) => {
const config = await loadSandboxConfig({}, { sandbox: value });
expect(config).toEqual({ command: 'docker', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'docker',
image: 'default/image',
});
},
);
@@ -257,7 +311,13 @@ describe('loadSandboxConfig', () => {
it('should use runsc via CLI argument on Linux', async () => {
const config = await loadSandboxConfig({}, { sandbox: 'runsc' });
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'runsc',
image: 'default/image',
});
expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc');
expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker');
});
@@ -266,7 +326,13 @@ describe('loadSandboxConfig', () => {
process.env['GEMINI_SANDBOX'] = 'runsc';
const config = await loadSandboxConfig({}, {});
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'runsc',
image: 'default/image',
});
expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc');
expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker');
});
@@ -277,7 +343,13 @@ describe('loadSandboxConfig', () => {
{},
);
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'runsc',
image: 'default/image',
});
expect(mockedCommandExistsSync).toHaveBeenCalledWith('runsc');
expect(mockedCommandExistsSync).toHaveBeenCalledWith('docker');
});
@@ -289,7 +361,13 @@ describe('loadSandboxConfig', () => {
{ sandbox: 'podman' },
);
expect(config).toEqual({ command: 'runsc', image: 'default/image' });
expect(config).toEqual({
enabled: true,
allowedPaths: [],
networkAccess: false,
command: 'runsc',
image: 'default/image',
});
});
it('should reject runsc on macOS (Linux-only)', async () => {
+3 -1
View File
@@ -34,7 +34,9 @@ const VALID_SANDBOX_COMMANDS: ReadonlyArray<SandboxConfig['command']> = [
function isSandboxCommand(
value: string,
): value is Exclude<SandboxConfig['command'], undefined> {
return (VALID_SANDBOX_COMMANDS as readonly string[]).includes(value);
return (VALID_SANDBOX_COMMANDS as ReadonlyArray<string | undefined>).includes(
value,
);
}
function getSandboxCommand(