1use std::collections::HashMap;
6use std::sync::{Mutex, OnceLock};
7use std::time::{Duration, Instant};
8
9use super::sanitize::{format_injected_message, sanitize_message, validate_agent_id};
10use super::types::*;
11use super::{now_unix_millis, sha256_hex, AUDIT_LOG_MAX, RATE_LIMIT_MAX};
12
13pub(super) struct RateLimiter {
16 tokens: u32,
17 max_tokens: u32,
18 last_refill: Instant,
19}
20
21impl RateLimiter {
22 pub(super) fn new(max_tokens: u32) -> Self {
23 Self {
24 tokens: max_tokens,
25 max_tokens,
26 last_refill: Instant::now(),
27 }
28 }
29
30 pub(super) fn check(&mut self) -> bool {
31 let now = Instant::now();
32 let elapsed = now.duration_since(self.last_refill);
33 if elapsed >= Duration::from_secs(1) {
34 self.tokens = self.max_tokens;
35 self.last_refill = now;
36 }
37 if self.tokens > 0 {
38 self.tokens -= 1;
39 true
40 } else {
41 false
42 }
43 }
44}
45
46pub struct Handler {
53 agent_to_block: HashMap<String, String>,
54 block_to_agent: HashMap<String, String>,
55 agent_info: HashMap<String, AgentRegistration>,
56 input_sender: Option<InputSender>,
57 message_sender: Option<MessageSender>,
61 audit_log: Vec<AuditLogEntry>,
62 rate_limiter: RateLimiter,
63 include_source_in_message: bool,
64}
65
66impl Handler {
67 pub fn new() -> Self {
70 Self {
71 agent_to_block: HashMap::new(),
72 block_to_agent: HashMap::new(),
73 agent_info: HashMap::new(),
74 input_sender: None,
75 message_sender: None,
76 audit_log: Vec::with_capacity(AUDIT_LOG_MAX),
77 rate_limiter: RateLimiter::new(RATE_LIMIT_MAX),
78 include_source_in_message: false,
79 }
80 }
81
82 pub fn set_input_sender(&mut self, sender: InputSender) {
84 self.input_sender = Some(sender);
85 }
86
87 pub fn set_message_sender(&mut self, sender: MessageSender) {
92 self.message_sender = Some(sender);
93 }
94
95 #[allow(dead_code)]
97 pub fn set_include_source(&mut self, include: bool) {
98 self.include_source_in_message = include;
99 }
100
101 pub fn register_agent(
103 &mut self,
104 agent_id: &str,
105 block_id: &str,
106 tab_id: Option<&str>,
107 ) -> Result<(), String> {
108 if !validate_agent_id(agent_id) {
109 return Err(format!("invalid agent ID: {}", agent_id));
110 }
111
112 let agent_key = agent_id.to_lowercase();
113
114 if let Some(old_block) = self.agent_to_block.remove(&agent_key) {
116 self.block_to_agent.remove(&old_block);
117 }
118
119 if let Some(old_agent) = self.block_to_agent.remove(block_id) {
121 self.agent_to_block.remove(&old_agent);
122 self.agent_info.remove(&old_agent);
123 }
124
125 let now = now_unix_millis();
126 self.agent_to_block
127 .insert(agent_key.clone(), block_id.to_string());
128 self.block_to_agent
129 .insert(block_id.to_string(), agent_key.clone());
130 self.agent_info.insert(
131 agent_key.clone(),
132 AgentRegistration {
133 agent_id: agent_id.to_string(),
134 block_id: block_id.to_string(),
135 tab_id: tab_id.map(|s| s.to_string()),
136 registered_at: now,
137 last_seen: now,
138 },
139 );
140
141 Ok(())
142 }
143
144 pub fn unregister_agent(&mut self, agent_id: &str) {
146 let agent_key = agent_id.to_lowercase();
147 if let Some(block_id) = self.agent_to_block.remove(&agent_key) {
148 self.block_to_agent.remove(&block_id);
149 }
150 self.agent_info.remove(&agent_key);
151 }
152
153 pub fn unregister_block(&mut self, block_id: &str) {
155 if let Some(agent_id) = self.block_to_agent.remove(block_id) {
156 self.agent_to_block.remove(&agent_id);
157 self.agent_info.remove(&agent_id);
158 }
159 }
160
161 #[allow(dead_code)]
163 pub fn update_last_seen(&mut self, agent_id: &str) {
164 if let Some(info) = self.agent_info.get_mut(&agent_id.to_lowercase()) {
165 info.last_seen = now_unix_millis();
166 }
167 }
168
169 pub fn get_agent(&self, agent_id: &str) -> Option<&AgentRegistration> {
171 self.agent_info.get(&agent_id.to_lowercase())
172 }
173
174 #[allow(dead_code)]
176 pub fn get_agent_by_block(&self, block_id: &str) -> Option<&AgentRegistration> {
177 self.block_to_agent
178 .get(block_id)
179 .and_then(|agent_id| self.agent_info.get(agent_id))
180 }
181
182 pub fn list_agents(&self) -> Vec<AgentRegistration> {
184 self.agent_info.values().cloned().collect()
185 }
186
187 pub fn list_active_blocks(&self) -> Vec<String> {
189 self.block_to_agent.keys().cloned().collect()
190 }
191
192 pub fn inject_message(&mut self, mut req: InjectionRequest) -> InjectionResponse {
198 let now = now_unix_millis();
199
200 if req.request_id.is_none() || req.request_id.as_deref() == Some("") {
202 req.request_id = Some(uuid::Uuid::new_v4().to_string());
203 }
204 let request_id = req.request_id.clone().unwrap_or_default();
205
206 if !self.rate_limiter.check() {
208 return InjectionResponse {
209 success: false,
210 request_id,
211 block_id: None,
212 error: Some("rate limit exceeded".to_string()),
213 timestamp: now,
214 };
215 }
216
217 if !validate_agent_id(&req.target_agent) {
219 return InjectionResponse {
220 success: false,
221 request_id,
222 block_id: None,
223 error: Some(format!("invalid agent ID: {}", req.target_agent)),
224 timestamp: now,
225 };
226 }
227
228 let sanitized = sanitize_message(&req.message);
230
231 let block_id = match self.agent_to_block.get(&req.target_agent.to_lowercase()) {
233 Some(id) => id.clone(),
234 None => {
235 let err = format!("agent not found: {}", req.target_agent);
236 self.log_audit(
237 req.source_agent.as_deref(),
238 &req.target_agent,
239 "",
240 &sanitized,
241 false,
242 Some(&err),
243 &request_id,
244 );
245 return InjectionResponse {
246 success: false,
247 request_id,
248 block_id: None,
249 error: Some(err),
250 timestamp: now,
251 };
252 }
253 };
254
255 let final_msg = format_injected_message(
257 &sanitized,
258 req.source_agent.as_deref(),
259 self.include_source_in_message,
260 );
261
262 if let Some(ref deliver) = self.message_sender {
268 match deliver(&block_id, &final_msg) {
269 Ok(true) => {
270 tracing::info!(
271 target_agent = %req.target_agent,
272 block_id = %block_id,
273 "inject: structured delivery to non-PTY controller (mid-turn steer)"
274 );
275 self.log_audit(
276 req.source_agent.as_deref(),
277 &req.target_agent,
278 &block_id,
279 &sanitized,
280 true,
281 None,
282 &request_id,
283 );
284 return InjectionResponse {
285 success: true,
286 request_id,
287 block_id: Some(block_id),
288 error: None,
289 timestamp: now,
290 };
291 }
292 Ok(false) => {
293 }
295 Err(e) => {
296 tracing::warn!(
300 target_agent = %req.target_agent,
301 block_id = %block_id,
302 error = %e,
303 "inject: structured delivery failed"
304 );
305 self.log_audit(
306 req.source_agent.as_deref(),
307 &req.target_agent,
308 &block_id,
309 &sanitized,
310 false,
311 Some(&e),
312 &request_id,
313 );
314 return InjectionResponse {
315 success: false,
316 request_id,
317 block_id: Some(block_id),
318 error: Some(e),
319 timestamp: now,
320 };
321 }
322 }
323 }
324
325 let sender = match &self.input_sender {
327 Some(s) => s.clone(),
328 None => {
329 let err = "input sender not configured".to_string();
330 self.log_audit(
331 req.source_agent.as_deref(),
332 &req.target_agent,
333 &block_id,
334 &sanitized,
335 false,
336 Some(&err),
337 &request_id,
338 );
339 return InjectionResponse {
340 success: false,
341 request_id,
342 block_id: Some(block_id),
343 error: Some(err),
344 timestamp: now,
345 };
346 }
347 };
348
349 let _ = sender(&block_id, b"\r");
354 let payload = format!("{}\r", final_msg);
355 tracing::info!(
356 target_agent = %req.target_agent,
357 block_id = %block_id,
358 msg_len = payload.len(),
359 "inject: sending payload to PTY"
360 );
361 if let Err(e) = sender(&block_id, payload.as_bytes()) {
362 tracing::error!(
363 target_agent = %req.target_agent,
364 block_id = %block_id,
365 error = %e,
366 "inject: sender failed"
367 );
368 self.log_audit(
369 req.source_agent.as_deref(),
370 &req.target_agent,
371 &block_id,
372 &sanitized,
373 false,
374 Some(&e),
375 &request_id,
376 );
377 return InjectionResponse {
378 success: false,
379 request_id,
380 block_id: Some(block_id),
381 error: Some(e),
382 timestamp: now,
383 };
384 }
385
386 let sender_enter = sender.clone();
388 let block_id_enter = block_id.clone();
389 tokio::spawn(async move {
390 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
391 let _ = sender_enter(&block_id_enter, b"\r");
392 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
393 let _ = sender_enter(&block_id_enter, b"\r");
394 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
395 let _ = sender_enter(&block_id_enter, b"\r");
396 });
397
398 self.log_audit(
400 req.source_agent.as_deref(),
401 &req.target_agent,
402 &block_id,
403 &sanitized,
404 true,
405 None,
406 &request_id,
407 );
408
409 InjectionResponse {
410 success: true,
411 request_id,
412 block_id: Some(block_id),
413 error: None,
414 timestamp: now,
415 }
416 }
417
418 pub fn get_audit_log(&self, limit: usize) -> Vec<AuditLogEntry> {
420 let start = if self.audit_log.len() > limit {
421 self.audit_log.len() - limit
422 } else {
423 0
424 };
425 let mut entries: Vec<_> = self.audit_log[start..].to_vec();
426 entries.reverse();
427 entries
428 }
429
430 #[allow(clippy::too_many_arguments)]
432 pub(super) fn log_audit(
433 &mut self,
434 source_agent: Option<&str>,
435 target_agent: &str,
436 block_id: &str,
437 message: &str,
438 success: bool,
439 error_message: Option<&str>,
440 request_id: &str,
441 ) {
442 let entry = AuditLogEntry {
443 timestamp: now_unix_millis(),
444 source_agent: source_agent.map(|s| s.to_string()),
445 target_agent: target_agent.to_string(),
446 block_id: block_id.to_string(),
447 message_hash: sha256_hex(message),
448 message_length: message.len(),
449 success,
450 error_message: error_message.map(|s| s.to_string()),
451 request_id: request_id.to_string(),
452 };
453
454 if self.audit_log.len() >= AUDIT_LOG_MAX {
455 self.audit_log.remove(0);
456 }
457 self.audit_log.push(entry);
458 }
459}
460
461impl Default for Handler {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467pub struct ReactiveHandler {
471 inner: Mutex<Handler>,
472}
473
474impl ReactiveHandler {
475 pub fn new() -> Self {
476 Self {
477 inner: Mutex::new(Handler::new()),
478 }
479 }
480
481 pub fn set_input_sender(&self, sender: InputSender) {
482 self.inner.lock().unwrap().set_input_sender(sender);
483 }
484
485 pub fn set_message_sender(&self, sender: MessageSender) {
486 self.inner.lock().unwrap().set_message_sender(sender);
487 }
488
489 #[allow(dead_code)]
490 pub fn set_include_source(&self, include: bool) {
491 self.inner.lock().unwrap().set_include_source(include);
492 }
493
494 pub fn register_agent(
495 &self,
496 agent_id: &str,
497 block_id: &str,
498 tab_id: Option<&str>,
499 ) -> Result<(), String> {
500 self.inner
501 .lock()
502 .unwrap()
503 .register_agent(agent_id, block_id, tab_id)
504 }
505
506 pub fn unregister_agent(&self, agent_id: &str) {
507 self.inner.lock().unwrap().unregister_agent(agent_id);
508 }
509
510 pub fn unregister_block(&self, block_id: &str) {
511 self.inner.lock().unwrap().unregister_block(block_id);
512 }
513
514 pub fn agent_id_for_block(&self, block_id: &str) -> Option<String> {
516 self.inner
517 .lock()
518 .unwrap()
519 .block_to_agent
520 .get(block_id)
521 .cloned()
522 }
523
524 #[allow(dead_code)]
525 pub fn update_last_seen(&self, agent_id: &str) {
526 self.inner.lock().unwrap().update_last_seen(agent_id);
527 }
528
529 pub fn get_agent(&self, agent_id: &str) -> Option<AgentRegistration> {
530 self.inner.lock().unwrap().get_agent(agent_id).cloned()
531 }
532
533 #[allow(dead_code)]
534 pub fn get_agent_by_block(&self, block_id: &str) -> Option<AgentRegistration> {
535 self.inner
536 .lock()
537 .unwrap()
538 .get_agent_by_block(block_id)
539 .cloned()
540 }
541
542 pub fn list_agents(&self) -> Vec<AgentRegistration> {
543 self.inner.lock().unwrap().list_agents()
544 }
545
546 pub fn list_active_blocks(&self) -> Vec<String> {
547 self.inner.lock().unwrap().list_active_blocks()
548 }
549
550 pub fn inject_message(&self, req: InjectionRequest) -> InjectionResponse {
551 self.inner.lock().unwrap().inject_message(req)
552 }
553
554 pub fn get_audit_log(&self, limit: usize) -> Vec<AuditLogEntry> {
555 self.inner.lock().unwrap().get_audit_log(limit)
556 }
557}
558
559impl Default for ReactiveHandler {
560 fn default() -> Self {
561 Self::new()
562 }
563}
564
565static GLOBAL_HANDLER: OnceLock<ReactiveHandler> = OnceLock::new();
567
568pub fn get_global_handler() -> &'static ReactiveHandler {
570 GLOBAL_HANDLER.get_or_init(ReactiveHandler::new)
571}