fix(core): improve shell command with redirection detection (#15683)

This commit is contained in:
Gal Zahavi
2026-01-02 11:36:59 -08:00
committed by GitHub
parent 958284dc24
commit 18fef0db31
8 changed files with 432 additions and 97 deletions
+1 -14
View File
@@ -51,7 +51,6 @@ import * as path from 'node:path';
import * as crypto from 'node:crypto';
import * as summarizer from '../utils/summarizer.js';
import { ToolErrorType } from './tool-error.js';
import { ToolConfirmationOutcome } from './tools.js';
import { OUTPUT_UPDATE_INTERVAL_MS } from './shell.js';
import { SHELL_TOOL_NAME } from './tool-names.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
@@ -472,7 +471,7 @@ describe('ShellTool', () => {
});
describe('shouldConfirmExecute', () => {
it('should request confirmation for a new command and allowlist it on "Always"', async () => {
it('should return confirmation details when PolicyEngine delegates', async () => {
const params = { command: 'npm install' };
const invocation = shellTool.build(params);
const confirmation = await invocation.shouldConfirmExecute(
@@ -481,18 +480,6 @@ describe('ShellTool', () => {
expect(confirmation).not.toBe(false);
expect(confirmation && confirmation.type).toBe('exec');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (confirmation as any).onConfirm(
ToolConfirmationOutcome.ProceedAlways,
);
// Should now be allowlisted
const secondInvocation = shellTool.build({ command: 'npm test' });
const secondConfirmation = await secondInvocation.shouldConfirmExecute(
new AbortController().signal,
);
expect(secondConfirmation).toBe(false);
});
it('should throw an error if validation fails', () => {
+4 -16
View File
@@ -62,7 +62,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
constructor(
private readonly config: Config,
params: ShellToolParams,
private readonly allowlist: Set<string>,
messageBus?: MessageBus,
_toolName?: string,
_toolDisplayName?: string,
@@ -127,23 +126,15 @@ export class ShellToolInvocation extends BaseToolInvocation<
);
}
const commandsToConfirm = rootCommands.filter(
(command) => !this.allowlist.has(command),
);
if (commandsToConfirm.length === 0) {
return false; // already approved and allowlisted
}
// 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.
const confirmationDetails: ToolExecuteConfirmationDetails = {
type: 'exec',
title: 'Confirm Shell Command',
command: this.params.command,
rootCommand: commandsToConfirm.join(', '),
rootCommand: rootCommands.join(', '),
onConfirm: async (outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
commandsToConfirm.forEach((command) => this.allowlist.add(command));
}
await this.publishPolicyUpdate(outcome);
},
};
@@ -451,8 +442,6 @@ export class ShellTool extends BaseDeclarativeTool<
> {
static readonly Name = SHELL_TOOL_NAME;
private allowlist: Set<string> = new Set();
constructor(
private readonly config: Config,
messageBus?: MessageBus,
@@ -533,7 +522,6 @@ export class ShellTool extends BaseDeclarativeTool<
return new ShellToolInvocation(
this.config,
params,
this.allowlist,
messageBus,
_toolName,
_toolDisplayName,