feat(core): improve shell redirection transparency and security (#16486)

This commit is contained in:
N. Taylor Mullen
2026-01-19 20:07:28 -08:00
committed by GitHub
parent 451e0b49cb
commit ec7413456e
16 changed files with 497 additions and 137 deletions
+27 -21
View File
@@ -45,8 +45,10 @@ export interface ToolInvocation<
toolLocations(): ToolLocation[];
/**
* Determines if the tool should prompt for confirmation before execution.
* @returns Confirmation details or false if no confirmation is needed.
* Checks if the tool call should be confirmed by the user before execution.
*
* @param abortSignal An AbortSignal that can be used to cancel the confirmation request.
* @returns A ToolCallConfirmationDetails object if confirmation is required, or false if not.
*/
shouldConfirmExecute(
abortSignal: AbortSignal,
@@ -143,7 +145,7 @@ export abstract class BaseToolInvocation<
) {
if (this._toolName) {
const options = this.getPolicyUpdateOptions(outcome);
await this.messageBus.publish({
void this.messageBus.publish({
type: MessageBusType.UPDATE_POLICY,
toolName: this._toolName,
persist: outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave,
@@ -179,16 +181,21 @@ export abstract class BaseToolInvocation<
protected getMessageBusDecision(
abortSignal: AbortSignal,
): Promise<'ALLOW' | 'DENY' | 'ASK_USER'> {
if (!this.messageBus) {
if (!this.messageBus || !this._toolName) {
// If there's no message bus, we can't make a decision, so we allow.
// The legacy confirmation flow will still apply if the tool needs it.
return Promise.resolve('ALLOW');
}
const correlationId = randomUUID();
const toolCall = {
name: this._toolName || this.constructor.name,
args: this.params as Record<string, unknown>,
const request: ToolConfirmationRequest = {
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
correlationId,
toolCall: {
name: this._toolName,
args: this.params as Record<string, unknown>,
},
serverName: this._serverName,
};
return new Promise<'ALLOW' | 'DENY' | 'ASK_USER'>((resolve) => {
@@ -197,18 +204,19 @@ export abstract class BaseToolInvocation<
return;
}
let timeoutId: NodeJS.Timeout | undefined;
let timeoutId: NodeJS.Timeout | null = null;
let unsubscribe: (() => void) | null = null;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
timeoutId = null;
}
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
abortSignal.removeEventListener('abort', abortHandler);
this.messageBus.unsubscribe(
MessageBusType.TOOL_CONFIRMATION_RESPONSE,
responseHandler,
);
};
const abortHandler = () => {
@@ -245,17 +253,15 @@ export abstract class BaseToolInvocation<
MessageBusType.TOOL_CONFIRMATION_RESPONSE,
responseHandler,
);
const request: ToolConfirmationRequest = {
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall,
correlationId,
serverName: this._serverName,
unsubscribe = () => {
this.messageBus?.unsubscribe(
MessageBusType.TOOL_CONFIRMATION_RESPONSE,
responseHandler,
);
};
try {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.messageBus.publish(request);
void this.messageBus.publish(request);
} catch (_error) {
cleanup();
resolve('ALLOW');