2025-04-24 18:03:33 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-27 13:17:40 -08:00
|
|
|
import fsPromises from 'node:fs/promises';
|
2025-08-25 22:11:27 +02:00
|
|
|
import path from 'node:path';
|
|
|
|
|
import os, { EOL } from 'node:os';
|
|
|
|
|
import crypto from 'node:crypto';
|
2025-08-26 00:04:53 +02:00
|
|
|
import type { Config } from '../config/config.js';
|
2026-01-04 00:19:00 -05:00
|
|
|
import { debugLogger } from '../index.js';
|
2025-08-26 15:26:16 -04:00
|
|
|
import { ToolErrorType } from './tool-error.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
import type {
|
2025-08-13 12:27:09 -07:00
|
|
|
ToolInvocation,
|
2025-04-25 14:05:58 -07:00
|
|
|
ToolResult,
|
|
|
|
|
ToolCallConfirmationDetails,
|
|
|
|
|
ToolExecuteConfirmationDetails,
|
2025-08-26 00:04:53 +02:00
|
|
|
} from './tools.js';
|
|
|
|
|
import {
|
|
|
|
|
BaseDeclarativeTool,
|
|
|
|
|
BaseToolInvocation,
|
2025-04-25 14:05:58 -07:00
|
|
|
ToolConfirmationOutcome,
|
2025-08-13 12:58:26 -03:00
|
|
|
Kind,
|
2025-12-12 13:45:39 -08:00
|
|
|
type PolicyUpdateOptions,
|
2025-04-25 14:05:58 -07:00
|
|
|
} from './tools.js';
|
2025-11-03 15:41:00 -08:00
|
|
|
|
fix(shell): Improve error reporting for shell command failures
This commit enhances the tool to provide more informative feedback to the user when a shell command fails, especially in non-debug mode.
Previously, if a command terminated due to a signal (e.g., SIGPIPE during a with no upstream) or failed without producing stdout/stderr, the user would see no output, making it difficult to diagnose the issue.
Changes:
- Modified to update the logic.
- If a command produces no direct output but results in an error, signal, non-zero exit code, or user cancellation, a concise message indicating this outcome is now shown in .
- Utilized the existing utility from for consistent error message formatting, which also resolved previous TypeScript type inference issues.
This ensures users receive clearer feedback on command execution status, improving the tool's usability and aiding in troubleshooting.
Fixes https://b.corp.google.com/issues/417998119
2025-05-18 00:23:57 -07:00
|
|
|
import { getErrorMessage } from '../utils/errors.js';
|
2025-07-25 21:56:49 -04:00
|
|
|
import { summarizeToolOutput } from '../utils/summarizer.js';
|
2025-09-11 13:27:27 -07:00
|
|
|
import type {
|
|
|
|
|
ShellExecutionConfig,
|
|
|
|
|
ShellOutputEvent,
|
|
|
|
|
} from '../services/shellExecutionService.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
import { ShellExecutionService } from '../services/shellExecutionService.js';
|
2026-01-27 17:21:53 +01:00
|
|
|
import { formatBytes } from '../utils/formatters.js';
|
2025-09-11 13:27:27 -07:00
|
|
|
import type { AnsiOutput } from '../utils/terminalSerializer.js';
|
2025-07-25 12:25:32 -07:00
|
|
|
import {
|
|
|
|
|
getCommandRoots,
|
2025-10-16 17:25:30 -07:00
|
|
|
initializeShellParsers,
|
2025-07-25 12:25:32 -07:00
|
|
|
stripShellWrapper,
|
2026-01-19 20:07:28 -08:00
|
|
|
parseCommandDetails,
|
|
|
|
|
hasRedirection,
|
2025-07-25 12:25:32 -07:00
|
|
|
} from '../utils/shell-utils.js';
|
2025-10-17 21:07:26 -04:00
|
|
|
import { SHELL_TOOL_NAME } from './tool-names.js';
|
2025-10-24 13:04:40 -07:00
|
|
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
2026-02-09 15:46:23 -05:00
|
|
|
import { getShellDefinition } from './definitions/coreTools.js';
|
|
|
|
|
import { resolveToolDeclaration } from './definitions/resolver.js';
|
2025-06-02 14:50:12 -07:00
|
|
|
|
2025-07-25 21:56:49 -04:00
|
|
|
export const OUTPUT_UPDATE_INTERVAL_MS = 1000;
|
|
|
|
|
|
2026-01-30 09:53:09 -08:00
|
|
|
// Delay so user does not see the output of the process before the process is moved to the background.
|
|
|
|
|
const BACKGROUND_DELAY_MS = 200;
|
|
|
|
|
|
2025-04-24 18:03:33 -07:00
|
|
|
export interface ShellToolParams {
|
|
|
|
|
command: string;
|
|
|
|
|
description?: string;
|
2025-11-06 15:03:52 -08:00
|
|
|
dir_path?: string;
|
2026-01-30 09:53:09 -08:00
|
|
|
is_background?: boolean;
|
2025-04-24 18:03:33 -07:00
|
|
|
}
|
2025-05-30 01:58:09 -07:00
|
|
|
|
2025-09-11 13:27:27 -07:00
|
|
|
export class ShellToolInvocation extends BaseToolInvocation<
|
2025-08-13 12:27:09 -07:00
|
|
|
ShellToolParams,
|
|
|
|
|
ToolResult
|
|
|
|
|
> {
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly config: Config,
|
|
|
|
|
params: ShellToolParams,
|
2026-01-04 17:11:43 -05:00
|
|
|
messageBus: MessageBus,
|
2025-10-28 09:20:57 -07:00
|
|
|
_toolName?: string,
|
|
|
|
|
_toolDisplayName?: string,
|
2025-08-13 12:27:09 -07:00
|
|
|
) {
|
2025-10-28 09:20:57 -07:00
|
|
|
super(params, messageBus, _toolName, _toolDisplayName);
|
2025-04-25 14:05:58 -07:00
|
|
|
}
|
|
|
|
|
|
2025-08-13 12:27:09 -07:00
|
|
|
getDescription(): string {
|
|
|
|
|
let description = `${this.params.command}`;
|
2025-04-30 12:27:56 -07:00
|
|
|
// append optional [in directory]
|
2025-04-29 15:31:46 -07:00
|
|
|
// note description is needed even if validation fails due to absolute path
|
2025-11-06 15:03:52 -08:00
|
|
|
if (this.params.dir_path) {
|
|
|
|
|
description += ` [in ${this.params.dir_path}]`;
|
2025-11-06 08:51:07 -08:00
|
|
|
} else {
|
|
|
|
|
description += ` [current working directory ${process.cwd()}]`;
|
2025-04-28 08:17:52 -07:00
|
|
|
}
|
|
|
|
|
// append optional (description), replacing any line breaks with spaces
|
2025-08-13 12:27:09 -07:00
|
|
|
if (this.params.description) {
|
|
|
|
|
description += ` (${this.params.description.replace(/\n/g, ' ')})`;
|
2025-04-27 18:57:10 -07:00
|
|
|
}
|
2026-01-30 09:53:09 -08:00
|
|
|
if (this.params.is_background) {
|
|
|
|
|
description += ' [background]';
|
|
|
|
|
}
|
2025-04-27 18:57:10 -07:00
|
|
|
return description;
|
2025-04-25 14:05:58 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-12 13:45:39 -08:00
|
|
|
protected override getPolicyUpdateOptions(
|
|
|
|
|
outcome: ToolConfirmationOutcome,
|
|
|
|
|
): PolicyUpdateOptions | undefined {
|
2025-12-26 18:48:44 -05:00
|
|
|
if (
|
|
|
|
|
outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave ||
|
|
|
|
|
outcome === ToolConfirmationOutcome.ProceedAlways
|
|
|
|
|
) {
|
|
|
|
|
const command = stripShellWrapper(this.params.command);
|
|
|
|
|
const rootCommands = [...new Set(getCommandRoots(command))];
|
|
|
|
|
if (rootCommands.length > 0) {
|
|
|
|
|
return { commandPrefix: rootCommands };
|
|
|
|
|
}
|
2025-12-12 13:45:39 -08:00
|
|
|
return { commandPrefix: this.params.command };
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 13:04:40 -07:00
|
|
|
protected override async getConfirmationDetails(
|
2025-05-27 23:40:25 -07:00
|
|
|
_abortSignal: AbortSignal,
|
2025-04-25 14:05:58 -07:00
|
|
|
): Promise<ToolCallConfirmationDetails | false> {
|
2025-08-13 12:27:09 -07:00
|
|
|
const command = stripShellWrapper(this.params.command);
|
2026-01-04 00:19:00 -05:00
|
|
|
|
2026-01-19 20:07:28 -08:00
|
|
|
const parsed = parseCommandDetails(command);
|
|
|
|
|
let rootCommandDisplay = '';
|
|
|
|
|
|
|
|
|
|
if (!parsed || parsed.hasError || parsed.details.length === 0) {
|
|
|
|
|
// Fallback if parser fails
|
2026-01-04 00:19:00 -05:00
|
|
|
const fallback = command.trim().split(/\s+/)[0];
|
2026-01-19 20:07:28 -08:00
|
|
|
rootCommandDisplay = fallback || 'shell command';
|
|
|
|
|
if (hasRedirection(command)) {
|
|
|
|
|
rootCommandDisplay += ', redirection';
|
2025-10-06 12:15:21 -07:00
|
|
|
}
|
2026-01-19 20:07:28 -08:00
|
|
|
} else {
|
|
|
|
|
rootCommandDisplay = parsed.details
|
|
|
|
|
.map((detail) => detail.name)
|
|
|
|
|
.join(', ');
|
2025-10-06 12:15:21 -07:00
|
|
|
}
|
|
|
|
|
|
2026-01-19 20:07:28 -08:00
|
|
|
const rootCommands = [...new Set(getCommandRoots(command))];
|
|
|
|
|
|
2026-01-02 11:36:59 -08:00
|
|
|
// Rely entirely on PolicyEngine for interactive confirmation.
|
|
|
|
|
// If we are here, it means PolicyEngine returned ASK_USER (or no message bus),
|
|
|
|
|
// so we must provide confirmation details.
|
2025-04-25 14:05:58 -07:00
|
|
|
const confirmationDetails: ToolExecuteConfirmationDetails = {
|
2025-05-22 06:00:36 +00:00
|
|
|
type: 'exec',
|
2025-04-25 14:05:58 -07:00
|
|
|
title: 'Confirm Shell Command',
|
2025-08-13 12:27:09 -07:00
|
|
|
command: this.params.command,
|
2026-01-19 20:07:28 -08:00
|
|
|
rootCommand: rootCommandDisplay,
|
2026-01-14 13:50:28 -05:00
|
|
|
rootCommands,
|
2025-04-25 14:05:58 -07:00
|
|
|
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
2025-12-12 13:45:39 -08:00
|
|
|
await this.publishPolicyUpdate(outcome);
|
2025-04-25 14:05:58 -07:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
return confirmationDetails;
|
2025-04-24 18:03:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-05-09 23:29:02 -07:00
|
|
|
async execute(
|
2025-07-25 12:25:32 -07:00
|
|
|
signal: AbortSignal,
|
2025-09-11 13:27:27 -07:00
|
|
|
updateOutput?: (output: string | AnsiOutput) => void,
|
|
|
|
|
shellExecutionConfig?: ShellExecutionConfig,
|
|
|
|
|
setPidCallback?: (pid: number) => void,
|
2025-05-09 23:29:02 -07:00
|
|
|
): Promise<ToolResult> {
|
2025-08-13 12:27:09 -07:00
|
|
|
const strippedCommand = stripShellWrapper(this.params.command);
|
2025-04-27 18:57:10 -07:00
|
|
|
|
2025-07-25 12:25:32 -07:00
|
|
|
if (signal.aborted) {
|
2025-06-08 15:42:49 -07:00
|
|
|
return {
|
|
|
|
|
llmContent: 'Command was cancelled by user before it could start.',
|
|
|
|
|
returnDisplay: 'Command cancelled by user.',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 12:19:42 -07:00
|
|
|
const isWindows = os.platform() === 'win32';
|
2025-06-15 02:19:19 -07:00
|
|
|
const tempFileName = `shell_pgrep_${crypto
|
|
|
|
|
.randomBytes(6)
|
|
|
|
|
.toString('hex')}.tmp`;
|
|
|
|
|
const tempFilePath = path.join(os.tmpdir(), tempFileName);
|
2025-05-06 10:44:40 -07:00
|
|
|
|
2025-11-26 13:43:33 -08:00
|
|
|
const timeoutMs = this.config.getShellToolInactivityTimeout();
|
|
|
|
|
const timeoutController = new AbortController();
|
|
|
|
|
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
|
|
|
|
|
|
|
|
// Handle signal combination manually to avoid TS issues or runtime missing features
|
|
|
|
|
const combinedController = new AbortController();
|
|
|
|
|
|
|
|
|
|
const onAbort = () => combinedController.abort();
|
|
|
|
|
|
2025-07-25 21:56:49 -04:00
|
|
|
try {
|
|
|
|
|
// pgrep is not available on Windows, so we can't get background PIDs
|
|
|
|
|
const commandToExecute = isWindows
|
|
|
|
|
? strippedCommand
|
|
|
|
|
: (() => {
|
|
|
|
|
// wrap command to append subprocess pids (via pgrep) to temporary file
|
|
|
|
|
let command = strippedCommand.trim();
|
|
|
|
|
if (!command.endsWith('&')) command += ';';
|
|
|
|
|
return `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`;
|
|
|
|
|
})();
|
2025-04-27 18:57:10 -07:00
|
|
|
|
2025-11-06 15:03:52 -08:00
|
|
|
const cwd = this.params.dir_path
|
|
|
|
|
? path.resolve(this.config.getTargetDir(), this.params.dir_path)
|
|
|
|
|
: this.config.getTargetDir();
|
2025-05-30 01:58:09 -07:00
|
|
|
|
2026-01-27 13:17:40 -08:00
|
|
|
const validationError = this.config.validatePathAccess(cwd);
|
|
|
|
|
if (validationError) {
|
|
|
|
|
return {
|
|
|
|
|
llmContent: validationError,
|
|
|
|
|
returnDisplay: 'Path not in workspace.',
|
|
|
|
|
error: {
|
|
|
|
|
message: validationError,
|
|
|
|
|
type: ToolErrorType.PATH_NOT_IN_WORKSPACE,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-09-11 13:27:27 -07:00
|
|
|
let cumulativeOutput: string | AnsiOutput = '';
|
2025-07-25 21:56:49 -04:00
|
|
|
let lastUpdateTime = Date.now();
|
|
|
|
|
let isBinaryStream = false;
|
2025-04-27 18:57:10 -07:00
|
|
|
|
2025-11-26 13:43:33 -08:00
|
|
|
const resetTimeout = () => {
|
|
|
|
|
if (timeoutMs <= 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
|
|
|
timeoutTimer = setTimeout(() => {
|
|
|
|
|
timeoutController.abort();
|
|
|
|
|
}, timeoutMs);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
|
|
|
timeoutController.signal.addEventListener('abort', onAbort, {
|
|
|
|
|
once: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Start timeout
|
|
|
|
|
resetTimeout();
|
|
|
|
|
|
2025-09-11 13:27:27 -07:00
|
|
|
const { result: resultPromise, pid } =
|
|
|
|
|
await ShellExecutionService.execute(
|
|
|
|
|
commandToExecute,
|
|
|
|
|
cwd,
|
|
|
|
|
(event: ShellOutputEvent) => {
|
2025-11-26 13:43:33 -08:00
|
|
|
resetTimeout(); // Reset timeout on any event
|
2025-09-11 13:27:27 -07:00
|
|
|
if (!updateOutput) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-27 18:57:10 -07:00
|
|
|
|
2025-09-11 13:27:27 -07:00
|
|
|
let shouldUpdate = false;
|
|
|
|
|
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
case 'data':
|
|
|
|
|
if (isBinaryStream) break;
|
|
|
|
|
cumulativeOutput = event.chunk;
|
2025-07-25 21:56:49 -04:00
|
|
|
shouldUpdate = true;
|
2025-09-11 13:27:27 -07:00
|
|
|
break;
|
|
|
|
|
case 'binary_detected':
|
|
|
|
|
isBinaryStream = true;
|
|
|
|
|
cumulativeOutput =
|
|
|
|
|
'[Binary output detected. Halting stream...]';
|
2025-07-25 21:56:49 -04:00
|
|
|
shouldUpdate = true;
|
2025-09-11 13:27:27 -07:00
|
|
|
break;
|
|
|
|
|
case 'binary_progress':
|
|
|
|
|
isBinaryStream = true;
|
2026-01-27 17:21:53 +01:00
|
|
|
cumulativeOutput = `[Receiving binary output... ${formatBytes(
|
2025-09-11 13:27:27 -07:00
|
|
|
event.bytesReceived,
|
|
|
|
|
)} received]`;
|
|
|
|
|
if (Date.now() - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS) {
|
|
|
|
|
shouldUpdate = true;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2026-01-30 09:53:09 -08:00
|
|
|
case 'exit':
|
|
|
|
|
break;
|
2025-09-11 13:27:27 -07:00
|
|
|
default: {
|
|
|
|
|
throw new Error('An unhandled ShellOutputEvent was found.');
|
2025-06-09 12:19:42 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-04-27 18:57:10 -07:00
|
|
|
|
2026-01-30 09:53:09 -08:00
|
|
|
if (shouldUpdate && !this.params.is_background) {
|
2025-09-11 13:27:27 -07:00
|
|
|
updateOutput(cumulativeOutput);
|
|
|
|
|
lastUpdateTime = Date.now();
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-11-26 13:43:33 -08:00
|
|
|
combinedController.signal,
|
2025-10-08 13:28:19 -07:00
|
|
|
this.config.getEnableInteractiveShell(),
|
2025-12-22 19:18:27 -08:00
|
|
|
{
|
|
|
|
|
...shellExecutionConfig,
|
|
|
|
|
pager: 'cat',
|
|
|
|
|
sanitizationConfig:
|
|
|
|
|
shellExecutionConfig?.sanitizationConfig ??
|
|
|
|
|
this.config.sanitizationConfig,
|
|
|
|
|
},
|
2025-09-11 13:27:27 -07:00
|
|
|
);
|
|
|
|
|
|
2026-01-30 09:53:09 -08:00
|
|
|
if (pid) {
|
|
|
|
|
if (setPidCallback) {
|
|
|
|
|
setPidCallback(pid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the model requested to run in the background, do so after a short delay.
|
|
|
|
|
if (this.params.is_background) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
ShellExecutionService.background(pid);
|
|
|
|
|
}, BACKGROUND_DELAY_MS);
|
|
|
|
|
}
|
2025-09-11 13:27:27 -07:00
|
|
|
}
|
2025-05-09 23:29:02 -07:00
|
|
|
|
2025-07-25 21:56:49 -04:00
|
|
|
const result = await resultPromise;
|
|
|
|
|
|
|
|
|
|
const backgroundPIDs: number[] = [];
|
|
|
|
|
if (os.platform() !== 'win32') {
|
2026-01-27 13:17:40 -08:00
|
|
|
let tempFileExists = false;
|
|
|
|
|
try {
|
|
|
|
|
await fsPromises.access(tempFilePath);
|
|
|
|
|
tempFileExists = true;
|
|
|
|
|
} catch {
|
|
|
|
|
tempFileExists = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tempFileExists) {
|
|
|
|
|
const pgrepContent = await fsPromises.readFile(tempFilePath, 'utf8');
|
|
|
|
|
const pgrepLines = pgrepContent.split(EOL).filter(Boolean);
|
2025-07-25 21:56:49 -04:00
|
|
|
for (const line of pgrepLines) {
|
|
|
|
|
if (!/^\d+$/.test(line)) {
|
2025-10-29 13:20:11 -07:00
|
|
|
debugLogger.error(`pgrep: ${line}`);
|
2025-07-25 21:56:49 -04:00
|
|
|
}
|
|
|
|
|
const pid = Number(line);
|
|
|
|
|
if (pid !== result.pid) {
|
|
|
|
|
backgroundPIDs.push(pid);
|
|
|
|
|
}
|
2025-06-09 12:19:42 -07:00
|
|
|
}
|
2025-07-25 21:56:49 -04:00
|
|
|
} else {
|
2026-01-30 09:53:09 -08:00
|
|
|
if (!signal.aborted && !result.backgrounded) {
|
2025-10-29 13:20:11 -07:00
|
|
|
debugLogger.error('missing pgrep output');
|
2025-06-09 12:19:42 -07:00
|
|
|
}
|
2025-05-06 10:44:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 09:53:09 -08:00
|
|
|
let data: Record<string, unknown> | undefined;
|
|
|
|
|
|
2025-07-25 21:56:49 -04:00
|
|
|
let llmContent = '';
|
2025-11-26 13:43:33 -08:00
|
|
|
let timeoutMessage = '';
|
2025-07-25 21:56:49 -04:00
|
|
|
if (result.aborted) {
|
2025-11-26 13:43:33 -08:00
|
|
|
if (timeoutController.signal.aborted) {
|
|
|
|
|
timeoutMessage = `Command was automatically cancelled because it exceeded the timeout of ${(
|
|
|
|
|
timeoutMs / 60000
|
|
|
|
|
).toFixed(1)} minutes without output.`;
|
|
|
|
|
llmContent = timeoutMessage;
|
|
|
|
|
} else {
|
|
|
|
|
llmContent =
|
|
|
|
|
'Command was cancelled by user before it could complete.';
|
|
|
|
|
}
|
2025-07-25 21:56:49 -04:00
|
|
|
if (result.output.trim()) {
|
2025-08-19 16:03:51 -07:00
|
|
|
llmContent += ` Below is the output before it was cancelled:\n${result.output}`;
|
2025-07-25 21:56:49 -04:00
|
|
|
} else {
|
|
|
|
|
llmContent += ' There was no output before it was cancelled.';
|
|
|
|
|
}
|
2026-01-30 09:53:09 -08:00
|
|
|
} else if (this.params.is_background || result.backgrounded) {
|
|
|
|
|
llmContent = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`;
|
|
|
|
|
data = {
|
|
|
|
|
pid: result.pid,
|
|
|
|
|
command: this.params.command,
|
|
|
|
|
initialOutput: result.output,
|
|
|
|
|
};
|
2025-05-27 13:47:40 -07:00
|
|
|
} else {
|
2025-07-25 21:56:49 -04:00
|
|
|
// Create a formatted error string for display, replacing the wrapper command
|
|
|
|
|
// with the user-facing command.
|
2026-01-26 10:12:21 -08:00
|
|
|
|
2026-02-18 11:40:05 -05:00
|
|
|
if (result.exitCode !== null) {
|
|
|
|
|
parts.push(`<exit_code>${result.exitCode}</exit_code>`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 00:01:33 -05:00
|
|
|
const output = result.output || '(empty)';
|
|
|
|
|
const parts = [`<output><![CDATA[${output}]]></output>`];
|
2026-01-26 10:12:21 -08:00
|
|
|
if (result.error) {
|
|
|
|
|
const finalError = result.error.message.replaceAll(
|
|
|
|
|
commandToExecute,
|
|
|
|
|
this.params.command,
|
|
|
|
|
);
|
2026-02-18 00:01:33 -05:00
|
|
|
parts.push(`<error><![CDATA[${finalError}]]></error>`);
|
2026-02-18 00:06:27 -05:00
|
|
|
}
|
2026-01-26 10:12:21 -08:00
|
|
|
|
|
|
|
|
if (result.signal) {
|
2026-02-16 17:15:25 +00:00
|
|
|
parts.push(`<signal>${result.signal}</signal>`);
|
2026-01-26 10:12:21 -08:00
|
|
|
}
|
|
|
|
|
if (backgroundPIDs.length) {
|
2026-02-16 17:15:25 +00:00
|
|
|
parts.push(
|
|
|
|
|
`<background_pids>${backgroundPIDs.join(', ')}</background_pids>`,
|
|
|
|
|
);
|
2026-01-26 10:12:21 -08:00
|
|
|
}
|
|
|
|
|
if (result.pid) {
|
2026-02-16 17:15:25 +00:00
|
|
|
parts.push(`<process_group_pgid>${result.pid}</process_group_pgid>`);
|
2026-01-26 10:12:21 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 17:15:25 +00:00
|
|
|
llmContent = `<subprocess_result>\n${parts
|
|
|
|
|
.map((p) => ` ${p}`)
|
|
|
|
|
.join('\n')}\n</subprocess_result>`;
|
2025-05-27 13:47:40 -07:00
|
|
|
}
|
2025-04-28 15:05:36 -07:00
|
|
|
|
2025-07-25 21:56:49 -04:00
|
|
|
let returnDisplayMessage = '';
|
|
|
|
|
if (this.config.getDebugMode()) {
|
|
|
|
|
returnDisplayMessage = llmContent;
|
fix(shell): Improve error reporting for shell command failures
This commit enhances the tool to provide more informative feedback to the user when a shell command fails, especially in non-debug mode.
Previously, if a command terminated due to a signal (e.g., SIGPIPE during a with no upstream) or failed without producing stdout/stderr, the user would see no output, making it difficult to diagnose the issue.
Changes:
- Modified to update the logic.
- If a command produces no direct output but results in an error, signal, non-zero exit code, or user cancellation, a concise message indicating this outcome is now shown in .
- Utilized the existing utility from for consistent error message formatting, which also resolved previous TypeScript type inference issues.
This ensures users receive clearer feedback on command execution status, improving the tool's usability and aiding in troubleshooting.
Fixes https://b.corp.google.com/issues/417998119
2025-05-18 00:23:57 -07:00
|
|
|
} else {
|
2026-01-30 09:53:09 -08:00
|
|
|
if (this.params.is_background || result.backgrounded) {
|
|
|
|
|
returnDisplayMessage = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`;
|
|
|
|
|
} else if (result.output.trim()) {
|
2025-07-25 21:56:49 -04:00
|
|
|
returnDisplayMessage = result.output;
|
|
|
|
|
} else {
|
|
|
|
|
if (result.aborted) {
|
2025-11-26 13:43:33 -08:00
|
|
|
if (timeoutMessage) {
|
|
|
|
|
returnDisplayMessage = timeoutMessage;
|
|
|
|
|
} else {
|
|
|
|
|
returnDisplayMessage = 'Command cancelled by user.';
|
|
|
|
|
}
|
2025-07-25 21:56:49 -04:00
|
|
|
} else if (result.signal) {
|
|
|
|
|
returnDisplayMessage = `Command terminated by signal: ${result.signal}`;
|
|
|
|
|
} else if (result.error) {
|
|
|
|
|
returnDisplayMessage = `Command failed: ${getErrorMessage(
|
|
|
|
|
result.error,
|
|
|
|
|
)}`;
|
|
|
|
|
} else if (result.exitCode !== null && result.exitCode !== 0) {
|
|
|
|
|
returnDisplayMessage = `Command exited with code: ${result.exitCode}`;
|
|
|
|
|
}
|
|
|
|
|
// If output is empty and command succeeded (code 0, no error/signal/abort),
|
|
|
|
|
// returnDisplayMessage will remain empty, which is fine.
|
fix(shell): Improve error reporting for shell command failures
This commit enhances the tool to provide more informative feedback to the user when a shell command fails, especially in non-debug mode.
Previously, if a command terminated due to a signal (e.g., SIGPIPE during a with no upstream) or failed without producing stdout/stderr, the user would see no output, making it difficult to diagnose the issue.
Changes:
- Modified to update the logic.
- If a command produces no direct output but results in an error, signal, non-zero exit code, or user cancellation, a concise message indicating this outcome is now shown in .
- Utilized the existing utility from for consistent error message formatting, which also resolved previous TypeScript type inference issues.
This ensures users receive clearer feedback on command execution status, improving the tool's usability and aiding in troubleshooting.
Fixes https://b.corp.google.com/issues/417998119
2025-05-18 00:23:57 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-12 21:09:12 -07:00
|
|
|
|
2025-07-25 21:56:49 -04:00
|
|
|
const summarizeConfig = this.config.getSummarizeToolOutputConfig();
|
2025-08-26 15:26:16 -04:00
|
|
|
const executionError = result.error
|
|
|
|
|
? {
|
|
|
|
|
error: {
|
|
|
|
|
message: result.error.message,
|
|
|
|
|
type: ToolErrorType.SHELL_EXECUTE_ERROR,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {};
|
2025-10-17 21:07:26 -04:00
|
|
|
if (summarizeConfig && summarizeConfig[SHELL_TOOL_NAME]) {
|
2025-07-25 21:56:49 -04:00
|
|
|
const summary = await summarizeToolOutput(
|
2025-11-11 08:10:50 -08:00
|
|
|
this.config,
|
|
|
|
|
{ model: 'summarizer-shell' },
|
2025-07-25 21:56:49 -04:00
|
|
|
llmContent,
|
|
|
|
|
this.config.getGeminiClient(),
|
|
|
|
|
signal,
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
llmContent: summary,
|
|
|
|
|
returnDisplay: returnDisplayMessage,
|
2025-08-26 15:26:16 -04:00
|
|
|
...executionError,
|
2025-07-25 21:56:49 -04:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-15 10:22:31 -07:00
|
|
|
return {
|
2025-07-25 21:56:49 -04:00
|
|
|
llmContent,
|
2025-07-15 10:22:31 -07:00
|
|
|
returnDisplay: returnDisplayMessage,
|
2026-01-30 09:53:09 -08:00
|
|
|
data,
|
2025-08-26 15:26:16 -04:00
|
|
|
...executionError,
|
2025-07-15 10:22:31 -07:00
|
|
|
};
|
2025-07-25 21:56:49 -04:00
|
|
|
} finally {
|
2025-11-26 13:43:33 -08:00
|
|
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
|
|
|
signal.removeEventListener('abort', onAbort);
|
|
|
|
|
timeoutController.signal.removeEventListener('abort', onAbort);
|
2026-01-27 13:17:40 -08:00
|
|
|
try {
|
|
|
|
|
await fsPromises.unlink(tempFilePath);
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore errors during unlink
|
2025-07-25 21:56:49 -04:00
|
|
|
}
|
2025-07-15 10:22:31 -07:00
|
|
|
}
|
2025-04-24 18:03:33 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-13 12:27:09 -07:00
|
|
|
|
2025-08-15 12:08:29 -07:00
|
|
|
export class ShellTool extends BaseDeclarativeTool<
|
|
|
|
|
ShellToolParams,
|
|
|
|
|
ToolResult
|
|
|
|
|
> {
|
2025-10-20 22:35:35 -04:00
|
|
|
static readonly Name = SHELL_TOOL_NAME;
|
|
|
|
|
|
2025-10-24 13:04:40 -07:00
|
|
|
constructor(
|
|
|
|
|
private readonly config: Config,
|
2026-01-04 17:11:43 -05:00
|
|
|
messageBus: MessageBus,
|
2025-10-24 13:04:40 -07:00
|
|
|
) {
|
2025-10-16 17:25:30 -07:00
|
|
|
void initializeShellParsers().catch(() => {
|
|
|
|
|
// Errors are surfaced when parsing commands.
|
|
|
|
|
});
|
2026-02-09 15:46:23 -05:00
|
|
|
const definition = getShellDefinition(
|
|
|
|
|
config.getEnableInteractiveShell(),
|
|
|
|
|
config.getEnableShellOutputEfficiency(),
|
|
|
|
|
);
|
2025-08-15 12:08:29 -07:00
|
|
|
super(
|
2025-10-20 22:35:35 -04:00
|
|
|
ShellTool.Name,
|
2025-08-15 12:08:29 -07:00
|
|
|
'Shell',
|
2026-02-09 15:46:23 -05:00
|
|
|
definition.base.description!,
|
2025-08-13 12:27:09 -07:00
|
|
|
Kind.Execute,
|
2026-02-09 15:46:23 -05:00
|
|
|
definition.base.parametersJsonSchema,
|
2026-01-04 17:11:43 -05:00
|
|
|
messageBus,
|
2025-08-13 12:27:09 -07:00
|
|
|
false, // output is not markdown
|
|
|
|
|
true, // output can be updated
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 13:55:06 -07:00
|
|
|
protected override validateToolParamValues(
|
2025-08-13 16:17:38 -04:00
|
|
|
params: ShellToolParams,
|
|
|
|
|
): string | null {
|
2025-10-16 17:25:30 -07:00
|
|
|
if (!params.command.trim()) {
|
|
|
|
|
return 'Command cannot be empty.';
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 15:03:52 -08:00
|
|
|
if (params.dir_path) {
|
|
|
|
|
const resolvedPath = path.resolve(
|
|
|
|
|
this.config.getTargetDir(),
|
|
|
|
|
params.dir_path,
|
2025-08-13 12:27:09 -07:00
|
|
|
);
|
2026-01-27 13:17:40 -08:00
|
|
|
return this.config.validatePathAccess(resolvedPath);
|
2025-08-13 12:27:09 -07:00
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected createInvocation(
|
|
|
|
|
params: ShellToolParams,
|
2026-01-04 17:11:43 -05:00
|
|
|
messageBus: MessageBus,
|
2025-10-28 09:20:57 -07:00
|
|
|
_toolName?: string,
|
|
|
|
|
_toolDisplayName?: string,
|
2025-08-13 12:27:09 -07:00
|
|
|
): ToolInvocation<ShellToolParams, ToolResult> {
|
2025-10-24 13:04:40 -07:00
|
|
|
return new ShellToolInvocation(
|
|
|
|
|
this.config,
|
|
|
|
|
params,
|
2026-01-04 17:11:43 -05:00
|
|
|
messageBus,
|
2025-10-28 09:20:57 -07:00
|
|
|
_toolName,
|
|
|
|
|
_toolDisplayName,
|
2025-10-24 13:04:40 -07:00
|
|
|
);
|
2025-08-13 12:27:09 -07:00
|
|
|
}
|
2026-02-09 15:46:23 -05:00
|
|
|
|
|
|
|
|
override getSchema(modelId?: string) {
|
|
|
|
|
const definition = getShellDefinition(
|
|
|
|
|
this.config.getEnableInteractiveShell(),
|
|
|
|
|
this.config.getEnableShellOutputEfficiency(),
|
|
|
|
|
);
|
|
|
|
|
return resolveToolDeclaration(definition, modelId);
|
|
|
|
|
}
|
2025-08-13 12:27:09 -07:00
|
|
|
}
|