1use std::sync::Arc;
21use std::time::{Duration, Instant};
22
23use dashmap::DashMap;
24use tokio::sync::{mpsc, oneshot};
25use tracing::{debug, warn};
26
27use crate::capability::CapabilityChecker;
28use crate::error::{KernelError, KernelResult};
29use crate::ipc::{KernelMessage, MessageTarget};
30use crate::process::{Pid, ProcessState, ProcessTable};
31use crate::service::ServiceRegistry;
32use crate::topic::{SubscriberSink, TopicRouter};
33
34#[cfg(feature = "exochain")]
35use crate::chain::ChainManager;
36
37#[cfg(feature = "mesh")]
38use crate::mesh_ipc::MeshIpcEnvelope;
39#[cfg(feature = "mesh")]
40use crate::mesh_runtime::MeshRuntime;
41
42const DEFAULT_INBOX_CAPACITY: usize = 1024;
44
45#[allow(dead_code)]
53const MAX_A2A_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
54
55struct PendingRequest {
57 response_tx: oneshot::Sender<KernelMessage>,
59 #[allow(dead_code)]
61 sent_at: Instant,
62}
63
64#[cfg(feature = "mesh")]
68#[async_trait::async_trait]
69pub trait RemoteForwarder: Send + Sync + 'static {
70 async fn forward(&self, node_id: &str, msg: KernelMessage) -> KernelResult<()>;
72}
73
74pub struct A2ARouter {
80 process_table: Arc<ProcessTable>,
82
83 capability_checker: Arc<CapabilityChecker>,
85
86 topic_router: Arc<TopicRouter>,
88
89 service_registry: Option<Arc<ServiceRegistry>>,
91
92 inboxes: DashMap<Pid, mpsc::Sender<KernelMessage>>,
94
95 pending_requests: DashMap<String, PendingRequest>,
97
98 #[cfg(feature = "exochain")]
104 gate: std::sync::OnceLock<Arc<dyn crate::gate::GateBackend>>,
105
106 #[cfg(feature = "os-patterns")]
108 dead_letter_queue: std::sync::OnceLock<Arc<crate::dead_letter::DeadLetterQueue>>,
109
110 #[cfg(feature = "mesh")]
112 mesh_runtime: std::sync::OnceLock<Arc<MeshRuntime>>,
113
114 #[cfg(feature = "mesh")]
116 remote_forwarder: std::sync::OnceLock<Arc<dyn RemoteForwarder>>,
117}
118
119impl A2ARouter {
120 pub fn new(
122 process_table: Arc<ProcessTable>,
123 capability_checker: Arc<CapabilityChecker>,
124 topic_router: Arc<TopicRouter>,
125 ) -> Self {
126 Self {
127 process_table,
128 capability_checker,
129 topic_router,
130 service_registry: None,
131 inboxes: DashMap::new(),
132 pending_requests: DashMap::new(),
133 #[cfg(feature = "exochain")]
134 gate: std::sync::OnceLock::new(),
135 #[cfg(feature = "os-patterns")]
136 dead_letter_queue: std::sync::OnceLock::new(),
137 #[cfg(feature = "mesh")]
138 mesh_runtime: std::sync::OnceLock::new(),
139 #[cfg(feature = "mesh")]
140 remote_forwarder: std::sync::OnceLock::new(),
141 }
142 }
143
144 pub fn with_service_registry(mut self, registry: Arc<ServiceRegistry>) -> Self {
146 self.service_registry = Some(registry);
147 self
148 }
149
150 #[cfg(feature = "exochain")]
156 pub fn with_gate(self, gate: Arc<dyn crate::gate::GateBackend>) -> Self {
157 let _ = self.gate.set(gate);
158 self
159 }
160
161 #[cfg(feature = "exochain")]
167 pub fn set_gate(&self, gate: Arc<dyn crate::gate::GateBackend>) {
168 let _ = self.gate.set(gate);
169 }
170
171 #[cfg(feature = "os-patterns")]
176 pub fn set_dead_letter_queue(&self, dlq: Arc<crate::dead_letter::DeadLetterQueue>) {
177 let _ = self.dead_letter_queue.set(dlq);
178 }
179
180 #[cfg(feature = "os-patterns")]
182 pub fn dead_letter_queue(&self) -> Option<&Arc<crate::dead_letter::DeadLetterQueue>> {
183 self.dead_letter_queue.get()
184 }
185
186 #[cfg(feature = "mesh")]
191 pub fn set_mesh_runtime(&self, runtime: Arc<MeshRuntime>) {
192 let _ = self.mesh_runtime.set(runtime);
193 }
194
195 #[cfg(feature = "mesh")]
198 pub fn set_remote_forwarder(&self, forwarder: Arc<dyn RemoteForwarder>) {
199 let _ = self.remote_forwarder.set(forwarder);
200 }
201
202 #[cfg(feature = "mesh")]
204 pub fn mesh_runtime(&self) -> Option<&Arc<MeshRuntime>> {
205 self.mesh_runtime.get()
206 }
207
208 pub fn service_registry(&self) -> Option<&Arc<ServiceRegistry>> {
210 self.service_registry.as_ref()
211 }
212
213 pub fn create_inbox(&self, pid: Pid) -> mpsc::Receiver<KernelMessage> {
222 let (tx, rx) = mpsc::channel(DEFAULT_INBOX_CAPACITY);
223 self.inboxes.insert(pid, tx);
224 debug!(pid, "created inbox");
225 rx
226 }
227
228 pub fn remove_inbox(&self, pid: Pid) {
230 self.inboxes.remove(&pid);
231 debug!(pid, "removed inbox");
232 }
233
234 pub async fn send(&self, msg: KernelMessage) -> KernelResult<()> {
255 let from = msg.from;
256
257 let sender = self
259 .process_table
260 .get(from)
261 .ok_or(KernelError::ProcessNotFound { pid: from })?;
262
263 if !matches!(
264 sender.state,
265 ProcessState::Running | ProcessState::Suspended
266 ) {
267 return Err(KernelError::Ipc(format!(
268 "sender PID {from} is not running (state: {})",
269 sender.state
270 )));
271 }
272
273 #[cfg(feature = "exochain")]
277 if let Some(gate) = self.gate.get() {
278 let action = match &msg.payload {
279 crate::ipc::MessagePayload::ToolCall { name, .. } => format!("tool.{name}"),
280 crate::ipc::MessagePayload::Signal(_) => "ipc.signal".to_string(),
281 _ => "ipc.send".to_string(),
282 };
283 use crate::governance::{GateEffectKind, with_effect_context};
284 let context = with_effect_context(
285 serde_json::json!({
286 "from": from,
287 "target": format!("{:?}", msg.target),
288 "layer": "routing",
289 }),
290 &GateEffectKind::A2aRouting.effect_vector(),
291 );
292 match gate.check(&from.to_string(), &action, &context) {
293 crate::gate::GateDecision::Deny { reason, .. } => {
294 return Err(KernelError::CapabilityDenied {
295 pid: from,
296 action,
297 reason: format!("routing gate denied: {reason}"),
298 });
299 }
300 crate::gate::GateDecision::Defer { .. }
301 | crate::gate::GateDecision::Permit { .. } => {
302 }
304 }
305 }
306
307 match &msg.target {
309 MessageTarget::Process(target_pid) => {
310 self.capability_checker
312 .check_ipc_target(from, *target_pid)?;
313
314 self.deliver_to_inbox(*target_pid, msg).await
315 }
316 MessageTarget::Topic(topic) => {
317 self.capability_checker.check_ipc_topic(from, topic)?;
323
324 let sinks = self.topic_router.live_sinks(topic);
325 let mut delivered = 0u32;
326
327 let mut external_line: Option<Vec<u8>> = None;
331 if sinks
332 .iter()
333 .any(|(_, s)| matches!(s, SubscriberSink::ExternalStream(_)))
334 && let Ok(mut bytes) = serde_json::to_vec(&msg)
335 {
336 bytes.push(b'\n');
337 external_line = Some(bytes);
338 }
339
340 for (_id, sink) in &sinks {
341 match sink {
342 SubscriberSink::PidInbox(sub_pid) => {
343 if *sub_pid != from {
344 let msg_clone = msg.clone();
345 if self.deliver_to_inbox(*sub_pid, msg_clone).await.is_ok() {
346 delivered += 1;
347 }
348 }
349 }
350 SubscriberSink::ExternalStream(tx) => {
351 if let Some(line) = external_line.as_ref() {
352 match tx.try_send(line.clone()) {
357 Ok(()) => {
358 delivered += 1;
359 }
360 Err(e) => {
361 warn!(topic, error = %e, "external stream drop");
362 }
363 }
364 }
365 }
366 }
367 }
368
369 #[cfg(feature = "mesh")]
372 if let Some(runtime) = self.mesh_runtime.get() {
373 let peer_ids = runtime.peers_for_topic(topic);
374 let mut mesh_delivered = 0u32;
375 for peer_id in &peer_ids {
376 let envelope = MeshIpcEnvelope::new(
377 runtime.node_id().to_string(),
378 peer_id.clone(),
379 msg.clone(),
380 );
381 match runtime.send_to_peer(peer_id, envelope).await {
382 Ok(()) => mesh_delivered += 1,
383 Err(e) => {
384 warn!(
385 topic,
386 peer = %peer_id,
387 error = %e,
388 "failed to forward topic to mesh peer"
389 );
390 }
391 }
392 }
393 if mesh_delivered > 0 {
394 debug!(from, topic, mesh_delivered, "forwarded topic to mesh peers");
395 }
396 }
397
398 debug!(from, topic, delivered, "published to topic");
399 Ok(())
400 }
401 MessageTarget::Broadcast => {
402 let mut delivered = 0u32;
403 let pids: Vec<Pid> = self.inboxes.iter().map(|entry| *entry.key()).collect();
404
405 for pid in pids {
406 if pid != from {
407 if self.capability_checker.check_ipc_target(from, pid).is_ok() {
409 let msg_clone = msg.clone();
410 if self.deliver_to_inbox(pid, msg_clone).await.is_ok() {
411 delivered += 1;
412 }
413 }
414 }
415 }
416 debug!(from, delivered, "broadcast sent");
417 Ok(())
418 }
419 MessageTarget::Service(name) => {
420 let name = name.clone();
421 self.route_to_service(from, &name, msg).await
422 }
423 MessageTarget::ServiceMethod { service, .. } => {
424 let service_name = service.clone();
425 self.route_to_service(from, &service_name, msg).await
426 }
427 MessageTarget::Kernel => {
428 debug!(from, "kernel message routing not yet implemented");
429 Ok(())
430 }
431 MessageTarget::RemoteNode { node_id, .. } => {
432 #[cfg(feature = "mesh")]
433 {
434 if let Some(forwarder) = self.remote_forwarder.get() {
435 let node_id = node_id.clone();
436 debug!(from, %node_id, "routing message to remote node via mesh service");
437 return forwarder.forward(&node_id, msg).await;
438 }
439 if let Some(runtime) = self.mesh_runtime.get() {
440 let node_id = node_id.clone();
441 debug!(from, %node_id, "routing message to remote node via mesh");
442 let envelope = MeshIpcEnvelope::new(
443 runtime.node_id().to_string(),
444 node_id.clone(),
445 msg,
446 );
447 runtime.send_to_peer(&node_id, envelope).await.map_err(|e| {
448 KernelError::Mesh(format!(
449 "failed to send to remote node '{node_id}': {e}"
450 ))
451 })
452 } else {
453 debug!(from, %node_id, "remote node routing: no mesh runtime attached");
454 Err(KernelError::Mesh(format!(
455 "remote routing to node '{node_id}' not yet implemented"
456 )))
457 }
458 }
459 #[cfg(not(feature = "mesh"))]
460 {
461 debug!(from, %node_id, "remote node routing not available (mesh feature disabled)");
462 Err(KernelError::Mesh(format!(
463 "remote routing to node '{node_id}' not yet implemented"
464 )))
465 }
466 }
467 }
468 }
469
470 async fn deliver_to_inbox(&self, pid: Pid, msg: KernelMessage) -> KernelResult<()> {
476 let tx = match self.inboxes.get(&pid) {
480 Some(tx) => tx.clone(),
481 None => {
482 warn!(pid, "no inbox for PID, dead-lettering");
483 #[cfg(feature = "os-patterns")]
484 if let Some(dlq) = self.dead_letter_queue.get() {
485 dlq.intake(
486 msg,
487 crate::dead_letter::DeadLetterReason::TargetNotFound { pid },
488 );
489 }
490 return Err(KernelError::Ipc(format!("no inbox for PID {pid}")));
491 }
492 };
493
494 match tx.try_send(msg) {
495 Ok(()) => {
496 debug!(pid, "message delivered to inbox");
497 Ok(())
498 }
499 Err(mpsc::error::TrySendError::Full(msg)) => {
500 let _rejected_msg = msg;
501 warn!(pid, "inbox full, dead-lettering");
502 #[cfg(feature = "os-patterns")]
503 if let Some(dlq) = self.dead_letter_queue.get() {
504 dlq.intake(
505 rejected_msg,
506 crate::dead_letter::DeadLetterReason::InboxFull { pid },
507 );
508 }
509 Err(KernelError::Ipc(format!("inbox full for PID {pid}")))
510 }
511 Err(mpsc::error::TrySendError::Closed(msg)) => {
512 let _rejected_msg = msg;
513 warn!(pid, "inbox closed, removing and dead-lettering");
514 self.inboxes.remove(&pid);
515 #[cfg(feature = "os-patterns")]
516 if let Some(dlq) = self.dead_letter_queue.get() {
517 dlq.intake(
518 rejected_msg,
519 crate::dead_letter::DeadLetterReason::AgentExited { pid },
520 );
521 }
522 Err(KernelError::Ipc(format!("inbox closed for PID {pid}")))
523 }
524 }
525 }
526
527 async fn route_to_service(
532 &self,
533 from: Pid,
534 service_name: &str,
535 msg: KernelMessage,
536 ) -> KernelResult<()> {
537 let registry = self.service_registry.as_ref().ok_or_else(|| {
538 KernelError::Ipc(format!(
539 "no service registry configured; cannot route to service '{service_name}'"
540 ))
541 })?;
542
543 let target_pid = registry
544 .resolve_target(service_name)
545 .ok_or_else(|| KernelError::Ipc(format!("service not found: '{service_name}'")))?;
546
547 self.capability_checker.check_ipc_target(from, target_pid)?;
549
550 self.deliver_to_inbox(target_pid, msg).await
551 }
552
553 #[cfg(feature = "exochain")]
563 pub async fn send_checked(
564 &self,
565 msg: KernelMessage,
566 chain: Option<&ChainManager>,
567 ) -> KernelResult<()> {
568 if let Some(cm) = chain {
571 cm.append(
572 "ipc",
573 "ipc.send",
574 Some(serde_json::json!({
575 "from": msg.from,
576 "target": format!("{:?}", msg.target),
577 "payload_type": msg.payload.type_name(),
578 "msg_id": msg.id,
579 })),
580 );
581 }
582 self.send(msg).await
583 }
584
585 pub fn topic_router(&self) -> &Arc<TopicRouter> {
587 &self.topic_router
588 }
589
590 pub fn inbox_count(&self) -> usize {
592 self.inboxes.len()
593 }
594
595 pub fn has_inbox(&self, pid: Pid) -> bool {
597 self.inboxes.contains_key(&pid)
598 }
599
600 pub async fn request(
613 &self,
614 msg: KernelMessage,
615 timeout: Duration,
616 ) -> KernelResult<KernelMessage> {
617 let request_id = msg.id.clone();
618 let (tx, rx) = oneshot::channel();
619
620 self.pending_requests.insert(
622 request_id.clone(),
623 PendingRequest {
624 response_tx: tx,
625 sent_at: Instant::now(),
626 },
627 );
628
629 if let Err(e) = self.send(msg).await {
631 self.pending_requests.remove(&request_id);
632 return Err(e);
633 }
634
635 match tokio::time::timeout(timeout, rx).await {
637 Ok(Ok(response)) => Ok(response),
638 Ok(Err(_)) => {
639 self.pending_requests.remove(&request_id);
640 Err(KernelError::Ipc("response channel closed".into()))
641 }
642 Err(_) => {
643 self.pending_requests.remove(&request_id);
644 Err(KernelError::Timeout {
645 operation: format!("request {request_id}"),
646 duration_ms: timeout.as_millis() as u64,
647 })
648 }
649 }
650 }
651
652 pub fn try_complete_request(&self, msg: KernelMessage) -> bool {
658 if let Some(ref corr_id) = msg.correlation_id
659 && let Some((_, pending)) = self.pending_requests.remove(corr_id)
660 {
661 let _ = pending.response_tx.send(msg);
662 return true;
663 }
664 false
665 }
666
667 pub fn pending_request_count(&self) -> usize {
669 self.pending_requests.len()
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use crate::capability::AgentCapabilities;
677 use crate::ipc::MessagePayload;
678 use crate::process::{ProcessEntry, ResourceUsage};
679 use tokio_util::sync::CancellationToken;
680
681 fn setup_router(
682 agent_count: usize,
683 ) -> (A2ARouter, Vec<Pid>, Vec<mpsc::Receiver<KernelMessage>>) {
684 let table = Arc::new(ProcessTable::new(64));
685 let mut pids = Vec::new();
686
687 for i in 0..agent_count {
688 let entry = ProcessEntry {
689 pid: 0,
690 agent_id: format!("agent-{i}"),
691 state: ProcessState::Running,
692 capabilities: AgentCapabilities::default(),
693 resource_usage: ResourceUsage::default(),
694 cancel_token: CancellationToken::new(),
695 parent_pid: None,
696 };
697 let pid = table.insert(entry).unwrap();
698 pids.push(pid);
699 }
700
701 let checker = Arc::new(CapabilityChecker::new(table.clone()));
702 let topic_router = Arc::new(TopicRouter::new(table.clone()));
703 let router = A2ARouter::new(table, checker, topic_router);
704
705 let mut receivers = Vec::new();
706 for &pid in &pids {
707 let rx = router.create_inbox(pid);
708 receivers.push(rx);
709 }
710
711 (router, pids, receivers)
712 }
713
714 #[tokio::test]
715 async fn direct_message_delivery() {
716 let (router, pids, mut receivers) = setup_router(2);
717
718 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "hello");
719 router.send(msg).await.unwrap();
720
721 let received = receivers[1].try_recv().unwrap();
722 assert_eq!(received.from, pids[0]);
723 assert!(matches!(
724 received.payload,
725 MessagePayload::Text(ref t) if t == "hello"
726 ));
727 }
728
729 #[tokio::test]
730 async fn message_to_self_works() {
731 let (router, pids, mut receivers) = setup_router(1);
732
733 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[0]), "self-msg");
734 router.send(msg).await.unwrap();
735
736 let received = receivers[0].try_recv().unwrap();
737 assert!(matches!(
738 received.payload,
739 MessagePayload::Text(ref t) if t == "self-msg"
740 ));
741 }
742
743 #[tokio::test]
744 async fn broadcast_delivers_to_all_except_sender() {
745 let (router, pids, mut receivers) = setup_router(3);
746
747 let msg = KernelMessage::text(pids[0], MessageTarget::Broadcast, "broadcast");
748 router.send(msg).await.unwrap();
749
750 assert!(receivers[0].try_recv().is_err());
752
753 let r1 = receivers[1].try_recv().unwrap();
755 assert!(matches!(
756 r1.payload,
757 MessagePayload::Text(ref t) if t == "broadcast"
758 ));
759 let r2 = receivers[2].try_recv().unwrap();
760 assert!(matches!(
761 r2.payload,
762 MessagePayload::Text(ref t) if t == "broadcast"
763 ));
764 }
765
766 #[tokio::test]
767 async fn topic_publish_delivers_to_subscribers() {
768 let (router, pids, mut receivers) = setup_router(3);
769
770 router.topic_router().subscribe(pids[1], "build");
772 router.topic_router().subscribe(pids[2], "build");
773
774 let msg = KernelMessage::text(pids[0], MessageTarget::Topic("build".into()), "build done");
775 router.send(msg).await.unwrap();
776
777 assert!(receivers[0].try_recv().is_err());
779
780 assert!(receivers[1].try_recv().is_ok());
782 assert!(receivers[2].try_recv().is_ok());
783 }
784
785 #[tokio::test]
786 async fn topic_publish_delivers_to_external_stream() {
787 let (router, pids, _receivers) = setup_router(1);
788
789 let (tx, mut rx) = mpsc::channel::<Vec<u8>>(8);
790 let _id = router
791 .topic_router()
792 .subscribe_sink("events", SubscriberSink::ExternalStream(tx));
793
794 let msg = KernelMessage::text(pids[0], MessageTarget::Topic("events".into()), "hi");
795 router.send(msg).await.unwrap();
796
797 let line = rx
799 .try_recv()
800 .expect("external stream should receive message");
801 assert!(line.ends_with(b"\n"));
802 let trimmed = std::str::from_utf8(&line[..line.len() - 1]).unwrap();
803 let parsed: serde_json::Value = serde_json::from_str(trimmed).unwrap();
804 assert_eq!(parsed["from"], pids[0]);
805 }
806
807 #[tokio::test]
808 async fn topic_publish_fans_out_to_pid_and_external() {
809 let (router, pids, mut receivers) = setup_router(2);
810 router.topic_router().subscribe(pids[1], "events");
811
812 let (tx, mut rx) = mpsc::channel::<Vec<u8>>(8);
813 router
814 .topic_router()
815 .subscribe_sink("events", SubscriberSink::ExternalStream(tx));
816
817 let msg = KernelMessage::text(pids[0], MessageTarget::Topic("events".into()), "both");
818 router.send(msg).await.unwrap();
819
820 assert!(receivers[1].try_recv().is_ok(), "pid subscriber missed");
821 assert!(rx.try_recv().is_ok(), "external subscriber missed");
822 }
823
824 #[tokio::test]
825 async fn topic_publish_excludes_sender_if_subscribed() {
826 let (router, pids, mut receivers) = setup_router(2);
827
828 router.topic_router().subscribe(pids[0], "build");
830 router.topic_router().subscribe(pids[1], "build");
831
832 let msg = KernelMessage::text(pids[0], MessageTarget::Topic("build".into()), "done");
833 router.send(msg).await.unwrap();
834
835 assert!(receivers[0].try_recv().is_err());
837 assert!(receivers[1].try_recv().is_ok());
839 }
840
841 #[tokio::test]
842 async fn send_from_nonexistent_pid_fails() {
843 let (router, _pids, _receivers) = setup_router(1);
844
845 let msg = KernelMessage::text(999, MessageTarget::Process(1), "hello");
846 let result = router.send(msg).await;
847 assert!(result.is_err());
848 }
849
850 #[tokio::test]
851 async fn send_to_pid_without_inbox_fails() {
852 let table = Arc::new(ProcessTable::new(64));
853
854 let sender_entry = ProcessEntry {
856 pid: 0,
857 agent_id: "sender".to_owned(),
858 state: ProcessState::Running,
859 capabilities: AgentCapabilities::default(),
860 resource_usage: ResourceUsage::default(),
861 cancel_token: CancellationToken::new(),
862 parent_pid: None,
863 };
864 let sender_pid = table.insert(sender_entry).unwrap();
865
866 let target_entry = ProcessEntry {
868 pid: 0,
869 agent_id: "target".to_owned(),
870 state: ProcessState::Running,
871 capabilities: AgentCapabilities::default(),
872 resource_usage: ResourceUsage::default(),
873 cancel_token: CancellationToken::new(),
874 parent_pid: None,
875 };
876 let target_pid = table.insert(target_entry).unwrap();
877
878 let checker = Arc::new(CapabilityChecker::new(table.clone()));
879 let topic_router = Arc::new(TopicRouter::new(table.clone()));
880 let router = A2ARouter::new(table, checker, topic_router);
881
882 let _rx = router.create_inbox(sender_pid);
884
885 let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "hello");
886 let result = router.send(msg).await;
887 assert!(result.is_err());
888 }
889
890 #[tokio::test]
891 async fn ipc_scope_restricts_messaging() {
892 let table = Arc::new(ProcessTable::new(64));
893
894 use crate::capability::IpcScope;
896 let sender_entry = ProcessEntry {
897 pid: 0,
898 agent_id: "restricted".to_owned(),
899 state: ProcessState::Running,
900 capabilities: AgentCapabilities {
901 ipc_scope: IpcScope::Restricted(vec![]), ..Default::default()
903 },
904 resource_usage: ResourceUsage::default(),
905 cancel_token: CancellationToken::new(),
906 parent_pid: None,
907 };
908 let sender_pid = table.insert(sender_entry).unwrap();
909
910 let target_entry = ProcessEntry {
912 pid: 0,
913 agent_id: "target".to_owned(),
914 state: ProcessState::Running,
915 capabilities: AgentCapabilities::default(),
916 resource_usage: ResourceUsage::default(),
917 cancel_token: CancellationToken::new(),
918 parent_pid: None,
919 };
920 let target_pid = table.insert(target_entry).unwrap();
921
922 let checker = Arc::new(CapabilityChecker::new(table.clone()));
923 let topic_router = Arc::new(TopicRouter::new(table.clone()));
924 let router = A2ARouter::new(table, checker, topic_router);
925 let _rx1 = router.create_inbox(sender_pid);
926 let _rx2 = router.create_inbox(target_pid);
927
928 let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "blocked");
929 let result = router.send(msg).await;
930 assert!(result.is_err());
931 }
932
933 #[tokio::test]
934 async fn ipc_scope_none_blocks_all() {
935 let table = Arc::new(ProcessTable::new(64));
936
937 use crate::capability::IpcScope;
938 let sender_entry = ProcessEntry {
939 pid: 0,
940 agent_id: "no-ipc".to_owned(),
941 state: ProcessState::Running,
942 capabilities: AgentCapabilities {
943 can_ipc: false,
944 ipc_scope: IpcScope::None,
945 ..Default::default()
946 },
947 resource_usage: ResourceUsage::default(),
948 cancel_token: CancellationToken::new(),
949 parent_pid: None,
950 };
951 let sender_pid = table.insert(sender_entry).unwrap();
952
953 let target_entry = ProcessEntry {
954 pid: 0,
955 agent_id: "target".to_owned(),
956 state: ProcessState::Running,
957 capabilities: AgentCapabilities::default(),
958 resource_usage: ResourceUsage::default(),
959 cancel_token: CancellationToken::new(),
960 parent_pid: None,
961 };
962 let target_pid = table.insert(target_entry).unwrap();
963
964 let checker = Arc::new(CapabilityChecker::new(table.clone()));
965 let topic_router = Arc::new(TopicRouter::new(table.clone()));
966 let router = A2ARouter::new(table, checker, topic_router);
967 let _rx1 = router.create_inbox(sender_pid);
968 let _rx2 = router.create_inbox(target_pid);
969
970 let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "blocked");
971 let result = router.send(msg).await;
972 assert!(result.is_err());
973 }
974
975 #[test]
976 fn create_and_remove_inbox() {
977 let table = Arc::new(ProcessTable::new(64));
978 let checker = Arc::new(CapabilityChecker::new(table.clone()));
979 let topic_router = Arc::new(TopicRouter::new(table.clone()));
980 let router = A2ARouter::new(table, checker, topic_router);
981
982 let _rx = router.create_inbox(42);
983 assert!(router.has_inbox(42));
984 assert_eq!(router.inbox_count(), 1);
985
986 router.remove_inbox(42);
987 assert!(!router.has_inbox(42));
988 assert_eq!(router.inbox_count(), 0);
989 }
990
991 #[tokio::test]
992 async fn tool_call_message_routes() {
993 let (router, pids, mut receivers) = setup_router(2);
994
995 let msg = KernelMessage::tool_call(
996 pids[0],
997 MessageTarget::Process(pids[1]),
998 "read_file",
999 serde_json::json!({"path": "/test"}),
1000 );
1001 router.send(msg).await.unwrap();
1002
1003 let received = receivers[1].try_recv().unwrap();
1004 assert!(matches!(
1005 received.payload,
1006 MessagePayload::ToolCall { ref name, .. } if name == "read_file"
1007 ));
1008 }
1009
1010 #[tokio::test]
1011 async fn tool_result_message_routes() {
1012 let (router, pids, mut receivers) = setup_router(2);
1013
1014 let msg = KernelMessage::tool_result(
1015 pids[1],
1016 MessageTarget::Process(pids[0]),
1017 "call-1",
1018 serde_json::json!({"content": "data"}),
1019 );
1020 router.send(msg).await.unwrap();
1021
1022 let received = receivers[0].try_recv().unwrap();
1023 assert!(matches!(
1024 received.payload,
1025 MessagePayload::ToolResult { ref call_id, .. } if call_id == "call-1"
1026 ));
1027 }
1028
1029 #[cfg(feature = "exochain")]
1030 #[tokio::test]
1031 async fn send_checked_logs_chain_event() {
1032 let (router, pids, mut receivers) = setup_router(2);
1033
1034 let chain = crate::chain::ChainManager::new(0, 1000);
1035 let initial_seq = chain.sequence();
1036
1037 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "audited");
1038 router.send_checked(msg, Some(&chain)).await.unwrap();
1039
1040 let received = receivers[1].try_recv().unwrap();
1042 assert!(matches!(
1043 received.payload,
1044 MessagePayload::Text(ref t) if t == "audited"
1045 ));
1046
1047 assert_eq!(chain.sequence(), initial_seq + 1);
1049 let events = chain.tail(1);
1050 assert_eq!(events[0].kind, "ipc.send");
1051 assert_eq!(events[0].source, "ipc");
1052 let payload = events[0].payload.as_ref().unwrap();
1053 assert_eq!(payload["from"], pids[0]);
1054 assert_eq!(payload["payload_type"], "text");
1055 }
1056
1057 #[cfg(feature = "exochain")]
1058 #[tokio::test]
1059 async fn send_checked_without_chain_still_delivers() {
1060 let (router, pids, mut receivers) = setup_router(2);
1061
1062 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "no-chain");
1063 router.send_checked(msg, None).await.unwrap();
1064
1065 let received = receivers[1].try_recv().unwrap();
1066 assert!(matches!(
1067 received.payload,
1068 MessagePayload::Text(ref t) if t == "no-chain"
1069 ));
1070 }
1071
1072 #[tokio::test]
1074 async fn restricted_scope_cannot_publish_leaf_push_topic() {
1075 use crate::capability::IpcScope;
1076 use crate::process::ProcessTable;
1077
1078 let table = Arc::new(ProcessTable::new(16));
1079 let entry = ProcessEntry {
1080 pid: 0,
1081 agent_id: "browser-like".into(),
1082 state: ProcessState::Running,
1083 capabilities: AgentCapabilities {
1084 ipc_scope: IpcScope::Restricted(vec![]),
1085 ..AgentCapabilities::default()
1086 },
1087 resource_usage: ResourceUsage::default(),
1088 cancel_token: CancellationToken::new(),
1089 parent_pid: None,
1090 };
1091 let pid = table.insert(entry).unwrap();
1092 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1093 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1094 let router = A2ARouter::new(table, checker, topic_router);
1095 router.create_inbox(pid);
1096
1097 let leaf_topic = "mesh.leaf.deadbeef.push";
1098 let msg = KernelMessage::text(pid, MessageTarget::Topic(leaf_topic.into()), "leaf");
1099 let err = router
1100 .send(msg)
1101 .await
1102 .expect_err("Restricted must not publish mesh.leaf.*.push");
1103 assert!(
1104 matches!(err, KernelError::CapabilityDenied { .. }),
1105 "expected CapabilityDenied, got {err:?}"
1106 );
1107
1108 let ok_msg =
1110 KernelMessage::text(pid, MessageTarget::Topic("public.events".into()), "ok");
1111 router
1112 .send(ok_msg)
1113 .await
1114 .expect("Restricted may publish public.*");
1115 }
1116
1117 #[tokio::test]
1119 async fn all_scope_can_publish_leaf_push_topic() {
1120 let (router, pids, _receivers) = setup_router(1);
1121 let msg = KernelMessage::text(
1122 pids[0],
1123 MessageTarget::Topic("mesh.leaf.abc123.push".into()),
1124 "scene",
1125 );
1126 router
1127 .send(msg)
1128 .await
1129 .expect("IpcScope::All may publish leaf-push topics");
1130 }
1131
1132 #[tokio::test]
1133 async fn rvf_payload_routes() {
1134 let (router, pids, mut receivers) = setup_router(2);
1135
1136 let msg = KernelMessage::new(
1137 pids[0],
1138 MessageTarget::Process(pids[1]),
1139 MessagePayload::Rvf {
1140 segment_type: 0x40,
1141 data: vec![0xCA, 0xFE],
1142 },
1143 );
1144 router.send(msg).await.unwrap();
1145
1146 let received = receivers[1].try_recv().unwrap();
1147 assert!(matches!(
1148 received.payload,
1149 MessagePayload::Rvf {
1150 segment_type: 0x40,
1151 ..
1152 }
1153 ));
1154 }
1155
1156 #[tokio::test]
1157 async fn closed_inbox_auto_removed() {
1158 let (router, pids, receivers) = setup_router(2);
1159
1160 drop(receivers);
1162
1163 assert!(router.has_inbox(pids[1]));
1164
1165 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "gone");
1167 let result = router.send(msg).await;
1168 assert!(result.is_err());
1169 let err_msg = format!("{}", result.unwrap_err());
1170 assert!(
1171 err_msg.contains("inbox closed"),
1172 "expected 'inbox closed', got: {err_msg}"
1173 );
1174
1175 assert!(!router.has_inbox(pids[1]));
1177 }
1178
1179 #[tokio::test]
1180 async fn inbox_overflow_returns_error() {
1181 let table = Arc::new(ProcessTable::new(64));
1185
1186 let sender_entry = ProcessEntry {
1187 pid: 0,
1188 agent_id: "sender".to_owned(),
1189 state: ProcessState::Running,
1190 capabilities: AgentCapabilities::default(),
1191 resource_usage: ResourceUsage::default(),
1192 cancel_token: CancellationToken::new(),
1193 parent_pid: None,
1194 };
1195 let sender_pid = table.insert(sender_entry).unwrap();
1196
1197 let target_entry = ProcessEntry {
1198 pid: 0,
1199 agent_id: "target".to_owned(),
1200 state: ProcessState::Running,
1201 capabilities: AgentCapabilities::default(),
1202 resource_usage: ResourceUsage::default(),
1203 cancel_token: CancellationToken::new(),
1204 parent_pid: None,
1205 };
1206 let target_pid = table.insert(target_entry).unwrap();
1207
1208 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1209 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1210 let router = A2ARouter::new(table, checker, topic_router);
1211
1212 let _rx_sender = router.create_inbox(sender_pid);
1214
1215 let (tx, _rx_target) = mpsc::channel(2);
1217 router.inboxes.insert(target_pid, tx);
1218
1219 let m1 = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "msg1");
1221 let m2 = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "msg2");
1222 router.send(m1).await.unwrap();
1223 router.send(m2).await.unwrap();
1224
1225 let m3 = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "overflow");
1227 let result = router.send(m3).await;
1228 assert!(result.is_err());
1229 let err_msg = format!("{}", result.unwrap_err());
1230 assert!(
1231 err_msg.contains("inbox full"),
1232 "expected 'inbox full', got: {err_msg}"
1233 );
1234
1235 assert!(router.has_inbox(target_pid));
1237 }
1238
1239 #[tokio::test]
1240 async fn concurrent_sends_to_same_pid() {
1241 let (router, pids, mut receivers) = setup_router(4);
1242 let router = Arc::new(router);
1243 let target = pids[0];
1244
1245 let mut handles = Vec::new();
1247 for &sender_pid in &pids[1..] {
1248 let r = Arc::clone(&router);
1249 let msg = KernelMessage::text(
1250 sender_pid,
1251 MessageTarget::Process(target),
1252 format!("from-{sender_pid}"),
1253 );
1254 handles.push(tokio::spawn(async move { r.send(msg).await }));
1255 }
1256
1257 for h in handles {
1259 h.await.unwrap().unwrap();
1260 }
1261
1262 let mut received = Vec::new();
1264 while let Ok(msg) = receivers[0].try_recv() {
1265 if let MessagePayload::Text(t) = &msg.payload {
1266 received.push(t.clone());
1267 }
1268 }
1269 assert_eq!(received.len(), 3);
1270 for &sender_pid in &pids[1..] {
1272 assert!(
1273 received.iter().any(|t| t == &format!("from-{sender_pid}")),
1274 "missing message from PID {sender_pid}"
1275 );
1276 }
1277 }
1278
1279 #[tokio::test]
1280 async fn send_from_non_running_process_fails() {
1281 let table = Arc::new(ProcessTable::new(64));
1282
1283 let sender_entry = ProcessEntry {
1285 pid: 0,
1286 agent_id: "exited-sender".to_owned(),
1287 state: ProcessState::Exited(0),
1288 capabilities: AgentCapabilities::default(),
1289 resource_usage: ResourceUsage::default(),
1290 cancel_token: CancellationToken::new(),
1291 parent_pid: None,
1292 };
1293 let sender_pid = table.insert(sender_entry).unwrap();
1294
1295 let target_entry = ProcessEntry {
1297 pid: 0,
1298 agent_id: "target".to_owned(),
1299 state: ProcessState::Running,
1300 capabilities: AgentCapabilities::default(),
1301 resource_usage: ResourceUsage::default(),
1302 cancel_token: CancellationToken::new(),
1303 parent_pid: None,
1304 };
1305 let target_pid = table.insert(target_entry).unwrap();
1306
1307 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1308 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1309 let router = A2ARouter::new(table, checker, topic_router);
1310 let _rx1 = router.create_inbox(sender_pid);
1311 let _rx2 = router.create_inbox(target_pid);
1312
1313 let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "from-dead");
1314 let result = router.send(msg).await;
1315 assert!(result.is_err(), "send from non-Running process should fail");
1316 }
1317
1318 fn setup_router_with_registry(
1321 agent_count: usize,
1322 ) -> (
1323 A2ARouter,
1324 Vec<Pid>,
1325 Vec<mpsc::Receiver<KernelMessage>>,
1326 Arc<crate::service::ServiceRegistry>,
1327 ) {
1328 let table = Arc::new(ProcessTable::new(64));
1329 let mut pids = Vec::new();
1330
1331 for i in 0..agent_count {
1332 let entry = ProcessEntry {
1333 pid: 0,
1334 agent_id: format!("agent-{i}"),
1335 state: ProcessState::Running,
1336 capabilities: AgentCapabilities::default(),
1337 resource_usage: ResourceUsage::default(),
1338 cancel_token: CancellationToken::new(),
1339 parent_pid: None,
1340 };
1341 let pid = table.insert(entry).unwrap();
1342 pids.push(pid);
1343 }
1344
1345 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1346 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1347 let registry = Arc::new(crate::service::ServiceRegistry::new());
1348 let router =
1349 A2ARouter::new(table, checker, topic_router).with_service_registry(registry.clone());
1350
1351 let mut receivers = Vec::new();
1352 for &pid in &pids {
1353 let rx = router.create_inbox(pid);
1354 receivers.push(rx);
1355 }
1356
1357 (router, pids, receivers, registry)
1358 }
1359
1360 #[tokio::test]
1361 async fn route_to_service_by_name() {
1362 let (router, pids, mut receivers, registry) = setup_router_with_registry(2);
1363
1364 registry
1366 .register_entry(crate::service::ServiceEntry {
1367 name: "auth".into(),
1368 owner_pid: Some(pids[1]),
1369 endpoint: crate::service::ServiceEndpoint::AgentInbox(pids[1]),
1370 audit_level: crate::service::ServiceAuditLevel::Full,
1371 registered_at: chrono::Utc::now(),
1372 })
1373 .unwrap();
1374
1375 let msg = KernelMessage::text(pids[0], MessageTarget::Service("auth".into()), "validate");
1376 router.send(msg).await.unwrap();
1377
1378 let received = receivers[1].try_recv().unwrap();
1379 assert_eq!(received.from, pids[0]);
1380 assert!(matches!(
1381 received.payload,
1382 MessagePayload::Text(ref t) if t == "validate"
1383 ));
1384 }
1385
1386 #[tokio::test]
1387 async fn route_service_method() {
1388 let (router, pids, mut receivers, registry) = setup_router_with_registry(2);
1389
1390 registry
1391 .register_entry(crate::service::ServiceEntry {
1392 name: "auth".into(),
1393 owner_pid: Some(pids[1]),
1394 endpoint: crate::service::ServiceEndpoint::AgentInbox(pids[1]),
1395 audit_level: crate::service::ServiceAuditLevel::Full,
1396 registered_at: chrono::Utc::now(),
1397 })
1398 .unwrap();
1399
1400 let msg = KernelMessage::text(
1401 pids[0],
1402 MessageTarget::ServiceMethod {
1403 service: "auth".into(),
1404 method: "validate_token".into(),
1405 },
1406 "token-123",
1407 );
1408 router.send(msg).await.unwrap();
1409
1410 let received = receivers[1].try_recv().unwrap();
1411 assert_eq!(received.from, pids[0]);
1412 assert!(matches!(
1413 received.target,
1414 MessageTarget::ServiceMethod { ref service, ref method }
1415 if service == "auth" && method == "validate_token"
1416 ));
1417 }
1418
1419 #[tokio::test]
1420 async fn service_not_found_returns_error() {
1421 let (router, pids, _receivers, _registry) = setup_router_with_registry(2);
1422
1423 let msg = KernelMessage::text(
1424 pids[0],
1425 MessageTarget::Service("nonexistent".into()),
1426 "hello",
1427 );
1428 let result = router.send(msg).await;
1429 assert!(result.is_err());
1430 let err_msg = format!("{}", result.unwrap_err());
1431 assert!(
1432 err_msg.contains("service not found"),
1433 "expected 'service not found', got: {err_msg}"
1434 );
1435 }
1436
1437 #[tokio::test]
1438 async fn service_entry_registration() {
1439 let registry = crate::service::ServiceRegistry::new();
1440
1441 let entry = crate::service::ServiceEntry {
1442 name: "cache".into(),
1443 owner_pid: Some(42),
1444 endpoint: crate::service::ServiceEndpoint::AgentInbox(42),
1445 audit_level: crate::service::ServiceAuditLevel::Full,
1446 registered_at: chrono::Utc::now(),
1447 };
1448 registry.register_entry(entry).unwrap();
1449
1450 let retrieved = registry.get_entry("cache").unwrap();
1451 assert_eq!(retrieved.name, "cache");
1452 assert_eq!(retrieved.owner_pid, Some(42));
1453 }
1454
1455 #[tokio::test]
1456 async fn service_entry_with_audit_level() {
1457 let registry = crate::service::ServiceRegistry::new();
1458
1459 let entry = crate::service::ServiceEntry {
1460 name: "metrics".into(),
1461 owner_pid: Some(10),
1462 endpoint: crate::service::ServiceEndpoint::AgentInbox(10),
1463 audit_level: crate::service::ServiceAuditLevel::GateOnly,
1464 registered_at: chrono::Utc::now(),
1465 };
1466 registry.register_entry(entry).unwrap();
1467
1468 let retrieved = registry.get_entry("metrics").unwrap();
1469 assert_eq!(
1470 retrieved.audit_level,
1471 crate::service::ServiceAuditLevel::GateOnly
1472 );
1473 }
1474
1475 #[tokio::test]
1476 async fn resolve_target_finds_owner_pid() {
1477 let registry = crate::service::ServiceRegistry::new();
1478
1479 let entry = crate::service::ServiceEntry {
1480 name: "search".into(),
1481 owner_pid: Some(77),
1482 endpoint: crate::service::ServiceEndpoint::AgentInbox(77),
1483 audit_level: crate::service::ServiceAuditLevel::Full,
1484 registered_at: chrono::Utc::now(),
1485 };
1486 registry.register_entry(entry).unwrap();
1487
1488 assert_eq!(registry.resolve_target("search"), Some(77));
1489 assert_eq!(registry.resolve_target("nonexistent"), None);
1490 }
1491
1492 #[tokio::test]
1493 async fn service_without_registry_returns_error() {
1494 let (router_no_reg, pids, _receivers) = setup_router(2);
1496
1497 let msg = KernelMessage::text(pids[0], MessageTarget::Service("missing".into()), "hello");
1498 let result = router_no_reg.send(msg).await;
1499 assert!(result.is_err());
1500 let err_msg = format!("{}", result.unwrap_err());
1501 assert!(
1502 err_msg.contains("no service registry"),
1503 "expected 'no service registry', got: {err_msg}"
1504 );
1505 }
1506
1507 #[tokio::test]
1508 async fn external_service_no_pid_returns_error() {
1509 let (router, pids, _receivers, registry) = setup_router_with_registry(2);
1510
1511 registry
1513 .register_entry(crate::service::ServiceEntry {
1514 name: "redis".into(),
1515 owner_pid: None,
1516 endpoint: crate::service::ServiceEndpoint::External {
1517 url: "redis://localhost:6379".into(),
1518 },
1519 audit_level: crate::service::ServiceAuditLevel::GateOnly,
1520 registered_at: chrono::Utc::now(),
1521 })
1522 .unwrap();
1523
1524 let msg = KernelMessage::text(pids[0], MessageTarget::Service("redis".into()), "ping");
1525 let result = router.send(msg).await;
1526 assert!(
1527 result.is_err(),
1528 "external service with no PID should fail routing"
1529 );
1530 }
1531
1532 #[tokio::test]
1535 async fn request_response_completes() {
1536 let (router, pids, mut receivers) = setup_router(2);
1537 let router = Arc::new(router);
1538 let router2 = Arc::clone(&router);
1539
1540 let from_pid = pids[0];
1542 let to_pid = pids[1];
1543 tokio::spawn(async move {
1544 let msg = receivers[1].recv().await.unwrap();
1545 let reply = KernelMessage::with_correlation(
1547 to_pid,
1548 MessageTarget::Process(from_pid),
1549 MessagePayload::Text("pong".into()),
1550 msg.id.clone(),
1551 );
1552 router2.try_complete_request(reply);
1553 });
1554
1555 let request = KernelMessage::text(from_pid, MessageTarget::Process(to_pid), "ping");
1556 let response = router
1557 .request(request, Duration::from_secs(5))
1558 .await
1559 .unwrap();
1560 assert!(matches!(
1561 response.payload,
1562 MessagePayload::Text(ref t) if t == "pong"
1563 ));
1564 assert_eq!(router.pending_request_count(), 0);
1565 }
1566
1567 #[tokio::test]
1568 async fn request_response_timeout() {
1569 let (router, pids, _receivers) = setup_router(2);
1570
1571 let request = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "no-reply");
1572 let result = router.request(request, Duration::from_millis(50)).await;
1573 assert!(result.is_err());
1574 let err_msg = format!("{}", result.unwrap_err());
1575 assert!(
1576 err_msg.contains("timeout"),
1577 "expected timeout error, got: {err_msg}"
1578 );
1579 assert_eq!(router.pending_request_count(), 0);
1580 }
1581
1582 #[tokio::test]
1583 async fn try_complete_request_matching() {
1584 let (router, pids, _receivers) = setup_router(2);
1585
1586 let (tx, rx) = oneshot::channel();
1588 let request_id = "req-42".to_string();
1589 router.pending_requests.insert(
1590 request_id.clone(),
1591 PendingRequest {
1592 response_tx: tx,
1593 sent_at: Instant::now(),
1594 },
1595 );
1596
1597 let reply = KernelMessage::with_correlation(
1599 pids[1],
1600 MessageTarget::Process(pids[0]),
1601 MessagePayload::Text("reply".into()),
1602 request_id,
1603 );
1604 let completed = router.try_complete_request(reply);
1605 assert!(
1606 completed,
1607 "try_complete_request should return true for matching id"
1608 );
1609
1610 let response = rx.await.unwrap();
1611 assert!(matches!(
1612 response.payload,
1613 MessagePayload::Text(ref t) if t == "reply"
1614 ));
1615 }
1616
1617 #[tokio::test]
1618 async fn try_complete_request_no_match() {
1619 let (router, pids, _receivers) = setup_router(2);
1620
1621 let reply = KernelMessage::with_correlation(
1623 pids[1],
1624 MessageTarget::Process(pids[0]),
1625 MessagePayload::Text("orphan".into()),
1626 "nonexistent-id".into(),
1627 );
1628 let completed = router.try_complete_request(reply);
1629 assert!(
1630 !completed,
1631 "try_complete_request should return false for non-matching id"
1632 );
1633
1634 let plain = KernelMessage::text(pids[1], MessageTarget::Process(pids[0]), "plain");
1636 assert!(!router.try_complete_request(plain));
1637 }
1638
1639 #[tokio::test]
1640 async fn pending_request_count_tracks_correctly() {
1641 let (router, _pids, _receivers) = setup_router(2);
1642 assert_eq!(router.pending_request_count(), 0);
1643
1644 let (tx1, _rx1) = oneshot::channel();
1646 let (tx2, _rx2) = oneshot::channel();
1647 router.pending_requests.insert(
1648 "req-a".into(),
1649 PendingRequest {
1650 response_tx: tx1,
1651 sent_at: Instant::now(),
1652 },
1653 );
1654 assert_eq!(router.pending_request_count(), 1);
1655
1656 router.pending_requests.insert(
1657 "req-b".into(),
1658 PendingRequest {
1659 response_tx: tx2,
1660 sent_at: Instant::now(),
1661 },
1662 );
1663 assert_eq!(router.pending_request_count(), 2);
1664
1665 let reply = KernelMessage::with_correlation(
1667 1,
1668 MessageTarget::Process(2),
1669 MessagePayload::Text("done".into()),
1670 "req-a".into(),
1671 );
1672 router.try_complete_request(reply);
1673 assert_eq!(router.pending_request_count(), 1);
1674
1675 router.pending_requests.remove("req-b");
1677 assert_eq!(router.pending_request_count(), 0);
1678 }
1679
1680 #[cfg(feature = "exochain")]
1683 #[tokio::test]
1684 async fn routing_gate_denies_message() {
1685 struct DenyGate;
1686 impl crate::gate::GateBackend for DenyGate {
1687 fn check(
1688 &self,
1689 _agent: &str,
1690 _action: &str,
1691 _ctx: &serde_json::Value,
1692 ) -> crate::gate::GateDecision {
1693 crate::gate::GateDecision::Deny {
1694 reason: "blocked by policy".into(),
1695 receipt: None,
1696 }
1697 }
1698 }
1699
1700 let (router, pids, _receivers) = setup_router(2);
1701 let router = router.with_gate(Arc::new(DenyGate));
1702
1703 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "hello");
1704 let result = router.send(msg).await;
1705 assert!(result.is_err());
1706 let err_msg = format!("{}", result.unwrap_err());
1707 assert!(
1708 err_msg.contains("routing gate denied"),
1709 "expected 'routing gate denied', got: {err_msg}"
1710 );
1711 }
1712
1713 #[cfg(feature = "exochain")]
1714 #[tokio::test]
1715 async fn routing_gate_permits_message() {
1716 struct PermitGate;
1717 impl crate::gate::GateBackend for PermitGate {
1718 fn check(
1719 &self,
1720 _agent: &str,
1721 _action: &str,
1722 _ctx: &serde_json::Value,
1723 ) -> crate::gate::GateDecision {
1724 crate::gate::GateDecision::Permit { token: None }
1725 }
1726 }
1727
1728 let (router, pids, mut receivers) = setup_router(2);
1729 let router = router.with_gate(Arc::new(PermitGate));
1730
1731 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "hello");
1732 router.send(msg).await.unwrap();
1733
1734 let received = receivers[1].try_recv().unwrap();
1735 assert_eq!(received.from, pids[0]);
1736 assert!(matches!(
1737 received.payload,
1738 MessagePayload::Text(ref t) if t == "hello"
1739 ));
1740 }
1741
1742 #[cfg(feature = "exochain")]
1743 #[tokio::test]
1744 async fn routing_gate_defer_still_delivers() {
1745 struct DeferGate;
1746 impl crate::gate::GateBackend for DeferGate {
1747 fn check(
1748 &self,
1749 _agent: &str,
1750 _action: &str,
1751 _ctx: &serde_json::Value,
1752 ) -> crate::gate::GateDecision {
1753 crate::gate::GateDecision::Defer {
1754 reason: "pending review".into(),
1755 }
1756 }
1757 }
1758
1759 let (router, pids, mut receivers) = setup_router(2);
1760 let router = router.with_gate(Arc::new(DeferGate));
1761
1762 let msg = KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), "deferred");
1763 router.send(msg).await.unwrap(); assert!(receivers[1].try_recv().is_ok());
1765 }
1766
1767 #[tokio::test]
1770 async fn multiple_messages_queued_in_order() {
1771 let (router, pids, mut receivers) = setup_router(2);
1772
1773 for i in 0..5 {
1774 let msg =
1775 KernelMessage::text(pids[0], MessageTarget::Process(pids[1]), format!("msg-{i}"));
1776 router.send(msg).await.unwrap();
1777 }
1778
1779 for i in 0..5 {
1781 let received = receivers[1].try_recv().unwrap();
1782 let expected = format!("msg-{i}");
1783 assert!(
1784 matches!(received.payload, MessagePayload::Text(ref t) if t == &expected),
1785 "expected '{expected}' but got {:?}",
1786 received.payload,
1787 );
1788 }
1789 }
1790
1791 #[tokio::test]
1792 async fn unsubscribe_stops_topic_delivery() {
1793 let (router, pids, mut receivers) = setup_router(3);
1794
1795 router.topic_router().subscribe(pids[1], "events");
1796 router.topic_router().subscribe(pids[2], "events");
1797
1798 router.topic_router().unsubscribe(pids[1], "events");
1800
1801 let msg = KernelMessage::text(
1802 pids[0],
1803 MessageTarget::Topic("events".into()),
1804 "after-unsub",
1805 );
1806 router.send(msg).await.unwrap();
1807
1808 assert!(
1810 receivers[1].try_recv().is_err(),
1811 "unsubscribed agent should not receive"
1812 );
1813
1814 let received = receivers[2].try_recv().unwrap();
1816 assert!(matches!(
1817 received.payload,
1818 MessagePayload::Text(ref t) if t == "after-unsub"
1819 ));
1820 }
1821
1822 #[tokio::test]
1823 async fn publish_to_empty_topic_succeeds() {
1824 let (router, pids, _receivers) = setup_router(2);
1825
1826 let msg = KernelMessage::text(
1828 pids[0],
1829 MessageTarget::Topic("empty-topic".into()),
1830 "nobody-listening",
1831 );
1832 let result = router.send(msg).await;
1833 assert!(result.is_ok(), "publish to empty topic should succeed");
1834 }
1835
1836 #[tokio::test]
1837 async fn broadcast_with_zero_other_agents() {
1838 let (router, pids, mut receivers) = setup_router(1);
1839
1840 let msg = KernelMessage::text(pids[0], MessageTarget::Broadcast, "alone");
1841 let result = router.send(msg).await;
1842 assert!(result.is_ok(), "broadcast with only sender should succeed");
1843
1844 assert!(receivers[0].try_recv().is_err());
1846 }
1847
1848 #[tokio::test]
1849 async fn ipc_scope_restricted_allows_listed_pids() {
1850 let table = Arc::new(ProcessTable::new(64));
1851
1852 use crate::capability::IpcScope;
1853 let target_entry = ProcessEntry {
1855 pid: 0,
1856 agent_id: "target".to_owned(),
1857 state: ProcessState::Running,
1858 capabilities: AgentCapabilities::default(),
1859 resource_usage: ResourceUsage::default(),
1860 cancel_token: CancellationToken::new(),
1861 parent_pid: None,
1862 };
1863 let target_pid = table.insert(target_entry).unwrap();
1864
1865 let sender_entry = ProcessEntry {
1867 pid: 0,
1868 agent_id: "restricted-sender".to_owned(),
1869 state: ProcessState::Running,
1870 capabilities: AgentCapabilities {
1871 ipc_scope: IpcScope::Restricted(vec![target_pid]),
1872 ..Default::default()
1873 },
1874 resource_usage: ResourceUsage::default(),
1875 cancel_token: CancellationToken::new(),
1876 parent_pid: None,
1877 };
1878 let sender_pid = table.insert(sender_entry).unwrap();
1879
1880 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1881 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1882 let router = A2ARouter::new(table, checker, topic_router);
1883 let _rx_sender = router.create_inbox(sender_pid);
1884 let mut rx_target = router.create_inbox(target_pid);
1885
1886 let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "allowed");
1887 let result = router.send(msg).await;
1888 assert!(result.is_ok(), "restricted sender should reach allowed PID");
1889
1890 let received = rx_target.try_recv().unwrap();
1891 assert!(matches!(
1892 received.payload,
1893 MessagePayload::Text(ref t) if t == "allowed"
1894 ));
1895 }
1896
1897 #[tokio::test]
1898 async fn ipc_scope_restricted_blocks_unlisted_pids() {
1899 let table = Arc::new(ProcessTable::new(64));
1900
1901 use crate::capability::IpcScope;
1902 let target_entry = ProcessEntry {
1903 pid: 0,
1904 agent_id: "target".to_owned(),
1905 state: ProcessState::Running,
1906 capabilities: AgentCapabilities::default(),
1907 resource_usage: ResourceUsage::default(),
1908 cancel_token: CancellationToken::new(),
1909 parent_pid: None,
1910 };
1911 let target_pid = table.insert(target_entry).unwrap();
1912
1913 let sender_entry = ProcessEntry {
1915 pid: 0,
1916 agent_id: "restricted-sender".to_owned(),
1917 state: ProcessState::Running,
1918 capabilities: AgentCapabilities {
1919 ipc_scope: IpcScope::Restricted(vec![999]),
1920 ..Default::default()
1921 },
1922 resource_usage: ResourceUsage::default(),
1923 cancel_token: CancellationToken::new(),
1924 parent_pid: None,
1925 };
1926 let sender_pid = table.insert(sender_entry).unwrap();
1927
1928 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1929 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1930 let router = A2ARouter::new(table, checker, topic_router);
1931 let _rx_sender = router.create_inbox(sender_pid);
1932 let _rx_target = router.create_inbox(target_pid);
1933
1934 let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "blocked");
1935 let result = router.send(msg).await;
1936 assert!(
1937 result.is_err(),
1938 "restricted sender should be blocked from unlisted PID"
1939 );
1940 }
1941
1942 #[test]
1943 fn create_inbox_returns_receiver() {
1944 let table = Arc::new(ProcessTable::new(64));
1945 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1946 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1947 let router = A2ARouter::new(table, checker, topic_router);
1948
1949 let rx = router.create_inbox(10);
1950 assert!(router.has_inbox(10));
1951 assert_eq!(router.inbox_count(), 1);
1952 drop(rx);
1953 }
1954
1955 #[test]
1956 fn create_inbox_replaces_existing() {
1957 let table = Arc::new(ProcessTable::new(64));
1958 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1959 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1960 let router = A2ARouter::new(table, checker, topic_router);
1961
1962 let _rx1 = router.create_inbox(10);
1963 assert_eq!(router.inbox_count(), 1);
1964
1965 let _rx2 = router.create_inbox(10);
1967 assert_eq!(router.inbox_count(), 1);
1968 assert!(router.has_inbox(10));
1969 }
1970
1971 #[test]
1972 fn remove_nonexistent_inbox_is_noop() {
1973 let table = Arc::new(ProcessTable::new(64));
1974 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1975 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1976 let router = A2ARouter::new(table, checker, topic_router);
1977
1978 router.remove_inbox(999);
1980 assert_eq!(router.inbox_count(), 0);
1981 }
1982
1983 #[test]
1984 fn inbox_count_tracks_multiple_inboxes() {
1985 let table = Arc::new(ProcessTable::new(64));
1986 let checker = Arc::new(CapabilityChecker::new(table.clone()));
1987 let topic_router = Arc::new(TopicRouter::new(table.clone()));
1988 let router = A2ARouter::new(table, checker, topic_router);
1989
1990 let _rx1 = router.create_inbox(1);
1991 let _rx2 = router.create_inbox(2);
1992 let _rx3 = router.create_inbox(3);
1993 assert_eq!(router.inbox_count(), 3);
1994
1995 router.remove_inbox(2);
1996 assert_eq!(router.inbox_count(), 2);
1997 assert!(!router.has_inbox(2));
1998 assert!(router.has_inbox(1));
1999 assert!(router.has_inbox(3));
2000 }
2001
2002 #[tokio::test]
2003 async fn send_to_kernel_target_succeeds() {
2004 let (router, pids, _receivers) = setup_router(1);
2005
2006 let msg = KernelMessage::text(pids[0], MessageTarget::Kernel, "kernel-msg");
2007 let result = router.send(msg).await;
2008 assert!(
2009 result.is_ok(),
2010 "kernel target routing should succeed (even if no-op)"
2011 );
2012 }
2013
2014 #[tokio::test]
2015 async fn send_to_remote_node_fails() {
2016 let (router, pids, _receivers) = setup_router(1);
2017
2018 let msg = KernelMessage::text(
2019 pids[0],
2020 MessageTarget::RemoteNode {
2021 node_id: "remote-1".into(),
2022 target: Box::new(MessageTarget::Process(42)),
2023 },
2024 "remote-msg",
2025 );
2026 let result = router.send(msg).await;
2027 assert!(
2028 result.is_err(),
2029 "remote node routing should fail (not yet implemented)"
2030 );
2031 let err_msg = format!("{}", result.unwrap_err());
2032 assert!(
2033 err_msg.contains("remote routing"),
2034 "expected remote routing error, got: {err_msg}"
2035 );
2036 }
2037
2038 #[tokio::test]
2039 async fn suspended_process_can_still_send() {
2040 let table = Arc::new(ProcessTable::new(64));
2041
2042 let sender_entry = ProcessEntry {
2043 pid: 0,
2044 agent_id: "suspended-sender".to_owned(),
2045 state: ProcessState::Suspended,
2046 capabilities: AgentCapabilities::default(),
2047 resource_usage: ResourceUsage::default(),
2048 cancel_token: CancellationToken::new(),
2049 parent_pid: None,
2050 };
2051 let sender_pid = table.insert(sender_entry).unwrap();
2052
2053 let target_entry = ProcessEntry {
2054 pid: 0,
2055 agent_id: "target".to_owned(),
2056 state: ProcessState::Running,
2057 capabilities: AgentCapabilities::default(),
2058 resource_usage: ResourceUsage::default(),
2059 cancel_token: CancellationToken::new(),
2060 parent_pid: None,
2061 };
2062 let target_pid = table.insert(target_entry).unwrap();
2063
2064 let checker = Arc::new(CapabilityChecker::new(table.clone()));
2065 let topic_router = Arc::new(TopicRouter::new(table.clone()));
2066 let router = A2ARouter::new(table, checker, topic_router);
2067 let _rx_sender = router.create_inbox(sender_pid);
2068 let mut rx_target = router.create_inbox(target_pid);
2069
2070 let msg = KernelMessage::text(
2071 sender_pid,
2072 MessageTarget::Process(target_pid),
2073 "from-suspended",
2074 );
2075 let result = router.send(msg).await;
2076 assert!(
2077 result.is_ok(),
2078 "suspended process should be allowed to send"
2079 );
2080
2081 let received = rx_target.try_recv().unwrap();
2082 assert!(matches!(
2083 received.payload,
2084 MessagePayload::Text(ref t) if t == "from-suspended"
2085 ));
2086 }
2087
2088 #[tokio::test]
2089 async fn topic_multiple_subscribers_receive_same_message() {
2090 let (router, pids, mut receivers) = setup_router(5);
2091
2092 for &pid in &pids[1..] {
2094 router.topic_router().subscribe(pid, "news");
2095 }
2096
2097 let msg = KernelMessage::text(pids[0], MessageTarget::Topic("news".into()), "breaking");
2098 router.send(msg).await.unwrap();
2099
2100 for (i, rx) in receivers[1..].iter_mut().enumerate() {
2102 let received = rx.try_recv().unwrap_or_else(|_| {
2103 panic!(
2104 "subscriber {} (pid {}) should have received message",
2105 i + 1,
2106 pids[i + 1]
2107 )
2108 });
2109 assert!(matches!(
2110 received.payload,
2111 MessagePayload::Text(ref t) if t == "breaking"
2112 ));
2113 assert_eq!(received.from, pids[0]);
2114 }
2115 }
2116
2117 #[tokio::test]
2118 async fn broadcast_skips_scope_restricted_targets() {
2119 let table = Arc::new(ProcessTable::new(64));
2120
2121 use crate::capability::IpcScope;
2122 let sender_entry = ProcessEntry {
2124 pid: 0,
2125 agent_id: "restricted-broadcaster".to_owned(),
2126 state: ProcessState::Running,
2127 capabilities: AgentCapabilities {
2128 ipc_scope: IpcScope::Restricted(vec![]),
2129 ..Default::default()
2130 },
2131 resource_usage: ResourceUsage::default(),
2132 cancel_token: CancellationToken::new(),
2133 parent_pid: None,
2134 };
2135 let sender_pid = table.insert(sender_entry).unwrap();
2136
2137 let target_entry = ProcessEntry {
2138 pid: 0,
2139 agent_id: "target".to_owned(),
2140 state: ProcessState::Running,
2141 capabilities: AgentCapabilities::default(),
2142 resource_usage: ResourceUsage::default(),
2143 cancel_token: CancellationToken::new(),
2144 parent_pid: None,
2145 };
2146 let target_pid = table.insert(target_entry).unwrap();
2147
2148 let checker = Arc::new(CapabilityChecker::new(table.clone()));
2149 let topic_router = Arc::new(TopicRouter::new(table.clone()));
2150 let router = A2ARouter::new(table, checker, topic_router);
2151 let _rx_sender = router.create_inbox(sender_pid);
2152 let mut rx_target = router.create_inbox(target_pid);
2153
2154 let msg = KernelMessage::text(sender_pid, MessageTarget::Broadcast, "restricted-broadcast");
2155 let result = router.send(msg).await;
2156 assert!(
2157 result.is_ok(),
2158 "broadcast itself should succeed even if all targets are blocked"
2159 );
2160
2161 assert!(
2163 rx_target.try_recv().is_err(),
2164 "restricted broadcast should not reach any target"
2165 );
2166 }
2167
2168 #[test]
2169 fn service_registry_accessor() {
2170 let table = Arc::new(ProcessTable::new(64));
2171 let checker = Arc::new(CapabilityChecker::new(table.clone()));
2172 let topic_router = Arc::new(TopicRouter::new(table.clone()));
2173
2174 let router = A2ARouter::new(table.clone(), checker.clone(), topic_router.clone());
2175 assert!(
2176 router.service_registry().is_none(),
2177 "no registry by default"
2178 );
2179
2180 let registry = Arc::new(crate::service::ServiceRegistry::new());
2181 let router = router.with_service_registry(registry);
2182 assert!(
2183 router.service_registry().is_some(),
2184 "registry should be present after with_service_registry"
2185 );
2186 }
2187
2188 #[test]
2189 fn topic_router_accessor() {
2190 let table = Arc::new(ProcessTable::new(64));
2191 let checker = Arc::new(CapabilityChecker::new(table.clone()));
2192 let topic_router = Arc::new(TopicRouter::new(table.clone()));
2193 let expected = Arc::clone(&topic_router);
2194
2195 let router = A2ARouter::new(table, checker, topic_router);
2196 assert!(Arc::ptr_eq(router.topic_router(), &expected));
2197 }
2198
2199 #[tokio::test]
2200 async fn json_payload_routes() {
2201 let (router, pids, mut receivers) = setup_router(2);
2202
2203 let msg = KernelMessage::new(
2204 pids[0],
2205 MessageTarget::Process(pids[1]),
2206 MessagePayload::Json(serde_json::json!({"key": "value"})),
2207 );
2208 router.send(msg).await.unwrap();
2209
2210 let received = receivers[1].try_recv().unwrap();
2211 assert!(matches!(
2212 received.payload,
2213 MessagePayload::Json(ref v) if v["key"] == "value"
2214 ));
2215 }
2216
2217 #[tokio::test]
2218 async fn signal_payload_routes() {
2219 use crate::ipc::KernelSignal;
2220 let (router, pids, mut receivers) = setup_router(2);
2221
2222 let msg = KernelMessage::signal(
2223 pids[0],
2224 MessageTarget::Process(pids[1]),
2225 KernelSignal::Shutdown,
2226 );
2227 router.send(msg).await.unwrap();
2228
2229 let received = receivers[1].try_recv().unwrap();
2230 assert!(matches!(
2231 received.payload,
2232 MessagePayload::Signal(KernelSignal::Shutdown)
2233 ));
2234 }
2235
2236 #[tokio::test]
2237 async fn request_to_nonexistent_target_cleans_up_pending() {
2238 let table = Arc::new(ProcessTable::new(64));
2239
2240 let sender_entry = ProcessEntry {
2241 pid: 0,
2242 agent_id: "sender".to_owned(),
2243 state: ProcessState::Running,
2244 capabilities: AgentCapabilities::default(),
2245 resource_usage: ResourceUsage::default(),
2246 cancel_token: CancellationToken::new(),
2247 parent_pid: None,
2248 };
2249 let sender_pid = table.insert(sender_entry).unwrap();
2250
2251 let target_entry = ProcessEntry {
2253 pid: 0,
2254 agent_id: "no-inbox-target".to_owned(),
2255 state: ProcessState::Running,
2256 capabilities: AgentCapabilities::default(),
2257 resource_usage: ResourceUsage::default(),
2258 cancel_token: CancellationToken::new(),
2259 parent_pid: None,
2260 };
2261 let target_pid = table.insert(target_entry).unwrap();
2262
2263 let checker = Arc::new(CapabilityChecker::new(table.clone()));
2264 let topic_router = Arc::new(TopicRouter::new(table.clone()));
2265 let router = A2ARouter::new(table, checker, topic_router);
2266 let _rx_sender = router.create_inbox(sender_pid);
2267 let msg = KernelMessage::text(sender_pid, MessageTarget::Process(target_pid), "request");
2270 let result = router.request(msg, Duration::from_millis(100)).await;
2271 assert!(result.is_err(), "request to PID without inbox should fail");
2272 assert_eq!(
2273 router.pending_request_count(),
2274 0,
2275 "pending request should be cleaned up on send failure"
2276 );
2277 }
2278
2279 #[tokio::test]
2280 async fn service_method_not_found_returns_error() {
2281 let (router, pids, _receivers, _registry) = setup_router_with_registry(2);
2282
2283 let msg = KernelMessage::text(
2284 pids[0],
2285 MessageTarget::ServiceMethod {
2286 service: "nonexistent-svc".into(),
2287 method: "do_thing".into(),
2288 },
2289 "call",
2290 );
2291 let result = router.send(msg).await;
2292 assert!(result.is_err());
2293 let err_msg = format!("{}", result.unwrap_err());
2294 assert!(
2295 err_msg.contains("service not found"),
2296 "expected 'service not found' for missing ServiceMethod target, got: {err_msg}"
2297 );
2298 }
2299}