mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-14 20:10:36 -07:00
migrate fireToolNotificationHook to hookSystem (#17398)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
@@ -4,23 +4,9 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
import { type McpToolContext, BeforeToolHookOutput } from '../hooks/types.js';
|
||||||
import {
|
|
||||||
MessageBusType,
|
|
||||||
type HookExecutionRequest,
|
|
||||||
type HookExecutionResponse,
|
|
||||||
} from '../confirmation-bus/types.js';
|
|
||||||
import {
|
|
||||||
NotificationType,
|
|
||||||
type McpToolContext,
|
|
||||||
BeforeToolHookOutput,
|
|
||||||
} from '../hooks/types.js';
|
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import type {
|
import type { ToolResult, AnyDeclarativeTool } from '../tools/tools.js';
|
||||||
ToolCallConfirmationDetails,
|
|
||||||
ToolResult,
|
|
||||||
AnyDeclarativeTool,
|
|
||||||
} from '../tools/tools.js';
|
|
||||||
import { ToolErrorType } from '../tools/tool-error.js';
|
import { ToolErrorType } from '../tools/tool-error.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
import type { AnsiOutput, ShellExecutionConfig } from '../index.js';
|
import type { AnsiOutput, ShellExecutionConfig } from '../index.js';
|
||||||
@@ -28,133 +14,6 @@ import type { AnyToolInvocation } from '../tools/tools.js';
|
|||||||
import { ShellToolInvocation } from '../tools/shell.js';
|
import { ShellToolInvocation } from '../tools/shell.js';
|
||||||
import { DiscoveredMCPToolInvocation } from '../tools/mcp-tool.js';
|
import { DiscoveredMCPToolInvocation } from '../tools/mcp-tool.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializable representation of tool confirmation details for hooks.
|
|
||||||
* Excludes function properties like onConfirm that can't be serialized.
|
|
||||||
*/
|
|
||||||
interface SerializableConfirmationDetails {
|
|
||||||
type: 'edit' | 'exec' | 'mcp' | 'info';
|
|
||||||
title: string;
|
|
||||||
// Edit-specific fields
|
|
||||||
fileName?: string;
|
|
||||||
filePath?: string;
|
|
||||||
fileDiff?: string;
|
|
||||||
originalContent?: string | null;
|
|
||||||
newContent?: string;
|
|
||||||
isModifying?: boolean;
|
|
||||||
// Exec-specific fields
|
|
||||||
command?: string;
|
|
||||||
rootCommand?: string;
|
|
||||||
// MCP-specific fields
|
|
||||||
serverName?: string;
|
|
||||||
toolName?: string;
|
|
||||||
toolDisplayName?: string;
|
|
||||||
// Info-specific fields
|
|
||||||
prompt?: string;
|
|
||||||
urls?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts ToolCallConfirmationDetails to a serializable format for hooks.
|
|
||||||
* Excludes function properties (onConfirm, ideConfirmation) that can't be serialized.
|
|
||||||
*/
|
|
||||||
function toSerializableDetails(
|
|
||||||
details: ToolCallConfirmationDetails,
|
|
||||||
): SerializableConfirmationDetails {
|
|
||||||
const base: SerializableConfirmationDetails = {
|
|
||||||
type: details.type,
|
|
||||||
title: details.title,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (details.type) {
|
|
||||||
case 'edit':
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
fileName: details.fileName,
|
|
||||||
filePath: details.filePath,
|
|
||||||
fileDiff: details.fileDiff,
|
|
||||||
originalContent: details.originalContent,
|
|
||||||
newContent: details.newContent,
|
|
||||||
isModifying: details.isModifying,
|
|
||||||
};
|
|
||||||
case 'exec':
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
command: details.command,
|
|
||||||
rootCommand: details.rootCommand,
|
|
||||||
};
|
|
||||||
case 'mcp':
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
serverName: details.serverName,
|
|
||||||
toolName: details.toolName,
|
|
||||||
toolDisplayName: details.toolDisplayName,
|
|
||||||
};
|
|
||||||
case 'info':
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
prompt: details.prompt,
|
|
||||||
urls: details.urls,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the message to display in the notification hook for tool confirmation.
|
|
||||||
*/
|
|
||||||
function getNotificationMessage(
|
|
||||||
confirmationDetails: ToolCallConfirmationDetails,
|
|
||||||
): string {
|
|
||||||
switch (confirmationDetails.type) {
|
|
||||||
case 'edit':
|
|
||||||
return `Tool ${confirmationDetails.title} requires editing`;
|
|
||||||
case 'exec':
|
|
||||||
return `Tool ${confirmationDetails.title} requires execution`;
|
|
||||||
case 'mcp':
|
|
||||||
return `Tool ${confirmationDetails.title} requires MCP`;
|
|
||||||
case 'info':
|
|
||||||
return `Tool ${confirmationDetails.title} requires information`;
|
|
||||||
default:
|
|
||||||
return `Tool requires confirmation`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires the ToolPermission notification hook for a tool that needs confirmation.
|
|
||||||
*
|
|
||||||
* @param messageBus The message bus to use for hook communication
|
|
||||||
* @param confirmationDetails The tool confirmation details
|
|
||||||
*/
|
|
||||||
export async function fireToolNotificationHook(
|
|
||||||
messageBus: MessageBus,
|
|
||||||
confirmationDetails: ToolCallConfirmationDetails,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const message = getNotificationMessage(confirmationDetails);
|
|
||||||
const serializedDetails = toSerializableDetails(confirmationDetails);
|
|
||||||
|
|
||||||
await messageBus.request<HookExecutionRequest, HookExecutionResponse>(
|
|
||||||
{
|
|
||||||
type: MessageBusType.HOOK_EXECUTION_REQUEST,
|
|
||||||
eventName: 'Notification',
|
|
||||||
input: {
|
|
||||||
notification_type: NotificationType.ToolPermission,
|
|
||||||
message,
|
|
||||||
details: serializedDetails,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MessageBusType.HOOK_EXECUTION_RESPONSE,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
debugLogger.debug(
|
|
||||||
`Notification hook failed for ${confirmationDetails.title}:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts MCP context from a tool invocation if it's an MCP tool.
|
* Extracts MCP context from a tool invocation if it's an MCP tool.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { getToolSuggestion } from '../utils/tool-utils.js';
|
|||||||
import type { ToolConfirmationRequest } from '../confirmation-bus/types.js';
|
import type { ToolConfirmationRequest } from '../confirmation-bus/types.js';
|
||||||
import { MessageBusType } from '../confirmation-bus/types.js';
|
import { MessageBusType } from '../confirmation-bus/types.js';
|
||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
import { fireToolNotificationHook } from './coreToolHookTriggers.js';
|
|
||||||
import {
|
import {
|
||||||
type ToolCall,
|
type ToolCall,
|
||||||
type ValidatingToolCall,
|
type ValidatingToolCall,
|
||||||
@@ -651,10 +650,9 @@ export class CoreToolScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fire Notification hook before showing confirmation to user
|
// Fire Notification hook before showing confirmation to user
|
||||||
const messageBus = this.config.getMessageBus();
|
const hookSystem = this.config.getHookSystem();
|
||||||
const hooksEnabled = this.config.getEnableHooks();
|
if (hookSystem) {
|
||||||
if (hooksEnabled && messageBus) {
|
await hookSystem.fireToolNotificationEvent(confirmationDetails);
|
||||||
await fireToolNotificationHook(messageBus, confirmationDetails);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow IDE to resolve confirmation
|
// Allow IDE to resolve confirmation
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
BeforeToolSelectionHookOutput,
|
BeforeToolSelectionHookOutput,
|
||||||
McpToolContext,
|
McpToolContext,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
import { NotificationType } from './types.js';
|
||||||
import type { AggregatedHookResult } from './hookAggregator.js';
|
import type { AggregatedHookResult } from './hookAggregator.js';
|
||||||
import type {
|
import type {
|
||||||
GenerateContentParameters,
|
GenerateContentParameters,
|
||||||
@@ -33,6 +34,7 @@ import type {
|
|||||||
ToolConfig,
|
ToolConfig,
|
||||||
ToolListUnion,
|
ToolListUnion,
|
||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
|
import type { ToolCallConfirmationDetails } from '../tools/tools.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main hook system that coordinates all hook-related functionality
|
* Main hook system that coordinates all hook-related functionality
|
||||||
@@ -78,6 +80,73 @@ export interface AfterModelHookResult {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts ToolCallConfirmationDetails to a serializable format for hooks.
|
||||||
|
* Excludes function properties (onConfirm, ideConfirmation) that can't be serialized.
|
||||||
|
*/
|
||||||
|
function toSerializableDetails(
|
||||||
|
details: ToolCallConfirmationDetails,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const base: Record<string, unknown> = {
|
||||||
|
type: details.type,
|
||||||
|
title: details.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (details.type) {
|
||||||
|
case 'edit':
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
fileName: details.fileName,
|
||||||
|
filePath: details.filePath,
|
||||||
|
fileDiff: details.fileDiff,
|
||||||
|
originalContent: details.originalContent,
|
||||||
|
newContent: details.newContent,
|
||||||
|
isModifying: details.isModifying,
|
||||||
|
};
|
||||||
|
case 'exec':
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
command: details.command,
|
||||||
|
rootCommand: details.rootCommand,
|
||||||
|
};
|
||||||
|
case 'mcp':
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
serverName: details.serverName,
|
||||||
|
toolName: details.toolName,
|
||||||
|
toolDisplayName: details.toolDisplayName,
|
||||||
|
};
|
||||||
|
case 'info':
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
prompt: details.prompt,
|
||||||
|
urls: details.urls,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the message to display in the notification hook for tool confirmation.
|
||||||
|
*/
|
||||||
|
function getNotificationMessage(
|
||||||
|
confirmationDetails: ToolCallConfirmationDetails,
|
||||||
|
): string {
|
||||||
|
switch (confirmationDetails.type) {
|
||||||
|
case 'edit':
|
||||||
|
return `Tool ${confirmationDetails.title} requires editing`;
|
||||||
|
case 'exec':
|
||||||
|
return `Tool ${confirmationDetails.title} requires execution`;
|
||||||
|
case 'mcp':
|
||||||
|
return `Tool ${confirmationDetails.title} requires MCP`;
|
||||||
|
case 'info':
|
||||||
|
return `Tool ${confirmationDetails.title} requires information`;
|
||||||
|
default:
|
||||||
|
return `Tool requires confirmation`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class HookSystem {
|
export class HookSystem {
|
||||||
private readonly hookRegistry: HookRegistry;
|
private readonly hookRegistry: HookRegistry;
|
||||||
private readonly hookRunner: HookRunner;
|
private readonly hookRunner: HookRunner;
|
||||||
@@ -312,7 +381,7 @@ export class HookSystem {
|
|||||||
);
|
);
|
||||||
return result.finalOutput;
|
return result.finalOutput;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLogger.debug(`BeforeTool hook failed for ${toolName}:`, error);
|
debugLogger.debug(`BeforeToolEvent failed for ${toolName}:`, error);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,8 +405,28 @@ export class HookSystem {
|
|||||||
);
|
);
|
||||||
return result.finalOutput;
|
return result.finalOutput;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLogger.debug(`AfterTool hook failed for ${toolName}:`, error);
|
debugLogger.debug(`AfterToolEvent failed for ${toolName}:`, error);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fireToolNotificationEvent(
|
||||||
|
confirmationDetails: ToolCallConfirmationDetails,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const message = getNotificationMessage(confirmationDetails);
|
||||||
|
const serializedDetails = toSerializableDetails(confirmationDetails);
|
||||||
|
|
||||||
|
await this.hookEventHandler.fireNotificationEvent(
|
||||||
|
NotificationType.ToolPermission,
|
||||||
|
message,
|
||||||
|
serializedDetails,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
debugLogger.debug(
|
||||||
|
`NotificationEvent failed for ${confirmationDetails.title}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,12 @@ import type { ValidatingToolCall, WaitingToolCall } from './types.js';
|
|||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import type { EditorType } from '../utils/editor.js';
|
import type { EditorType } from '../utils/editor.js';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { fireToolNotificationHook } from '../core/coreToolHookTriggers.js';
|
|
||||||
|
|
||||||
// Mock Dependencies
|
// Mock Dependencies
|
||||||
vi.mock('node:crypto', () => ({
|
vi.mock('node:crypto', () => ({
|
||||||
randomUUID: vi.fn(),
|
randomUUID: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../core/coreToolHookTriggers.js', () => ({
|
|
||||||
fireToolNotificationHook: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('confirmation.ts', () => {
|
describe('confirmation.ts', () => {
|
||||||
let mockMessageBus: MessageBus;
|
let mockMessageBus: MessageBus;
|
||||||
|
|
||||||
@@ -140,15 +135,19 @@ describe('confirmation.ts', () => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockHookSystem = {
|
||||||
|
fireToolNotificationEvent: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
mockConfig = {
|
||||||
|
getEnableHooks: vi.fn().mockReturnValue(true),
|
||||||
|
getHookSystem: vi.fn().mockReturnValue(mockHookSystem),
|
||||||
|
} as unknown as Mocked<Config>;
|
||||||
|
|
||||||
mockModifier = {
|
mockModifier = {
|
||||||
handleModifyWithEditor: vi.fn(),
|
handleModifyWithEditor: vi.fn(),
|
||||||
applyInlineModify: vi.fn(),
|
applyInlineModify: vi.fn(),
|
||||||
} as unknown as Mocked<ToolModificationHandler>;
|
} as unknown as Mocked<ToolModificationHandler>;
|
||||||
|
|
||||||
mockConfig = {
|
|
||||||
getEnableHooks: vi.fn().mockReturnValue(true),
|
|
||||||
} as unknown as Mocked<Config>;
|
|
||||||
|
|
||||||
getPreferredEditor = vi.fn().mockReturnValue('vim');
|
getPreferredEditor = vi.fn().mockReturnValue('vim');
|
||||||
|
|
||||||
invocationMock = {
|
invocationMock = {
|
||||||
@@ -263,8 +262,9 @@ describe('confirmation.ts', () => {
|
|||||||
});
|
});
|
||||||
await promise;
|
await promise;
|
||||||
|
|
||||||
expect(fireToolNotificationHook).toHaveBeenCalledWith(
|
expect(
|
||||||
mockMessageBus,
|
mockConfig.getHookSystem()?.fireToolNotificationEvent,
|
||||||
|
).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: details.type,
|
type: details.type,
|
||||||
prompt: details.prompt,
|
prompt: details.prompt,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import type { SchedulerStateManager } from './state-manager.js';
|
|||||||
import type { ToolModificationHandler } from './tool-modifier.js';
|
import type { ToolModificationHandler } from './tool-modifier.js';
|
||||||
import type { EditorType } from '../utils/editor.js';
|
import type { EditorType } from '../utils/editor.js';
|
||||||
import type { DiffUpdateResult } from '../ide/ide-client.js';
|
import type { DiffUpdateResult } from '../ide/ide-client.js';
|
||||||
import { fireToolNotificationHook } from '../core/coreToolHookTriggers.js';
|
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
|
||||||
export interface ConfirmationResult {
|
export interface ConfirmationResult {
|
||||||
@@ -168,8 +167,8 @@ async function notifyHooks(
|
|||||||
deps: { config: Config; messageBus: MessageBus },
|
deps: { config: Config; messageBus: MessageBus },
|
||||||
details: ToolCallConfirmationDetails,
|
details: ToolCallConfirmationDetails,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (deps.config.getEnableHooks()) {
|
if (deps.config.getHookSystem()) {
|
||||||
await fireToolNotificationHook(deps.messageBus, {
|
await deps.config.getHookSystem()?.fireToolNotificationEvent({
|
||||||
...details,
|
...details,
|
||||||
// Pass no-op onConfirm to satisfy type definition; side-effects via
|
// Pass no-op onConfirm to satisfy type definition; side-effects via
|
||||||
// callbacks are disallowed.
|
// callbacks are disallowed.
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ vi.mock('../telemetry/types.js', () => ({
|
|||||||
ToolCallEvent: vi.fn().mockImplementation((call) => ({ ...call })),
|
ToolCallEvent: vi.fn().mockImplementation((call) => ({ ...call })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../core/coreToolHookTriggers.js', () => ({
|
|
||||||
fireToolNotificationHook: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { SchedulerStateManager } from './state-manager.js';
|
import { SchedulerStateManager } from './state-manager.js';
|
||||||
import { resolveConfirmation } from './confirmation.js';
|
import { resolveConfirmation } from './confirmation.js';
|
||||||
import { checkPolicy, updatePolicy } from './policy.js';
|
import { checkPolicy, updatePolicy } from './policy.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user