PR cleanup.

This commit is contained in:
Christian Gunderman
2026-03-13 09:42:05 -07:00
parent 6f50890fcd
commit eaefa83036
5 changed files with 179 additions and 156 deletions
+14 -52
View File
@@ -50,8 +50,7 @@ import { ToolExecutor } from '../scheduler/tool-executor.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js'; import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { getPolicyDenialError } from '../scheduler/policy.js'; import { getPolicyDenialError } from '../scheduler/policy.js';
import { GeminiCliOperation } from '../telemetry/constants.js'; import { GeminiCliOperation } from '../telemetry/constants.js';
import { extractMcpContext } from './coreToolHookTriggers.js'; import { evaluateBeforeToolHook } from '../scheduler/hook-utils.js';
import { BeforeToolHookOutput } from '../hooks/types.js';
export type { export type {
ToolCall, ToolCall,
@@ -639,72 +638,38 @@ export class CoreToolScheduler {
} }
// 1. Hook Check (BeforeTool) // 1. Hook Check (BeforeTool)
let hookDecision: 'ask' | 'block' | undefined; const hookResult = await evaluateBeforeToolHook(
let hookSystemMessage: string | undefined; this.config,
toolCall.tool,
const hookSystem = this.config.getHookSystem(); reqInfo,
if (hookSystem) { invocation,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const toolInput = (invocation.params || {}) as Record<
string,
unknown
>;
const mcpContext = extractMcpContext(invocation, this.config);
const beforeOutput = await hookSystem.fireBeforeToolEvent(
toolCall.request.name,
toolInput,
mcpContext,
toolCall.request.originalRequestName,
); );
if (beforeOutput) { if (hookResult.status === 'error') {
// Check if hook requested to stop entire agent execution
if (beforeOutput.shouldStopExecution()) {
const reason = beforeOutput.getEffectiveReason();
this.setStatusInternal( this.setStatusInternal(
reqInfo.callId, reqInfo.callId,
CoreToolCallStatus.Error, CoreToolCallStatus.Error,
signal, signal,
createErrorResponse( createErrorResponse(
reqInfo, reqInfo,
new Error(`Agent execution stopped by hook: ${reason}`), hookResult.error,
ToolErrorType.STOP_EXECUTION, hookResult.errorType,
), ),
); );
await this.checkAndNotifyCompletion(signal); await this.checkAndNotifyCompletion(signal);
return; return;
} }
// Check if hook blocked the tool execution const { hookDecision, hookSystemMessage, modifiedArgs, newInvocation } =
const blockingError = beforeOutput.getBlockingError(); hookResult;
if (blockingError?.blocked) {
this.setStatusInternal(
reqInfo.callId,
CoreToolCallStatus.Error,
signal,
createErrorResponse(
reqInfo,
new Error(`Tool execution blocked: ${blockingError.reason}`),
ToolErrorType.POLICY_VIOLATION,
),
);
await this.checkAndNotifyCompletion(signal);
return;
}
if (beforeOutput.isAskDecision()) { if (hookDecision === 'ask') {
hookDecision = 'ask';
hookSystemMessage = beforeOutput.systemMessage;
// Mark the request so UI knows not to auto-approve it // Mark the request so UI knows not to auto-approve it
toolCall.request.forcedAsk = true; toolCall.request.forcedAsk = true;
} }
// Check if hook requested to update tool input if (modifiedArgs && newInvocation) {
if (beforeOutput instanceof BeforeToolHookOutput) { this.setArgsInternal(reqInfo.callId, modifiedArgs);
const modifiedInput = beforeOutput.getModifiedToolInput();
if (modifiedInput) {
this.setArgsInternal(reqInfo.callId, modifiedInput);
// IMPORTANT: toolCall and invocation must be updated because setArgsInternal created a new one // IMPORTANT: toolCall and invocation must be updated because setArgsInternal created a new one
const updatedCall = this.toolCalls.find( const updatedCall = this.toolCalls.find(
@@ -718,9 +683,6 @@ export class CoreToolScheduler {
} }
} }
} }
}
}
}
// Policy Check using PolicyEngine // Policy Check using PolicyEngine
// We must reconstruct the FunctionCall format expected by PolicyEngine // We must reconstruct the FunctionCall format expected by PolicyEngine
+109
View File
@@ -0,0 +1,109 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '../config/config.js';
import type { AnyDeclarativeTool, AnyToolInvocation } from '../tools/tools.js';
import type { ToolCallRequestInfo } from './types.js';
import { extractMcpContext } from '../core/coreToolHookTriggers.js';
import { BeforeToolHookOutput } from '../hooks/types.js';
import { ToolErrorType } from '../tools/tool-error.js';
export type HookEvaluationResult =
| {
status: 'continue';
hookDecision?: 'ask' | 'block';
hookSystemMessage?: string;
modifiedArgs?: Record<string, unknown>;
newInvocation?: AnyToolInvocation;
}
| {
status: 'error';
error: Error;
errorType: ToolErrorType;
};
export async function evaluateBeforeToolHook(
config: Config,
tool: AnyDeclarativeTool,
request: ToolCallRequestInfo,
invocation: AnyToolInvocation,
): Promise<HookEvaluationResult> {
const hookSystem = config.getHookSystem();
if (!hookSystem) {
return { status: 'continue' };
}
const params = invocation.params || {};
const toolInput: Record<string, unknown> = { ...params };
const mcpContext = extractMcpContext(invocation, config);
const beforeOutput = await hookSystem.fireBeforeToolEvent(
request.name,
toolInput,
mcpContext,
request.originalRequestName,
);
if (!beforeOutput) {
return { status: 'continue' };
}
if (beforeOutput.shouldStopExecution()) {
return {
status: 'error',
error: new Error(
`Agent execution stopped by hook: ${beforeOutput.getEffectiveReason()}`,
),
errorType: ToolErrorType.STOP_EXECUTION,
};
}
const blockingError = beforeOutput.getBlockingError();
if (blockingError?.blocked) {
return {
status: 'error',
error: new Error(`Tool execution blocked: ${blockingError.reason}`),
errorType: ToolErrorType.POLICY_VIOLATION,
};
}
let hookDecision: 'ask' | 'block' | undefined;
let hookSystemMessage: string | undefined;
if (beforeOutput.isAskDecision()) {
hookDecision = 'ask';
hookSystemMessage = beforeOutput.systemMessage;
}
let modifiedArgs: Record<string, unknown> | undefined;
let newInvocation: AnyToolInvocation | undefined;
if (beforeOutput instanceof BeforeToolHookOutput) {
const modifiedInput = beforeOutput.getModifiedToolInput();
if (modifiedInput) {
modifiedArgs = modifiedInput;
try {
newInvocation = tool.build(modifiedInput);
} catch (error) {
return {
status: 'error',
error: new Error(
`Tool parameter modification by hook failed validation: ${error instanceof Error ? error.message : String(error)}`,
),
errorType: ToolErrorType.INVALID_TOOL_PARAMS,
};
}
}
}
return {
status: 'continue',
hookDecision,
hookSystemMessage,
modifiedArgs,
newInvocation,
};
}
+16 -64
View File
@@ -25,8 +25,8 @@ import {
type ScheduledToolCall, type ScheduledToolCall,
} from './types.js'; } from './types.js';
import { ToolErrorType } from '../tools/tool-error.js'; import { ToolErrorType } from '../tools/tool-error.js';
import { extractMcpContext } from '../core/coreToolHookTriggers.js'; import { evaluateBeforeToolHook } from './hook-utils.js';
import { BeforeToolHookOutput } from '../hooks/types.js';
import { PolicyDecision, type ApprovalMode } from '../policy/types.js'; import { PolicyDecision, type ApprovalMode } from '../policy/types.js';
import { import {
ToolConfirmationOutcome, ToolConfirmationOutcome,
@@ -564,85 +564,37 @@ export class Scheduler {
): Promise<void> { ): Promise<void> {
const callId = toolCall.request.callId; const callId = toolCall.request.callId;
let hookDecision: 'ask' | 'block' | undefined; const hookResult = await evaluateBeforeToolHook(
let hookSystemMessage: string | undefined; this.config,
toolCall.tool,
const hookSystem = this.config.getHookSystem(); toolCall.request,
if (hookSystem) { toolCall.invocation,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const toolInput = (toolCall.invocation.params || {}) as Record<
string,
unknown
>;
const mcpContext = extractMcpContext(toolCall.invocation, this.config);
const beforeOutput = await hookSystem.fireBeforeToolEvent(
toolCall.request.name,
toolInput,
mcpContext,
toolCall.request.originalRequestName,
); );
if (beforeOutput) { if (hookResult.status === 'error') {
if (beforeOutput.shouldStopExecution()) {
this.state.updateStatus( this.state.updateStatus(
callId, callId,
CoreToolCallStatus.Error, CoreToolCallStatus.Error,
createErrorResponse( createErrorResponse(
toolCall.request, toolCall.request,
new Error( hookResult.error,
`Agent execution stopped by hook: ${beforeOutput.getEffectiveReason()}`, hookResult.errorType,
),
ToolErrorType.STOP_EXECUTION,
), ),
); );
return; return;
} }
const blockingError = beforeOutput.getBlockingError(); const { hookDecision, hookSystemMessage, modifiedArgs, newInvocation } =
if (blockingError?.blocked) { hookResult;
this.state.updateStatus(
callId,
CoreToolCallStatus.Error,
createErrorResponse(
toolCall.request,
new Error(`Tool execution blocked: ${blockingError.reason}`),
ToolErrorType.POLICY_VIOLATION,
),
);
return;
}
if (beforeOutput.isAskDecision()) { if (hookDecision === 'ask') {
hookDecision = 'ask';
hookSystemMessage = beforeOutput.systemMessage;
toolCall.request.forcedAsk = true; toolCall.request.forcedAsk = true;
} }
if (beforeOutput instanceof BeforeToolHookOutput) { if (modifiedArgs && newInvocation) {
const modifiedInput = beforeOutput.getModifiedToolInput(); toolCall.request.args = modifiedArgs;
if (modifiedInput) {
toolCall.request.args = modifiedInput;
toolCall.request.inputModifiedByHook = true; toolCall.request.inputModifiedByHook = true;
try { toolCall.invocation = newInvocation;
toolCall.invocation = toolCall.tool.build(modifiedInput);
} catch (error) {
this.state.updateStatus(
callId,
CoreToolCallStatus.Error,
createErrorResponse(
toolCall.request,
new Error(
`Tool parameter modification by hook failed validation: ${error instanceof Error ? error.message : String(error)}`,
),
ToolErrorType.INVALID_TOOL_PARAMS,
),
);
return;
}
}
}
}
} }
// Policy & Security // Policy & Security
+3 -3
View File
@@ -712,13 +712,13 @@ class EditToolInvocation
* It needs to calculate the diff to show the user. * It needs to calculate the diff to show the user.
*/ */
protected override async getConfirmationDetails( protected override async getConfirmationDetails(
_abortSignal: AbortSignal, abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> { ): Promise<ToolCallConfirmationDetails | false> {
let editData: CalculatedEdit; let editData: CalculatedEdit;
try { try {
editData = await this.calculateEdit(this.params, _abortSignal); editData = await this.calculateEdit(this.params, abortSignal);
} catch (error) { } catch (error) {
if (_abortSignal.aborted) { if (abortSignal.aborted) {
throw error; throw error;
} }
const errorMsg = error instanceof Error ? error.message : String(error); const errorMsg = error instanceof Error ? error.message : String(error);
+2 -2
View File
@@ -181,13 +181,13 @@ class WriteFileToolInvocation extends BaseToolInvocation<
} }
protected override async getConfirmationDetails( protected override async getConfirmationDetails(
_abortSignal: AbortSignal, abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> { ): Promise<ToolCallConfirmationDetails | false> {
const correctedContentResult = await getCorrectedFileContent( const correctedContentResult = await getCorrectedFileContent(
this.config, this.config,
this.resolvedPath, this.resolvedPath,
this.params.content, this.params.content,
_abortSignal, abortSignal,
); );
if (correctedContentResult.error) { if (correctedContentResult.error) {