1use std::collections::HashMap;
4use std::panic::AssertUnwindSafe;
5use std::sync::Arc;
6
7use futures_util::FutureExt;
8use tokio::time::timeout;
9
10use crate::error::HookError;
11use crate::event::{HookEvent, HookEventKind};
12use crate::handler::{HookContext, HookHandler, HookResult};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct HookRegistration {
17 pub event: HookEventKind,
19 pub handler: String,
21}
22
23#[derive(Debug)]
25pub enum HookOutcome<'a> {
26 Continue(HookEvent<'a>),
28 Skip(HookEvent<'a>),
30 Deny {
32 event: HookEvent<'a>,
34 reason: String,
36 },
37}
38
39impl<'a> HookOutcome<'a> {
40 #[must_use]
42 pub fn into_event(self) -> HookEvent<'a> {
43 match self {
44 Self::Continue(event) | Self::Skip(event) => event,
45 Self::Deny { event, .. } => event,
46 }
47 }
48}
49
50#[derive(Default)]
52pub struct HookRegistry {
53 handlers: HashMap<HookEventKind, Vec<Arc<dyn HookHandler>>>,
54}
55
56impl HookRegistry {
57 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 pub fn register(&mut self, handler: Arc<dyn HookHandler>) {
65 for kind in handler.subscribed() {
66 self.handlers
67 .entry(*kind)
68 .or_default()
69 .push(handler.clone());
70 }
71 }
72
73 #[must_use]
75 pub fn handlers_for(&self, kind: HookEventKind) -> usize {
76 self.handlers.get(&kind).map_or(0, Vec::len)
77 }
78
79 #[must_use]
81 pub fn registrations(&self) -> Vec<HookRegistration> {
82 let mut registrations = Vec::new();
83 for kind in crate::event::ALL_HOOK_EVENT_KINDS {
84 if let Some(handlers) = self.handlers.get(&kind) {
85 for handler in handlers {
86 registrations.push(HookRegistration {
87 event: kind,
88 handler: handler.name().to_string(),
89 });
90 }
91 }
92 }
93 registrations
94 }
95
96 pub async fn dispatch<'a>(&self, ctx: &HookContext, event: HookEvent<'a>) -> HookOutcome<'a> {
98 self.dispatch_inner(ctx, event, false).await
99 }
100
101 pub async fn dispatch_permission_gate<'a>(
106 &self,
107 ctx: &HookContext,
108 event: HookEvent<'a>,
109 ) -> HookOutcome<'a> {
110 debug_assert!(event.is_permission_boundary());
111 self.dispatch_inner(ctx, event, true).await
112 }
113
114 async fn dispatch_inner<'a>(
115 &self,
116 ctx: &HookContext,
117 mut event: HookEvent<'a>,
118 fail_closed: bool,
119 ) -> HookOutcome<'a> {
120 let kind = event.kind();
121 let Some(handlers) = self.handlers.get(&kind) else {
122 return HookOutcome::Continue(event);
123 };
124
125 for handler in handlers {
126 let handler_name = handler.name().to_owned();
127 let handler_future = AssertUnwindSafe(handler.on_event(ctx, &mut event)).catch_unwind();
128
129 match timeout(handler.timeout(), handler_future).await {
130 Ok(Ok(HookResult::Continue)) => {}
131 Ok(Ok(HookResult::Skip)) if fail_closed => {
132 return HookOutcome::Deny {
133 event,
134 reason: format!("permission hook '{handler_name}' skipped final gate"),
135 };
136 }
137 Ok(Ok(HookResult::Skip)) => return HookOutcome::Skip(event),
138 Ok(Ok(HookResult::Deny { reason })) => {
139 let error = HookError::Denied {
140 handler: handler_name,
141 reason: reason.clone(),
142 };
143 tracing::warn!(error = %error, event = %kind, "hook denied event");
144 return HookOutcome::Deny { event, reason };
145 }
146 Ok(Ok(HookResult::Modify(modified))) => {
147 if event.is_permission_boundary() {
148 tracing::error!(
149 handler = %handler.name(),
150 event = %kind,
151 "hook modify ignored for permission boundary"
152 );
153 } else {
154 event = modified;
155 }
156 }
157 Ok(Err(_)) => {
158 let error = HookError::Panic {
159 handler: handler_name,
160 };
161 tracing::error!(error = %error, event = %kind, "hook panicked; aborting hook chain");
162 if fail_closed {
163 return HookOutcome::Deny {
164 event,
165 reason: "permission hook panicked".to_owned(),
166 };
167 }
168 return HookOutcome::Continue(event);
169 }
170 Err(_) => {
171 let error = HookError::Timeout {
172 handler: handler_name,
173 timeout: handler.timeout(),
174 };
175 tracing::warn!(error = %error, event = %kind, "hook timed out");
176 if fail_closed {
177 return HookOutcome::Deny {
178 event,
179 reason: "permission hook timed out".to_owned(),
180 };
181 }
182 }
183 }
184 }
185
186 HookOutcome::Continue(event)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::event::TurnId;
194 use crate::handler::HookContext;
195 use async_trait::async_trait;
196 use std::time::Duration;
197
198 struct DiagnosticHook;
199
200 #[async_trait]
201 impl HookHandler for DiagnosticHook {
202 fn name(&self) -> &str {
203 "diagnostic"
204 }
205
206 fn subscribed(&self) -> &'static [HookEventKind] {
207 &[HookEventKind::TurnStart, HookEventKind::TurnComplete]
208 }
209
210 fn timeout(&self) -> Duration {
211 Duration::from_millis(10)
212 }
213
214 async fn on_event(&self, _ctx: &HookContext, _event: &mut HookEvent<'_>) -> HookResult {
215 HookResult::Continue
216 }
217 }
218
219 #[test]
220 fn pre_filter_by_kind() {
221 let registry = HookRegistry::new();
222 let ctx = HookContext::new(TurnId::new(), std::path::PathBuf::from("."));
223 let runtime = tokio::runtime::Builder::new_current_thread()
224 .enable_time()
225 .build()
226 .expect("runtime");
227 runtime.block_on(async {
228 let outcome = registry
229 .dispatch(
230 &ctx,
231 HookEvent::TurnStart {
232 turn_id: ctx.turn_id,
233 },
234 )
235 .await;
236 assert!(matches!(
237 outcome,
238 HookOutcome::Continue(HookEvent::TurnStart { .. })
239 ));
240 });
241 }
242
243 #[test]
244 fn registrations_reports_handlers_without_dispatch() {
245 let mut registry = HookRegistry::new();
246 registry.register(Arc::new(DiagnosticHook));
247
248 let registrations = registry.registrations();
249
250 assert_eq!(registrations.len(), 2);
251 assert_eq!(
252 registrations[0],
253 HookRegistration {
254 event: HookEventKind::TurnStart,
255 handler: "diagnostic".to_string(),
256 }
257 );
258 assert_eq!(
259 registrations[1],
260 HookRegistration {
261 event: HookEventKind::TurnComplete,
262 handler: "diagnostic".to_string(),
263 }
264 );
265 }
266}