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
+40 -12
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -301,15 +301,41 @@ class GrepToolInvocation extends BaseToolInvocation<
* @param {string} command The command name (e.g., 'git', 'grep').
* @returns {Promise<boolean>} True if the command is available, false otherwise.
*/
private isCommandAvailable(command: string): Promise<boolean> {
return new Promise((resolve) => {
const checkCommand = process.platform === 'win32' ? 'where' : 'command';
const checkArgs =
process.platform === 'win32' ? [command] : ['-v', command];
try {
const child = spawn(checkCommand, checkArgs, {
private async isCommandAvailable(command: string): Promise<boolean> {
const checkCommand = process.platform === 'win32' ? 'where' : 'command';
const checkArgs =
process.platform === 'win32' ? [command] : ['-v', command];
try {
const sandboxManager = this.config.sandboxManager;
let finalCommand = checkCommand;
let finalArgs = checkArgs;
let finalEnv = process.env;
if (sandboxManager) {
try {
const prepared = await sandboxManager.prepareCommand({
command: checkCommand,
args: checkArgs,
cwd: process.cwd(),
env: process.env,
});
finalCommand = prepared.program;
finalArgs = prepared.args;
finalEnv = prepared.env;
} catch (err) {
debugLogger.debug(
`[GrepTool] Sandbox preparation failed for '${command}':`,
err,
);
}
}
return await new Promise((resolve) => {
const child = spawn(finalCommand, finalArgs, {
stdio: 'ignore',
shell: true,
env: finalEnv,
});
child.on('close', (code) => resolve(code === 0));
child.on('error', (err) => {
@@ -319,10 +345,10 @@ class GrepToolInvocation extends BaseToolInvocation<
);
resolve(false);
});
} catch {
resolve(false);
}
});
});
} catch {
return false;
}
}
/**
@@ -381,6 +407,7 @@ class GrepToolInvocation extends BaseToolInvocation<
cwd: absolutePath,
signal: options.signal,
allowedExitCodes: [0, 1],
sandboxManager: this.config.sandboxManager,
});
const results: GrepMatch[] = [];
@@ -452,6 +479,7 @@ class GrepToolInvocation extends BaseToolInvocation<
cwd: absolutePath,
signal: options.signal,
allowedExitCodes: [0, 1],
sandboxManager: this.config.sandboxManager,
});
for await (const line of generator) {
+1
View File
@@ -476,6 +476,7 @@ class GrepToolInvocation extends BaseToolInvocation<
const generator = execStreaming(rgPath, rgArgs, {
signal: options.signal,
allowedExitCodes: [0, 1],
sandboxManager: this.config.sandboxManager,
});
let matchesFound = 0;
+22 -4
View File
@@ -45,6 +45,7 @@ import { initializeShellParsers } from '../utils/shell-utils.js';
import { ShellTool, OUTPUT_UPDATE_INTERVAL_MS } from './shell.js';
import { debugLogger } from '../index.js';
import { type Config } from '../config/config.js';
import { NoopSandboxManager } from '../services/sandboxManager.js';
import {
type ShellExecutionResult,
type ShellOutputEvent,
@@ -137,6 +138,7 @@ describe('ShellTool', () => {
getEnableInteractiveShell: vi.fn().mockReturnValue(false),
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
sanitizationConfig: {},
sandboxManager: new NoopSandboxManager(),
} as unknown as Config;
const bus = createMockMessageBus();
@@ -281,7 +283,11 @@ describe('ShellTool', () => {
expect.any(Function),
expect.any(AbortSignal),
false,
{ pager: 'cat', sanitizationConfig: {} },
expect.objectContaining({
pager: 'cat',
sanitizationConfig: {},
sandboxManager: expect.any(Object),
}),
);
expect(result.llmContent).toContain('Background PIDs: 54322');
// The file should be deleted by the tool
@@ -306,7 +312,11 @@ describe('ShellTool', () => {
expect.any(Function),
expect.any(AbortSignal),
false,
{ pager: 'cat', sanitizationConfig: {} },
expect.objectContaining({
pager: 'cat',
sanitizationConfig: {},
sandboxManager: expect.any(Object),
}),
);
});
@@ -327,7 +337,11 @@ describe('ShellTool', () => {
expect.any(Function),
expect.any(AbortSignal),
false,
{ pager: 'cat', sanitizationConfig: {} },
expect.objectContaining({
pager: 'cat',
sanitizationConfig: {},
sandboxManager: expect.any(Object),
}),
);
});
@@ -373,7 +387,11 @@ describe('ShellTool', () => {
expect.any(Function),
expect.any(AbortSignal),
false,
{ pager: 'cat', sanitizationConfig: {} },
{
pager: 'cat',
sanitizationConfig: {},
sandboxManager: new NoopSandboxManager(),
},
);
},
20000,
+1
View File
@@ -278,6 +278,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
sanitizationConfig:
shellExecutionConfig?.sanitizationConfig ??
this.context.config.sanitizationConfig,
sandboxManager: this.context.config.sandboxManager,
},
);
+52 -3
View File
@@ -57,7 +57,28 @@ class DiscoveredToolInvocation extends BaseToolInvocation<
_updateOutput?: (output: string) => void,
): Promise<ToolResult> {
const callCommand = this.config.getToolCallCommand()!;
const child = spawn(callCommand, [this.originalToolName]);
const args = [this.originalToolName];
let finalCommand = callCommand;
let finalArgs = args;
let finalEnv = process.env;
const sandboxManager = this.config.sandboxManager;
if (sandboxManager) {
const prepared = await sandboxManager.prepareCommand({
command: callCommand,
args,
cwd: process.cwd(),
env: process.env,
});
finalCommand = prepared.program;
finalArgs = prepared.args;
finalEnv = prepared.env;
}
const child = spawn(finalCommand, finalArgs, {
env: finalEnv,
});
child.stdin.write(JSON.stringify(this.params));
child.stdin.end();
@@ -322,8 +343,36 @@ export class ToolRegistry {
'Tool discovery command is empty or contains only whitespace.',
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const proc = spawn(cmdParts[0] as string, cmdParts.slice(1) as string[]);
const firstPart = cmdParts[0];
if (typeof firstPart !== 'string') {
throw new Error(
'Tool discovery command must start with a program name.',
);
}
let finalCommand: string = firstPart;
let finalArgs: string[] = cmdParts
.slice(1)
.filter((p): p is string => typeof p === 'string');
let finalEnv = process.env;
const sandboxManager = this.config.sandboxManager;
if (sandboxManager) {
const prepared = await sandboxManager.prepareCommand({
command: finalCommand,
args: finalArgs,
cwd: process.cwd(),
env: process.env,
});
finalCommand = prepared.program;
finalArgs = prepared.args;
finalEnv = prepared.env;
}
const proc = spawn(finalCommand, finalArgs, {
env: finalEnv,
});
let stdout = '';
const stdoutDecoder = new StringDecoder('utf8');
let stderr = '';