1use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::Arc;
25use std::time::Duration;
26
27use async_trait::async_trait;
28use dashmap::DashMap;
29use nexo_extensions::HookResponse;
30use serde_json::Value;
31use tokio::sync::{mpsc, oneshot};
32
33use crate::agent::hook_registry::HookHandler;
34
35const DEFAULT_HOOK_TIMEOUT: Duration = Duration::from_secs(5);
36
37pub struct RemoteHookHandler {
41 hook_name: String,
42 plugin_id: String,
43 stdin_tx: mpsc::Sender<Value>,
44 pending: Arc<DashMap<u64, oneshot::Sender<Result<Value, String>>>>,
45 next_id: Arc<AtomicU64>,
46 request_timeout: Duration,
47}
48
49impl RemoteHookHandler {
50 pub fn new(
51 hook_name: String,
52 plugin_id: String,
53 stdin_tx: mpsc::Sender<Value>,
54 pending: Arc<DashMap<u64, oneshot::Sender<Result<Value, String>>>>,
55 next_id: Arc<AtomicU64>,
56 ) -> Self {
57 Self {
58 hook_name,
59 plugin_id,
60 stdin_tx,
61 pending,
62 next_id,
63 request_timeout: Self::resolve_timeout(),
64 }
65 }
66
67 fn resolve_timeout() -> Duration {
68 std::env::var("NEXO_PLUGIN_HOOK_TIMEOUT_MS")
69 .ok()
70 .and_then(|s| s.parse::<u64>().ok())
71 .map(Duration::from_millis)
72 .unwrap_or(DEFAULT_HOOK_TIMEOUT)
73 }
74
75 pub fn plugin_id(&self) -> &str {
76 &self.plugin_id
77 }
78
79 pub fn hook_name(&self) -> &str {
80 &self.hook_name
81 }
82}
83
84#[async_trait]
85impl HookHandler for RemoteHookHandler {
86 async fn on_hook(&self, name: &str, event: Value) -> anyhow::Result<HookResponse> {
87 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
88 let frame = serde_json::json!({
89 "jsonrpc": "2.0",
90 "id": id,
91 "method": "hook.on_hook",
92 "params": {
93 "plugin_id": &self.plugin_id,
94 "hook_name": name,
95 "event": event,
96 },
97 });
98 let (tx, rx) = oneshot::channel();
99 self.pending.insert(id, tx);
100
101 if self.stdin_tx.send(frame).await.is_err() {
102 self.pending.remove(&id);
103 tracing::warn!(
104 plugin = %self.plugin_id,
105 hook = %self.hook_name,
106 "hook.on_hook stdin send failed — Continue"
107 );
108 return Ok(HookResponse::default());
109 }
110
111 match tokio::time::timeout(self.request_timeout, rx).await {
112 Ok(Ok(Ok(value))) => match serde_json::from_value::<HookResponse>(value) {
113 Ok(resp) => Ok(resp),
114 Err(e) => {
115 tracing::warn!(
116 plugin = %self.plugin_id,
117 hook = %self.hook_name,
118 error = %e,
119 "hook.on_hook reply decode failed — Continue"
120 );
121 Ok(HookResponse::default())
122 }
123 },
124 Ok(Ok(Err(err_str))) => {
125 tracing::warn!(
126 plugin = %self.plugin_id,
127 hook = %self.hook_name,
128 error = %err_str,
129 "hook.on_hook returned error — Continue"
130 );
131 Ok(HookResponse::default())
132 }
133 Ok(Err(_)) => {
134 self.pending.remove(&id);
135 tracing::warn!(
136 plugin = %self.plugin_id,
137 hook = %self.hook_name,
138 "hook.on_hook pending dropped (subprocess gone) — Continue"
139 );
140 Ok(HookResponse::default())
141 }
142 Err(_) => {
143 self.pending.remove(&id);
144 tracing::warn!(
145 plugin = %self.plugin_id,
146 hook = %self.hook_name,
147 timeout_ms = self.request_timeout.as_millis() as u64,
148 "hook.on_hook timed out — Continue"
149 );
150 Ok(HookResponse::default())
151 }
152 }
153 }
154}
155
156#[derive(Debug, thiserror::Error)]
161pub enum HookHandlerRegistrationError {
162 #[error(
163 "subprocess plugin inner not initialized — call register_remote_hook_handlers AFTER init()"
164 )]
165 InnerUnavailable,
166}
167
168#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn build() -> (
175 Arc<RemoteHookHandler>,
176 mpsc::Receiver<Value>,
177 Arc<DashMap<u64, oneshot::Sender<Result<Value, String>>>>,
178 ) {
179 let (stdin_tx, stdin_rx) = mpsc::channel(8);
180 let pending: Arc<DashMap<u64, oneshot::Sender<Result<Value, String>>>> =
181 Arc::new(DashMap::new());
182 let next_id = Arc::new(AtomicU64::new(1));
183 let handler = Arc::new(RemoteHookHandler::new(
184 "before_message".to_string(),
185 "mock_plugin".to_string(),
186 stdin_tx,
187 pending.clone(),
188 next_id,
189 ));
190 (handler, stdin_rx, pending)
191 }
192
193 fn resolve_with_result(
194 pending: &DashMap<u64, oneshot::Sender<Result<Value, String>>>,
195 id: u64,
196 result: Value,
197 ) {
198 if let Some((_, sender)) = pending.remove(&id) {
199 let _ = sender.send(Ok(result));
200 }
201 }
202
203 fn resolve_with_error(
204 pending: &DashMap<u64, oneshot::Sender<Result<Value, String>>>,
205 id: u64,
206 err_obj: Value,
207 ) {
208 if let Some((_, sender)) = pending.remove(&id) {
209 let _ = sender.send(Err(err_obj.to_string()));
210 }
211 }
212
213 #[tokio::test]
214 async fn on_hook_serializes_request_with_hook_name_and_event() {
215 let (handler, mut stdin_rx, pending) = build();
216 let task = tokio::spawn({
217 let handler = handler.clone();
218 async move {
219 handler
220 .on_hook("before_message", serde_json::json!({"sender":"alice"}))
221 .await
222 }
223 });
224
225 let frame = stdin_rx.recv().await.expect("frame");
226 assert_eq!(frame["method"], "hook.on_hook");
227 assert_eq!(frame["params"]["plugin_id"], "mock_plugin");
228 assert_eq!(frame["params"]["hook_name"], "before_message");
229 assert_eq!(frame["params"]["event"]["sender"], "alice");
230 let id = frame["id"].as_u64().unwrap();
231 resolve_with_result(&pending, id, serde_json::json!({}));
232
233 let resp = task.await.unwrap().unwrap();
234 assert!(!resp.abort);
235 assert_eq!(resp.decision, None);
236 }
237
238 #[tokio::test]
239 async fn on_hook_deserializes_response_with_decision() {
240 let (handler, mut stdin_rx, pending) = build();
241 let task = tokio::spawn({
242 let handler = handler.clone();
243 async move {
244 handler
245 .on_hook("before_message", serde_json::json!({}))
246 .await
247 }
248 });
249
250 let frame = stdin_rx.recv().await.expect("frame");
251 let id = frame["id"].as_u64().unwrap();
252 resolve_with_result(
253 &pending,
254 id,
255 serde_json::json!({
256 "abort": true,
257 "reason": "PII detected",
258 "decision": "block"
259 }),
260 );
261
262 let resp = task.await.unwrap().unwrap();
263 assert!(resp.abort);
264 assert_eq!(resp.reason.as_deref(), Some("PII detected"));
265 assert_eq!(resp.decision.as_deref(), Some("block"));
266 }
267
268 #[tokio::test]
269 async fn on_hook_unsupported_method_returns_continue() {
270 let (handler, mut stdin_rx, pending) = build();
271 let task = tokio::spawn({
272 let handler = handler.clone();
273 async move {
274 handler
275 .on_hook("before_message", serde_json::json!({}))
276 .await
277 }
278 });
279
280 let frame = stdin_rx.recv().await.expect("frame");
281 let id = frame["id"].as_u64().unwrap();
282 resolve_with_error(
283 &pending,
284 id,
285 serde_json::json!({
286 "code": -32601,
287 "message": "hook.on_hook"
288 }),
289 );
290
291 let resp = task.await.unwrap().unwrap();
293 assert_eq!(resp, HookResponse::default());
294 }
295
296 #[tokio::test(flavor = "current_thread", start_paused = true)]
297 async fn on_hook_timeout_returns_continue() {
298 let (stdin_tx, mut stdin_rx) = mpsc::channel(8);
299 let pending: Arc<DashMap<u64, oneshot::Sender<Result<Value, String>>>> =
300 Arc::new(DashMap::new());
301 let next_id = Arc::new(AtomicU64::new(1));
302 let handler = RemoteHookHandler {
303 hook_name: "before_message".into(),
304 plugin_id: "mock_plugin".into(),
305 stdin_tx,
306 pending,
307 next_id,
308 request_timeout: Duration::from_millis(50),
309 };
310
311 let task = tokio::spawn(async move {
312 handler
313 .on_hook("before_message", serde_json::json!({}))
314 .await
315 });
316 let _frame = stdin_rx.recv().await.expect("frame");
317 tokio::time::advance(Duration::from_millis(200)).await;
319
320 let resp = task.await.unwrap().unwrap();
321 assert_eq!(resp, HookResponse::default());
322 }
323
324 #[tokio::test]
325 async fn on_hook_invalid_response_returns_continue() {
326 let (handler, mut stdin_rx, pending) = build();
327 let task = tokio::spawn({
328 let handler = handler.clone();
329 async move {
330 handler
331 .on_hook("before_message", serde_json::json!({}))
332 .await
333 }
334 });
335
336 let frame = stdin_rx.recv().await.expect("frame");
337 let id = frame["id"].as_u64().unwrap();
338 resolve_with_result(&pending, id, serde_json::json!([1, 2, 3]));
341
342 let resp = task.await.unwrap().unwrap();
343 assert_eq!(resp, HookResponse::default());
344 }
345
346 #[tokio::test]
347 async fn on_hook_transform_decision_round_trips_transformed_body() {
348 let (handler, mut stdin_rx, pending) = build();
349 let task = tokio::spawn({
350 let handler = handler.clone();
351 async move {
352 handler
353 .on_hook(
354 "before_message",
355 serde_json::json!({"body": "ssn 123-45-6789"}),
356 )
357 .await
358 }
359 });
360
361 let frame = stdin_rx.recv().await.expect("frame");
362 let id = frame["id"].as_u64().unwrap();
363 resolve_with_result(
364 &pending,
365 id,
366 serde_json::json!({
367 "decision": "transform",
368 "transformed_body": "ssn [REDACTED]"
369 }),
370 );
371
372 let resp = task.await.unwrap().unwrap();
373 assert_eq!(resp.decision.as_deref(), Some("transform"));
374 assert_eq!(resp.transformed_body.as_deref(), Some("ssn [REDACTED]"));
375 }
376
377 #[tokio::test]
378 async fn on_hook_do_not_reply_again_round_trips() {
379 let (handler, mut stdin_rx, pending) = build();
380 let task = tokio::spawn({
381 let handler = handler.clone();
382 async move {
383 handler
384 .on_hook("after_message", serde_json::json!({}))
385 .await
386 }
387 });
388
389 let frame = stdin_rx.recv().await.expect("frame");
390 let id = frame["id"].as_u64().unwrap();
391 resolve_with_result(
392 &pending,
393 id,
394 serde_json::json!({
395 "decision": "allow",
396 "do_not_reply_again": true
397 }),
398 );
399
400 let resp = task.await.unwrap().unwrap();
401 assert!(resp.do_not_reply_again);
402 }
403
404 #[tokio::test]
405 async fn on_hook_override_event_round_trips() {
406 let (handler, mut stdin_rx, pending) = build();
407 let task = tokio::spawn({
408 let handler = handler.clone();
409 async move {
410 handler
411 .on_hook("before_message", serde_json::json!({"k": "v"}))
412 .await
413 }
414 });
415
416 let frame = stdin_rx.recv().await.expect("frame");
417 let id = frame["id"].as_u64().unwrap();
418 resolve_with_result(
419 &pending,
420 id,
421 serde_json::json!({
422 "override": { "k": "rewritten" }
423 }),
424 );
425
426 let resp = task.await.unwrap().unwrap();
427 assert_eq!(
428 resp.override_event
429 .as_ref()
430 .and_then(|v| v.get("k"))
431 .and_then(|v| v.as_str()),
432 Some("rewritten")
433 );
434 }
435}