mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-01 15:34:29 -07:00
feat(hooks): Hook Event Handling (#9097)
This commit is contained in:
@@ -4,10 +4,16 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { PolicyEngine } from '../policy/policy-engine.js';
|
||||
import { PolicyDecision } from '../policy/types.js';
|
||||
import { MessageBusType, type Message } from './types.js';
|
||||
import { PolicyDecision, getHookSource } from '../policy/types.js';
|
||||
import {
|
||||
MessageBusType,
|
||||
type Message,
|
||||
type HookExecutionRequest,
|
||||
type HookPolicyDecision,
|
||||
} from './types.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
|
||||
export class MessageBus extends EventEmitter {
|
||||
@@ -83,6 +89,39 @@ export class MessageBus extends EventEmitter {
|
||||
default:
|
||||
throw new Error(`Unknown policy decision: ${decision}`);
|
||||
}
|
||||
} else if (message.type === MessageBusType.HOOK_EXECUTION_REQUEST) {
|
||||
// Handle hook execution requests through policy evaluation
|
||||
const hookRequest = message as HookExecutionRequest;
|
||||
const decision = await this.policyEngine.checkHook(hookRequest);
|
||||
|
||||
// Map decision to allow/deny for observability (ASK_USER treated as deny for hooks)
|
||||
const effectiveDecision =
|
||||
decision === PolicyDecision.ALLOW ? 'allow' : 'deny';
|
||||
|
||||
// Emit policy decision for observability
|
||||
this.emitMessage({
|
||||
type: MessageBusType.HOOK_POLICY_DECISION,
|
||||
eventName: hookRequest.eventName,
|
||||
hookSource: getHookSource(hookRequest.input),
|
||||
decision: effectiveDecision,
|
||||
reason:
|
||||
decision !== PolicyDecision.ALLOW
|
||||
? 'Hook execution denied by policy'
|
||||
: undefined,
|
||||
} as HookPolicyDecision);
|
||||
|
||||
// If allowed, emit the request for hook system to handle
|
||||
if (decision === PolicyDecision.ALLOW) {
|
||||
this.emitMessage(message);
|
||||
} else {
|
||||
// If denied or ASK_USER, emit error response (hooks don't support interactive confirmation)
|
||||
this.emitMessage({
|
||||
type: MessageBusType.HOOK_EXECUTION_RESPONSE,
|
||||
correlationId: hookRequest.correlationId,
|
||||
success: false,
|
||||
error: new Error('Hook execution denied by policy'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// For all other message types, just emit them
|
||||
this.emitMessage(message);
|
||||
@@ -105,4 +144,46 @@ export class MessageBus extends EventEmitter {
|
||||
): void {
|
||||
this.off(type, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request-response pattern: Publish a message and wait for a correlated response
|
||||
* This enables synchronous-style communication over the async MessageBus
|
||||
* The correlation ID is generated internally and added to the request
|
||||
*/
|
||||
async request<TRequest extends Message, TResponse extends Message>(
|
||||
request: Omit<TRequest, 'correlationId'>,
|
||||
responseType: TResponse['type'],
|
||||
timeoutMs: number = 60000,
|
||||
): Promise<TResponse> {
|
||||
const correlationId = randomUUID();
|
||||
|
||||
return new Promise<TResponse>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error(`Request timed out waiting for ${responseType}`));
|
||||
}, timeoutMs);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeoutId);
|
||||
this.unsubscribe(responseType, responseHandler);
|
||||
};
|
||||
|
||||
const responseHandler = (response: TResponse) => {
|
||||
// Check if this response matches our request
|
||||
if (
|
||||
'correlationId' in response &&
|
||||
response.correlationId === correlationId
|
||||
) {
|
||||
cleanup();
|
||||
resolve(response);
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe to responses
|
||||
this.subscribe<TResponse>(responseType, responseHandler);
|
||||
|
||||
// Publish the request with correlation ID
|
||||
this.publish({ ...request, correlationId } as TRequest);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ export enum MessageBusType {
|
||||
TOOL_EXECUTION_SUCCESS = 'tool-execution-success',
|
||||
TOOL_EXECUTION_FAILURE = 'tool-execution-failure',
|
||||
UPDATE_POLICY = 'update-policy',
|
||||
HOOK_EXECUTION_REQUEST = 'hook-execution-request',
|
||||
HOOK_EXECUTION_RESPONSE = 'hook-execution-response',
|
||||
HOOK_POLICY_DECISION = 'hook-policy-decision',
|
||||
}
|
||||
|
||||
export interface ToolConfirmationRequest {
|
||||
@@ -55,10 +58,36 @@ export interface ToolExecutionFailure<E = Error> {
|
||||
error: E;
|
||||
}
|
||||
|
||||
export interface HookExecutionRequest {
|
||||
type: MessageBusType.HOOK_EXECUTION_REQUEST;
|
||||
eventName: string;
|
||||
input: Record<string, unknown>;
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
export interface HookExecutionResponse {
|
||||
type: MessageBusType.HOOK_EXECUTION_RESPONSE;
|
||||
correlationId: string;
|
||||
success: boolean;
|
||||
output?: Record<string, unknown>;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface HookPolicyDecision {
|
||||
type: MessageBusType.HOOK_POLICY_DECISION;
|
||||
eventName: string;
|
||||
hookSource: 'project' | 'user' | 'system' | 'extension';
|
||||
decision: 'allow' | 'deny';
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export type Message =
|
||||
| ToolConfirmationRequest
|
||||
| ToolConfirmationResponse
|
||||
| ToolPolicyRejection
|
||||
| ToolExecutionSuccess
|
||||
| ToolExecutionFailure
|
||||
| UpdatePolicy;
|
||||
| UpdatePolicy
|
||||
| HookExecutionRequest
|
||||
| HookExecutionResponse
|
||||
| HookPolicyDecision;
|
||||
|
||||
Reference in New Issue
Block a user