feat(core): integrate SandboxManager to sandbox all process-spawning tools (#22231)

This commit is contained in:
Gal Zahavi
2026-03-13 14:11:51 -07:00
committed by GitHub
parent 24adacdbc2
commit fa024133e6
31 changed files with 558 additions and 94 deletions
@@ -7,6 +7,7 @@
import type { GeminiClient } from '../core/client.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import type { ToolRegistry } from '../tools/tool-registry.js';
import type { SandboxManager } from '../services/sandboxManager.js';
import type { Config } from './config.js';
/**
@@ -28,4 +29,7 @@ export interface AgentLoopContext {
/** The client used to communicate with the LLM in this context. */
readonly geminiClient: GeminiClient;
/** The service used to prepare commands for sandboxed execution. */
readonly sandboxManager: SandboxManager;
}
+28 -1
View File
@@ -41,6 +41,10 @@ import { LocalLiteRtLmClient } from '../core/localLiteRtLmClient.js';
import type { HookDefinition, HookEventName } from '../hooks/types.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { GitService } from '../services/gitService.js';
import {
createSandboxManager,
type SandboxManager,
} from '../services/sandboxManager.js';
import {
initializeTelemetry,
DEFAULT_TELEMETRY_TARGET,
@@ -510,6 +514,7 @@ export interface ConfigParameters {
clientVersion?: string;
embeddingModel?: string;
sandbox?: SandboxConfig;
toolSandboxing?: boolean;
targetDir: string;
debugMode: boolean;
question?: string;
@@ -686,6 +691,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly telemetrySettings: TelemetrySettings;
private readonly usageStatisticsEnabled: boolean;
private _geminiClient!: GeminiClient;
private readonly _sandboxManager: SandboxManager;
private baseLlmClient!: BaseLlmClient;
private localLiteRtLmClient?: LocalLiteRtLmClient;
private modelRouterService: ModelRouterService;
@@ -855,7 +861,19 @@ export class Config implements McpContext, AgentLoopContext {
this.embeddingModel =
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
this.fileSystemService = new StandardFileSystemService();
this.sandbox = params.sandbox;
this.sandbox = params.sandbox
? {
enabled: params.sandbox.enabled ?? false,
allowedPaths: params.sandbox.allowedPaths ?? [],
networkAccess: params.sandbox.networkAccess ?? false,
command: params.sandbox.command,
image: params.sandbox.image,
}
: {
enabled: false,
allowedPaths: [],
networkAccess: false,
};
this.targetDir = path.resolve(params.targetDir);
this.folderTrust = params.folderTrust ?? false;
this.workspaceContext = new WorkspaceContext(this.targetDir, []);
@@ -985,6 +1003,7 @@ export class Config implements McpContext, AgentLoopContext {
showColor: params.shellExecutionConfig?.showColor ?? false,
pager: params.shellExecutionConfig?.pager ?? 'cat',
sanitizationConfig: this.sanitizationConfig,
sandboxManager: this.sandboxManager,
};
this.truncateToolOutputThreshold =
params.truncateToolOutputThreshold ??
@@ -1102,6 +1121,8 @@ export class Config implements McpContext, AgentLoopContext {
}
}
this._geminiClient = new GeminiClient(this);
this._sandboxManager = createSandboxManager(params.toolSandboxing ?? false);
this.shellExecutionConfig.sandboxManager = this._sandboxManager;
this.modelRouterService = new ModelRouterService(this);
// HACK: The settings loading logic doesn't currently merge the default
@@ -1423,6 +1444,10 @@ export class Config implements McpContext, AgentLoopContext {
return this._geminiClient;
}
get sandboxManager(): SandboxManager {
return this._sandboxManager;
}
getSessionId(): string {
return this.promptId;
}
@@ -2810,6 +2835,8 @@ export class Config implements McpContext, AgentLoopContext {
sanitizationConfig:
config.sanitizationConfig ??
this.shellExecutionConfig.sanitizationConfig,
sandboxManager:
config.sandboxManager ?? this.shellExecutionConfig.sandboxManager,
};
}
getScreenReader(): boolean {
@@ -0,0 +1,65 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { Config } from './config.js';
import { NoopSandboxManager } from '../services/sandboxManager.js';
// Minimal mocks for Config dependencies to allow instantiation
vi.mock('../core/client.js');
vi.mock('../core/contentGenerator.js');
vi.mock('../telemetry/index.js');
vi.mock('../core/tokenLimits.js');
vi.mock('../services/fileDiscoveryService.js');
vi.mock('../services/gitService.js');
vi.mock('../services/trackerService.js');
vi.mock('../confirmation-bus/message-bus.js', () => ({
MessageBus: vi.fn(),
}));
vi.mock('../policy/policy-engine.js', () => ({
PolicyEngine: vi.fn().mockImplementation(() => ({
getExcludedTools: vi.fn().mockReturnValue(new Set()),
})),
}));
vi.mock('../skills/skillManager.js', () => ({
SkillManager: vi.fn().mockImplementation(() => ({
setAdminSettings: vi.fn(),
})),
}));
vi.mock('../agents/registry.js', () => ({
AgentRegistry: vi.fn().mockImplementation(() => ({
initialize: vi.fn(),
})),
}));
vi.mock('../agents/acknowledgedAgents.js', () => ({
AcknowledgedAgentsService: vi.fn(),
}));
vi.mock('../services/modelConfigService.js', () => ({
ModelConfigService: vi.fn(),
}));
vi.mock('./models.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./models.js')>();
return {
...actual,
isPreviewModel: vi.fn().mockReturnValue(false),
resolveModel: vi.fn().mockReturnValue('test-model'),
};
});
describe('Sandbox Integration', () => {
it('should have a NoopSandboxManager by default in Config', () => {
const config = new Config({
sessionId: 'test-session',
targetDir: '.',
model: 'test-model',
cwd: '.',
debugMode: false,
});
expect(config.sandboxManager).toBeDefined();
expect(config.sandboxManager).toBeInstanceOf(NoopSandboxManager);
});
});