mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
fix(core): ensure robust sandbox cleanup in all process execution paths (#24763)
Co-authored-by: Spencer <spencertang@google.com>
This commit is contained in:
@@ -326,6 +326,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
let finalCommand = checkCommand;
|
||||
let finalArgs = checkArgs;
|
||||
let finalEnv = process.env;
|
||||
let cleanup: (() => void) | undefined;
|
||||
|
||||
if (sandboxManager) {
|
||||
try {
|
||||
@@ -338,6 +339,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
finalCommand = prepared.program;
|
||||
finalArgs = prepared.args;
|
||||
finalEnv = prepared.env;
|
||||
cleanup = prepared.cleanup;
|
||||
} catch (err) {
|
||||
debugLogger.debug(
|
||||
`[GrepTool] Sandbox preparation failed for '${command}':`,
|
||||
@@ -346,21 +348,27 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
}
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
stdio: 'ignore',
|
||||
shell: true,
|
||||
env: finalEnv,
|
||||
try {
|
||||
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) => {
|
||||
debugLogger.debug(
|
||||
`[GrepTool] Failed to start process for '${command}':`,
|
||||
err.message,
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
child.on('close', (code) => resolve(code === 0));
|
||||
child.on('error', (err) => {
|
||||
debugLogger.debug(
|
||||
`[GrepTool] Failed to start process for '${command}':`,
|
||||
err.message,
|
||||
);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
cleanup?.();
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ class DiscoveredToolInvocation extends BaseToolInvocation<
|
||||
let finalCommand = callCommand;
|
||||
let finalArgs = args;
|
||||
let finalEnv = process.env;
|
||||
let cleanupFunc: (() => void) | undefined;
|
||||
|
||||
const sandboxManager = this.config.sandboxManager;
|
||||
if (sandboxManager) {
|
||||
@@ -77,58 +78,63 @@ class DiscoveredToolInvocation extends BaseToolInvocation<
|
||||
finalCommand = prepared.program;
|
||||
finalArgs = prepared.args;
|
||||
finalEnv = prepared.env;
|
||||
cleanupFunc = prepared.cleanup;
|
||||
}
|
||||
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
child.stdin.write(JSON.stringify(this.params));
|
||||
child.stdin.end();
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let error: Error | null = null;
|
||||
let code: number | null = null;
|
||||
let signal: NodeJS.Signals | null = null;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const onStdout = (data: Buffer) => {
|
||||
stdout += data?.toString();
|
||||
};
|
||||
try {
|
||||
const child = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
child.stdin.write(JSON.stringify(this.params));
|
||||
child.stdin.end();
|
||||
|
||||
const onStderr = (data: Buffer) => {
|
||||
stderr += data?.toString();
|
||||
};
|
||||
await new Promise<void>((resolve) => {
|
||||
const onStdout = (data: Buffer) => {
|
||||
stdout += data?.toString();
|
||||
};
|
||||
|
||||
const onError = (err: Error) => {
|
||||
error = err;
|
||||
};
|
||||
const onStderr = (data: Buffer) => {
|
||||
stderr += data?.toString();
|
||||
};
|
||||
|
||||
const onClose = (
|
||||
_code: number | null,
|
||||
_signal: NodeJS.Signals | null,
|
||||
) => {
|
||||
code = _code;
|
||||
signal = _signal;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = (err: Error) => {
|
||||
error = err;
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
child.stdout.removeListener('data', onStdout);
|
||||
child.stderr.removeListener('data', onStderr);
|
||||
child.removeListener('error', onError);
|
||||
child.removeListener('close', onClose);
|
||||
if (child.connected) {
|
||||
child.disconnect();
|
||||
}
|
||||
};
|
||||
const onClose = (
|
||||
_code: number | null,
|
||||
_signal: NodeJS.Signals | null,
|
||||
) => {
|
||||
code = _code;
|
||||
signal = _signal;
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
|
||||
child.stdout.on('data', onStdout);
|
||||
child.stderr.on('data', onStderr);
|
||||
child.on('error', onError);
|
||||
child.on('close', onClose);
|
||||
});
|
||||
const cleanup = () => {
|
||||
child.stdout.removeListener('data', onStdout);
|
||||
child.stderr.removeListener('data', onStderr);
|
||||
child.removeListener('error', onError);
|
||||
child.removeListener('close', onClose);
|
||||
if (child.connected) {
|
||||
child.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout.on('data', onStdout);
|
||||
child.stderr.on('data', onStderr);
|
||||
child.on('error', onError);
|
||||
child.on('close', onClose);
|
||||
});
|
||||
} finally {
|
||||
cleanupFunc?.();
|
||||
}
|
||||
|
||||
// if there is any error, non-zero exit code, signal, or stderr, return error details instead of stdout
|
||||
if (error || code !== 0 || signal || stderr) {
|
||||
@@ -374,6 +380,7 @@ export class ToolRegistry {
|
||||
.slice(1)
|
||||
.filter((p): p is string => typeof p === 'string');
|
||||
let finalEnv = process.env;
|
||||
let cleanupFunc: (() => void) | undefined;
|
||||
|
||||
const sandboxManager = this.config.sandboxManager;
|
||||
if (sandboxManager) {
|
||||
@@ -386,118 +393,127 @@ export class ToolRegistry {
|
||||
finalCommand = prepared.program;
|
||||
finalArgs = prepared.args;
|
||||
finalEnv = prepared.env;
|
||||
cleanupFunc = prepared.cleanup;
|
||||
}
|
||||
|
||||
const proc = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
let stdout = '';
|
||||
const stdoutDecoder = new StringDecoder('utf8');
|
||||
let stderr = '';
|
||||
const stderrDecoder = new StringDecoder('utf8');
|
||||
let sizeLimitExceeded = false;
|
||||
const MAX_STDOUT_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
const MAX_STDERR_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
|
||||
let stdoutByteLength = 0;
|
||||
let stderrByteLength = 0;
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stdoutByteLength + data.length > MAX_STDOUT_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stdoutByteLength += data.length;
|
||||
stdout += stdoutDecoder.write(data);
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stderrByteLength + data.length > MAX_STDERR_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stderrByteLength += data.length;
|
||||
stderr += stderrDecoder.write(data);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
proc.on('error', reject);
|
||||
proc.on('close', (code) => {
|
||||
stdout += stdoutDecoder.end();
|
||||
stderr += stderrDecoder.end();
|
||||
|
||||
if (sizeLimitExceeded) {
|
||||
return reject(
|
||||
new Error(
|
||||
`Tool discovery command output exceeded size limit of ${MAX_STDOUT_SIZE} bytes.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Tool discovery command failed with code ${code}.`,
|
||||
stderr,
|
||||
);
|
||||
return reject(
|
||||
new Error(`Tool discovery command failed with exit code ${code}`),
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
try {
|
||||
const proc = spawn(finalCommand, finalArgs, {
|
||||
env: finalEnv,
|
||||
});
|
||||
});
|
||||
let stdout = '';
|
||||
const stdoutDecoder = new StringDecoder('utf8');
|
||||
let stderr = '';
|
||||
const stderrDecoder = new StringDecoder('utf8');
|
||||
let sizeLimitExceeded = false;
|
||||
const MAX_STDOUT_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
const MAX_STDERR_SIZE = 10 * 1024 * 1024; // 10MB limit
|
||||
|
||||
// execute discovery command and extract function declarations (w/ or w/o "tool" wrappers)
|
||||
const functions: FunctionDeclaration[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const discoveredItems = JSON.parse(stdout.trim());
|
||||
let stdoutByteLength = 0;
|
||||
let stderrByteLength = 0;
|
||||
|
||||
if (!discoveredItems || !Array.isArray(discoveredItems)) {
|
||||
throw new Error(
|
||||
'Tool discovery command did not return a JSON array of tools.',
|
||||
);
|
||||
}
|
||||
proc.stdout.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stdoutByteLength + data.length > MAX_STDOUT_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stdoutByteLength += data.length;
|
||||
stdout += stdoutDecoder.write(data);
|
||||
});
|
||||
|
||||
for (const tool of discoveredItems) {
|
||||
if (tool && typeof tool === 'object') {
|
||||
if (Array.isArray(tool['function_declarations'])) {
|
||||
functions.push(...tool['function_declarations']);
|
||||
} else if (Array.isArray(tool['functionDeclarations'])) {
|
||||
functions.push(...tool['functionDeclarations']);
|
||||
} else if (tool['name']) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
functions.push(tool as FunctionDeclaration);
|
||||
proc.stderr.on('data', (data) => {
|
||||
if (sizeLimitExceeded) return;
|
||||
if (stderrByteLength + data.length > MAX_STDERR_SIZE) {
|
||||
sizeLimitExceeded = true;
|
||||
proc.kill();
|
||||
return;
|
||||
}
|
||||
stderrByteLength += data.length;
|
||||
stderr += stderrDecoder.write(data);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
proc.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
proc.on('close', (code) => {
|
||||
stdout += stdoutDecoder.end();
|
||||
stderr += stderrDecoder.end();
|
||||
|
||||
if (sizeLimitExceeded) {
|
||||
return reject(
|
||||
new Error(
|
||||
`Tool discovery command output exceeded size limit of ${MAX_STDOUT_SIZE} bytes.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Tool discovery command failed with code ${code}.`,
|
||||
stderr,
|
||||
);
|
||||
return reject(
|
||||
new Error(
|
||||
`Tool discovery command failed with exit code ${code}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// execute discovery command and extract function declarations (w/ or w/o "tool" wrappers)
|
||||
const functions: FunctionDeclaration[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const discoveredItems = JSON.parse(stdout.trim());
|
||||
|
||||
if (!discoveredItems || !Array.isArray(discoveredItems)) {
|
||||
throw new Error(
|
||||
'Tool discovery command did not return a JSON array of tools.',
|
||||
);
|
||||
}
|
||||
|
||||
for (const tool of discoveredItems) {
|
||||
if (tool && typeof tool === 'object') {
|
||||
if (Array.isArray(tool['function_declarations'])) {
|
||||
functions.push(...tool['function_declarations']);
|
||||
} else if (Array.isArray(tool['functionDeclarations'])) {
|
||||
functions.push(...tool['functionDeclarations']);
|
||||
} else if (tool['name']) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
functions.push(tool as FunctionDeclaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// register each function as a tool
|
||||
for (const func of functions) {
|
||||
if (!func.name) {
|
||||
debugLogger.warn('Discovered a tool with no name. Skipping.');
|
||||
continue;
|
||||
// register each function as a tool
|
||||
for (const func of functions) {
|
||||
if (!func.name) {
|
||||
debugLogger.warn('Discovered a tool with no name. Skipping.');
|
||||
continue;
|
||||
}
|
||||
const parameters =
|
||||
func.parametersJsonSchema &&
|
||||
typeof func.parametersJsonSchema === 'object' &&
|
||||
!Array.isArray(func.parametersJsonSchema)
|
||||
? func.parametersJsonSchema
|
||||
: {};
|
||||
this.registerTool(
|
||||
new DiscoveredTool(
|
||||
this.config,
|
||||
func.name,
|
||||
DISCOVERED_TOOL_PREFIX + func.name,
|
||||
func.description ?? '',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
parameters as Record<string, unknown>,
|
||||
this.messageBus,
|
||||
),
|
||||
);
|
||||
}
|
||||
const parameters =
|
||||
func.parametersJsonSchema &&
|
||||
typeof func.parametersJsonSchema === 'object' &&
|
||||
!Array.isArray(func.parametersJsonSchema)
|
||||
? func.parametersJsonSchema
|
||||
: {};
|
||||
this.registerTool(
|
||||
new DiscoveredTool(
|
||||
this.config,
|
||||
func.name,
|
||||
DISCOVERED_TOOL_PREFIX + func.name,
|
||||
func.description ?? '',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
parameters as Record<string, unknown>,
|
||||
this.messageBus,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
cleanupFunc?.();
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.error(`Tool discovery command "${discoveryCmd}" failed:`, e);
|
||||
|
||||
Reference in New Issue
Block a user