1use std::collections::HashMap;
23use std::sync::OnceLock;
24
25use parking_lot::RwLock;
26use uuid::Uuid;
27
28use crate::identity::{Keypair, PubKey, Signature};
29use crate::inbox::{AdmissionOutcome, DropReason, InboxSender};
30use crate::peer_meta::PeerMeta;
31use crate::types::{Envelope, InboxItem, MessageKind};
32
33const DEFAULT_NAMESPACE: &str = "";
34
35#[derive(Debug, Clone)]
37pub struct InprocPeerInfo {
38 pub name: String,
39 pub pubkey: PubKey,
40 pub meta: PeerMeta,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum RegistrationRejection {
46 ZeroPubkey,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum RegistrationOutcome {
60 Registered,
62 ReplacedPubkey { evicted_name: String },
65 EvictedName { evicted_pubkey: PubKey },
68 ReplacedPubkeyAndEvictedName {
71 evicted_name: String,
72 evicted_pubkey: PubKey,
73 },
74 Rejected { reason: RegistrationRejection },
76}
77
78impl RegistrationOutcome {
79 pub fn displaced_existing(&self) -> bool {
81 matches!(
82 self,
83 Self::ReplacedPubkey { .. }
84 | Self::EvictedName { .. }
85 | Self::ReplacedPubkeyAndEvictedName { .. }
86 )
87 }
88
89 pub fn is_rejected(&self) -> bool {
91 matches!(self, Self::Rejected { .. })
92 }
93}
94
95static GLOBAL_REGISTRY: OnceLock<InprocRegistry> = OnceLock::new();
97
98#[derive(Clone)]
100struct InprocPeer {
101 name: String,
102 pubkey: PubKey,
103 sender: InboxSender,
104 meta: PeerMeta,
105}
106
107#[derive(Default)]
109struct NamespaceState {
110 peers: HashMap<PubKey, InprocPeer>,
112 names: HashMap<String, PubKey>,
114}
115
116struct RegistryState {
118 namespaces: HashMap<String, NamespaceState>,
119}
120
121impl RegistryState {
122 fn namespace_mut(&mut self, namespace: &str) -> &mut NamespaceState {
123 self.namespaces.entry(namespace.to_string()).or_default()
124 }
125
126 fn namespace(&self, namespace: &str) -> Option<&NamespaceState> {
127 self.namespaces.get(namespace)
128 }
129
130 fn namespace_len(&self, namespace: &str) -> usize {
131 self.namespace(namespace).map_or(0, |ns| ns.peers.len())
132 }
133
134 fn namespace_is_empty(&self, namespace: &str) -> bool {
135 self.namespace_len(namespace) == 0
136 }
137}
138
139pub struct InprocRegistry {
149 state: RwLock<RegistryState>,
150}
151
152impl InprocRegistry {
153 pub fn new() -> Self {
155 Self {
156 state: RwLock::new(RegistryState {
157 namespaces: HashMap::new(),
158 }),
159 }
160 }
161
162 pub fn global() -> &'static InprocRegistry {
166 GLOBAL_REGISTRY.get_or_init(InprocRegistry::new)
167 }
168
169 pub fn register(
175 &self,
176 name: impl Into<String>,
177 pubkey: PubKey,
178 sender: InboxSender,
179 ) -> RegistrationOutcome {
180 self.register_with_meta_in_namespace(
181 DEFAULT_NAMESPACE,
182 name,
183 pubkey,
184 sender,
185 PeerMeta::default(),
186 )
187 }
188
189 pub fn register_with_meta_in_namespace(
200 &self,
201 namespace: &str,
202 name: impl Into<String>,
203 pubkey: PubKey,
204 sender: InboxSender,
205 meta: PeerMeta,
206 ) -> RegistrationOutcome {
207 let name = name.into();
208 if pubkey.is_zero() {
209 tracing::warn!(
210 inproc_namespace = %namespace,
211 peer_name = %name,
212 "rejecting zero-pubkey inproc registration"
213 );
214 return RegistrationOutcome::Rejected {
215 reason: RegistrationRejection::ZeroPubkey,
216 };
217 }
218 let peer = InprocPeer {
219 name: name.clone(),
220 pubkey,
221 sender,
222 meta,
223 };
224
225 let mut state = self.state.write();
226 let namespace_state = state.namespace_mut(namespace);
227
228 let evicted_name = namespace_state
230 .peers
231 .get(&pubkey)
232 .filter(|old_peer| old_peer.name != name)
233 .map(|old_peer| old_peer.name.clone());
234 if let Some(old_name) = &evicted_name {
235 namespace_state.names.remove(old_name);
236 }
237
238 let evicted_pubkey = namespace_state
241 .names
242 .get(&name)
243 .filter(|&&old_pk| old_pk != pubkey)
244 .copied();
245 if let Some(old_pubkey) = evicted_pubkey {
246 namespace_state.peers.remove(&old_pubkey);
247 }
248
249 namespace_state.peers.insert(pubkey, peer);
250 namespace_state.names.insert(name, pubkey);
251
252 match (evicted_name, evicted_pubkey) {
253 (None, None) => RegistrationOutcome::Registered,
254 (Some(evicted_name), None) => RegistrationOutcome::ReplacedPubkey { evicted_name },
255 (None, Some(evicted_pubkey)) => RegistrationOutcome::EvictedName { evicted_pubkey },
256 (Some(evicted_name), Some(evicted_pubkey)) => {
257 RegistrationOutcome::ReplacedPubkeyAndEvictedName {
258 evicted_name,
259 evicted_pubkey,
260 }
261 }
262 }
263 }
264
265 pub fn unregister(&self, pubkey: &PubKey) -> bool {
269 self.unregister_in_namespace(DEFAULT_NAMESPACE, pubkey)
270 }
271
272 pub fn unregister_in_namespace(&self, namespace: &str, pubkey: &PubKey) -> bool {
274 let mut state = self.state.write();
275 if let Some(namespace_state) = state.namespaces.get_mut(namespace)
276 && let Some(peer) = namespace_state.peers.remove(pubkey)
277 {
278 namespace_state.names.remove(&peer.name);
279 return true;
280 }
281 false
282 }
283
284 pub fn get_by_pubkey(&self, pubkey: &PubKey) -> Option<InboxSender> {
286 self.get_by_pubkey_in_namespace(DEFAULT_NAMESPACE, pubkey)
287 }
288
289 pub fn get_by_pubkey_in_namespace(
291 &self,
292 namespace: &str,
293 pubkey: &PubKey,
294 ) -> Option<InboxSender> {
295 if pubkey.is_zero() {
296 return None;
297 }
298 self.state
299 .read()
300 .namespace(namespace)?
301 .peers
302 .get(pubkey)
303 .map(|p| p.sender.clone())
304 }
305
306 pub(crate) fn get_by_pubkey_any_namespace(&self, pubkey: &PubKey) -> Option<InboxSender> {
312 if pubkey.is_zero() {
313 return None;
314 }
315 let state = self.state.read();
316 let mut found = None;
317 for namespace_state in state.namespaces.values() {
318 if let Some(peer) = namespace_state.peers.get(pubkey) {
319 if found.is_some() {
320 return None;
321 }
322 found = Some(peer.sender.clone());
323 }
324 }
325 found
326 }
327
328 pub fn get_name_by_pubkey(&self, pubkey: &PubKey) -> Option<String> {
330 self.get_name_by_pubkey_in_namespace(DEFAULT_NAMESPACE, pubkey)
331 }
332
333 pub fn get_name_by_pubkey_in_namespace(
335 &self,
336 namespace: &str,
337 pubkey: &PubKey,
338 ) -> Option<String> {
339 if pubkey.is_zero() {
340 return None;
341 }
342 self.state
343 .read()
344 .namespace(namespace)?
345 .peers
346 .get(pubkey)
347 .map(|peer| peer.name.clone())
348 }
349
350 pub fn contains(&self, pubkey: &PubKey) -> bool {
352 self.state
353 .read()
354 .namespace(DEFAULT_NAMESPACE)
355 .is_some_and(|ns| ns.peers.contains_key(pubkey))
356 }
357
358 pub fn contains_name(&self, name: &str) -> bool {
360 self.state
361 .read()
362 .namespace(DEFAULT_NAMESPACE)
363 .is_some_and(|ns| ns.names.contains_key(name))
364 }
365
366 pub fn len(&self) -> usize {
368 self.state.read().namespace_len(DEFAULT_NAMESPACE)
369 }
370
371 pub fn is_empty(&self) -> bool {
373 self.state.read().namespace_is_empty(DEFAULT_NAMESPACE)
374 }
375
376 pub fn clear(&self) {
378 self.state.write().namespaces.clear();
379 }
380
381 pub(crate) async fn send_to_pubkey_any_namespace_with_id_wait(
386 &self,
387 from_keypair: &Keypair,
388 to_pubkey: &PubKey,
389 envelope_id: Uuid,
390 kind: MessageKind,
391 sign_envelope: bool,
392 ) -> Result<uuid::Uuid, InprocSendError> {
393 let sender = self
394 .get_by_pubkey_any_namespace(to_pubkey)
395 .ok_or_else(|| InprocSendError::PeerNotFound(to_pubkey.to_peer_id().to_string()))?;
396
397 Self::deliver_to_sender_wait(
398 from_keypair,
399 *to_pubkey,
400 sender,
401 envelope_id,
402 kind,
403 sign_envelope,
404 )
405 .await
406 }
407
408 pub(crate) async fn send_to_pubkey_in_namespace_with_id_wait(
420 &self,
421 namespace: &str,
422 from_keypair: &Keypair,
423 to_pubkey: &PubKey,
424 envelope_id: Uuid,
425 kind: MessageKind,
426 sign_envelope: bool,
427 ) -> Result<uuid::Uuid, InprocSendError> {
428 let sender = self
429 .get_by_pubkey_in_namespace(namespace, to_pubkey)
430 .ok_or_else(|| InprocSendError::PeerNotFound(to_pubkey.to_peer_id().to_string()))?;
431
432 Self::deliver_to_sender_wait(
433 from_keypair,
434 *to_pubkey,
435 sender,
436 envelope_id,
437 kind,
438 sign_envelope,
439 )
440 .await
441 }
442
443 async fn deliver_to_sender_wait(
444 from_keypair: &Keypair,
445 to_pubkey: PubKey,
446 sender: InboxSender,
447 envelope_id: Uuid,
448 kind: MessageKind,
449 sign_envelope: bool,
450 ) -> Result<uuid::Uuid, InprocSendError> {
451 let mut envelope = Envelope {
452 id: envelope_id,
453 from: from_keypair.public_key(),
454 to: to_pubkey,
455 kind,
456 sig: Signature::new([0u8; 64]),
457 };
458 if sign_envelope {
459 envelope.sign(from_keypair);
460 }
461
462 let envelope_id = envelope.id;
463 match sender.send_wait(InboxItem::External { envelope }).await {
464 AdmissionOutcome::Admitted => {}
465 AdmissionOutcome::Dropped {
466 reason: DropReason::SessionClosed,
467 } => return Err(InprocSendError::InboxClosed),
468 AdmissionOutcome::Dropped {
469 reason: DropReason::InboxFull,
470 } => return Err(InprocSendError::InboxFull),
471 AdmissionOutcome::Dropped { reason } => {
472 return Err(InprocSendError::IngressDropped(reason));
473 }
474 }
475
476 Ok(envelope_id)
477 }
478
479 pub fn peer_names_in_namespace(&self, namespace: &str) -> Vec<String> {
481 self.state
482 .read()
483 .namespace(namespace)
484 .map_or_else(Vec::new, |ns| ns.names.keys().cloned().collect())
485 }
486
487 pub fn peers(&self) -> Vec<InprocPeerInfo> {
489 self.peers_in_namespace(DEFAULT_NAMESPACE)
490 }
491
492 pub fn peers_in_namespace(&self, namespace: &str) -> Vec<InprocPeerInfo> {
494 self.state
495 .read()
496 .namespace(namespace)
497 .map_or_else(Vec::new, |ns| {
498 ns.peers
499 .values()
500 .map(|peer| InprocPeerInfo {
501 name: peer.name.clone(),
502 pubkey: peer.pubkey,
503 meta: peer.meta.clone(),
504 })
505 .collect()
506 })
507 }
508}
509
510impl Default for InprocRegistry {
511 fn default() -> Self {
512 Self::new()
513 }
514}
515
516#[derive(Debug, thiserror::Error)]
518pub enum InprocSendError {
519 #[error("Inproc peer not found: {0}")]
520 PeerNotFound(String),
521 #[error("Peer inbox has been closed")]
522 InboxClosed,
523 #[error("Peer inbox is full")]
524 InboxFull,
525 #[error("Peer inbox dropped ingress: {0:?}")]
526 IngressDropped(crate::inbox::DropReason),
527}
528
529#[cfg(test)]
530#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
531mod tests {
532 use super::*;
533 use crate::classify::test_support;
534 use crate::inbox::Inbox;
535 use crate::trust::TrustStore;
536
537 fn classified_inbox() -> (Inbox, crate::InboxSender) {
538 Inbox::new_classified(test_support::classification_context(
539 TrustStore::new(),
540 false,
541 ))
542 }
543
544 fn make_keypair() -> Keypair {
545 Keypair::generate()
546 }
547
548 #[test]
549 fn test_registry_new() {
550 let registry = InprocRegistry::new();
551 assert!(registry.is_empty());
552 assert_eq!(registry.len(), 0);
553 }
554
555 #[test]
556 fn test_registry_register_and_lookup() {
557 let registry = InprocRegistry::new();
558 let keypair = make_keypair();
559 let pubkey = keypair.public_key();
560 let (_, sender) = classified_inbox();
561
562 registry.register("test-agent", pubkey, sender);
563
564 assert!(!registry.is_empty());
565 assert_eq!(registry.len(), 1);
566 assert!(registry.contains(&pubkey));
567 assert!(registry.contains_name("test-agent"));
568
569 assert_eq!(
571 registry.get_name_by_pubkey(&pubkey).as_deref(),
572 Some("test-agent")
573 );
574 assert!(registry.get_by_pubkey(&pubkey).is_some());
575 }
576
577 #[test]
578 fn test_registry_rejects_zero_pubkey_registration() {
579 let registry = InprocRegistry::new();
580 let (_, sender) = classified_inbox();
581 let zero_pubkey = PubKey::new([0u8; 32]);
582
583 registry.register("zero-agent", zero_pubkey, sender);
584
585 assert!(registry.is_empty());
586 assert!(!registry.contains_name("zero-agent"));
587 assert!(registry.get_by_pubkey(&zero_pubkey).is_none());
588 }
589
590 #[test]
591 fn test_registry_zero_pubkey_registration_does_not_shadow_valid_name() {
592 let registry = InprocRegistry::new();
593 let valid_keypair = make_keypair();
594 let valid_pubkey = valid_keypair.public_key();
595 let (_, valid_sender) = classified_inbox();
596 let (_, zero_sender) = classified_inbox();
597 let zero_pubkey = PubKey::new([0u8; 32]);
598
599 registry.register("stable-agent", valid_pubkey, valid_sender);
600 registry.register("stable-agent", zero_pubkey, zero_sender);
601
602 assert_eq!(registry.len(), 1);
603 assert!(registry.contains(&valid_pubkey));
604 assert!(registry.contains_name("stable-agent"));
605 assert!(registry.get_by_pubkey(&valid_pubkey).is_some());
606 assert!(registry.get_by_pubkey(&zero_pubkey).is_none());
607
608 assert_eq!(
609 registry.get_name_by_pubkey(&valid_pubkey).as_deref(),
610 Some("stable-agent"),
611 "valid name mapping should remain"
612 );
613 }
614
615 #[test]
616 fn test_registry_unregister() {
617 let registry = InprocRegistry::new();
618 let keypair = make_keypair();
619 let pubkey = keypair.public_key();
620 let (_, sender) = classified_inbox();
621
622 registry.register("test-agent", pubkey, sender);
623 assert!(registry.contains(&pubkey));
624
625 let removed = registry.unregister(&pubkey);
626 assert!(removed);
627 assert!(!registry.contains(&pubkey));
628 assert!(!registry.contains_name("test-agent"));
629 assert!(registry.is_empty());
630
631 let removed_again = registry.unregister(&pubkey);
633 assert!(!removed_again);
634 }
635
636 #[test]
637 fn test_registry_replace_on_same_pubkey() {
638 let registry = InprocRegistry::new();
639 let keypair = make_keypair();
640 let pubkey = keypair.public_key();
641 let (_, sender1) = classified_inbox();
642 let (_, sender2) = classified_inbox();
643
644 registry.register("agent-v1", pubkey, sender1);
646 assert!(registry.contains_name("agent-v1"));
647
648 registry.register("agent-v2", pubkey, sender2);
650
651 assert!(!registry.contains_name("agent-v1"));
653 assert!(registry.contains_name("agent-v2"));
654 assert_eq!(registry.len(), 1);
655 }
656
657 #[test]
658 fn test_registry_replace_on_same_name_different_pubkey() {
659 let registry = InprocRegistry::new();
660 let keypair1 = make_keypair();
661 let pubkey1 = keypair1.public_key();
662 let keypair2 = make_keypair();
663 let pubkey2 = keypair2.public_key();
664 let (_, sender1) = classified_inbox();
665 let (_, sender2) = classified_inbox();
666
667 registry.register("my-agent", pubkey1, sender1);
669 assert!(registry.contains(&pubkey1));
670 assert!(registry.contains_name("my-agent"));
671 assert_eq!(registry.len(), 1);
672
673 registry.register("my-agent", pubkey2, sender2);
675
676 assert!(!registry.contains(&pubkey1), "old pubkey should be evicted");
678 assert!(registry.contains(&pubkey2));
679 assert!(registry.contains_name("my-agent"));
680 assert_eq!(registry.len(), 1);
681
682 assert_eq!(
684 registry.get_name_by_pubkey(&pubkey2).as_deref(),
685 Some("my-agent")
686 );
687 }
688
689 #[test]
693 fn registration_outcome_is_typed_for_displacement_and_rejection() {
694 let registry = InprocRegistry::new();
695 let keypair = make_keypair();
696 let pubkey = keypair.public_key();
697 let (_, sender1) = classified_inbox();
698 let (_, sender2) = classified_inbox();
699 let (_, sender3) = classified_inbox();
700
701 assert_eq!(
703 registry.register("agent-v1", pubkey, sender1),
704 RegistrationOutcome::Registered
705 );
706
707 assert_eq!(
710 registry.register("agent-v2", pubkey, sender2),
711 RegistrationOutcome::ReplacedPubkey {
712 evicted_name: "agent-v1".to_string()
713 }
714 );
715
716 let other = make_keypair();
719 let other_pubkey = other.public_key();
720 match registry.register("agent-v2", other_pubkey, sender3) {
721 RegistrationOutcome::EvictedName { evicted_pubkey } => {
722 assert_eq!(evicted_pubkey, pubkey);
723 }
724 other => panic!("expected EvictedName, got {other:?}"),
725 }
726
727 let (_, zero_sender) = classified_inbox();
730 let zero_pubkey = PubKey::new([0u8; 32]);
731 assert_eq!(
732 registry.register("zero", zero_pubkey, zero_sender),
733 RegistrationOutcome::Rejected {
734 reason: RegistrationRejection::ZeroPubkey
735 }
736 );
737 assert!(!registry.contains_name("zero"));
738 }
739
740 #[test]
744 fn test_registry_aba_scenario_safe() {
745 let registry = InprocRegistry::new();
746 let keypair_old = make_keypair();
747 let pubkey_old = keypair_old.public_key();
748 let keypair_new = make_keypair();
749 let pubkey_new = keypair_new.public_key();
750 let (_, sender_old) = classified_inbox();
751 let (_, sender_new) = classified_inbox();
752
753 registry.register("agent", pubkey_old, sender_old);
755 assert!(registry.contains(&pubkey_old));
756
757 registry.register("agent", pubkey_new, sender_new);
759 assert!(
760 !registry.contains(&pubkey_old),
761 "old pubkey should be evicted"
762 );
763 assert!(registry.contains(&pubkey_new));
764
765 let removed = registry.unregister(&pubkey_old);
768 assert!(!removed, "unregister of evicted pubkey should return false");
769
770 assert!(
772 registry.contains(&pubkey_new),
773 "new agent should still be registered"
774 );
775 assert!(
776 registry.contains_name("agent"),
777 "name should still map to new agent"
778 );
779
780 assert_eq!(
782 registry.get_name_by_pubkey(&pubkey_new).as_deref(),
783 Some("agent"),
784 "name should map to the new pubkey"
785 );
786 }
787
788 #[test]
789 fn test_registry_peer_names_in_namespace() {
790 let registry = InprocRegistry::new();
791
792 for i in 0..3 {
793 let keypair = make_keypair();
794 let (_, sender) = classified_inbox();
795 registry.register_with_meta_in_namespace(
796 "realm-names",
797 format!("agent-{i}"),
798 keypair.public_key(),
799 sender,
800 PeerMeta::default(),
801 );
802 }
803
804 let names = registry.peer_names_in_namespace("realm-names");
805 assert_eq!(names.len(), 3);
806 assert!(names.contains(&"agent-0".to_string()));
807 assert!(names.contains(&"agent-1".to_string()));
808 assert!(names.contains(&"agent-2".to_string()));
809 assert!(registry.peer_names_in_namespace("realm-other").is_empty());
810 }
811
812 #[test]
813 fn test_registry_peers_snapshot() {
814 let registry = InprocRegistry::new();
815 let keypair = make_keypair();
816 let pubkey = keypair.public_key();
817 let (_, sender) = classified_inbox();
818 registry.register("agent-a", pubkey, sender);
819
820 let peers = registry.peers();
821 assert_eq!(peers.len(), 1);
822 assert_eq!(peers[0].name, "agent-a");
823 assert_eq!(peers[0].pubkey, pubkey);
824 }
825
826 #[test]
827 fn test_registry_clear() {
828 let registry = InprocRegistry::new();
829
830 for i in 0..3 {
831 let keypair = make_keypair();
832 let (_, sender) = classified_inbox();
833 registry.register(format!("agent-{i}"), keypair.public_key(), sender);
834 }
835
836 assert_eq!(registry.len(), 3);
837 registry.clear();
838 assert!(registry.is_empty());
839 }
840
841 #[tokio::test]
842 async fn test_registry_send_delivers_to_inbox() {
843 let registry = InprocRegistry::new();
844
845 let receiver_keypair = make_keypair();
847 let (mut inbox, sender) = classified_inbox();
848 registry.register("receiver", receiver_keypair.public_key(), sender);
849
850 let sender_keypair = make_keypair();
852
853 let result = registry
855 .send_to_pubkey_in_namespace_with_id_wait(
856 "",
857 &sender_keypair,
858 &receiver_keypair.public_key(),
859 Uuid::new_v4(),
860 MessageKind::Message {
861 content_taint: None,
862 blocks: None,
863 body: "hello inproc".to_string(),
864 handling_mode: None,
865 },
866 true,
867 )
868 .await;
869 assert!(result.is_ok());
870
871 let items = inbox.try_drain_classified();
873 assert_eq!(items.len(), 1);
874
875 match &items[0].item {
876 InboxItem::External { envelope } => {
877 assert_eq!(envelope.from, sender_keypair.public_key());
878 assert_eq!(envelope.to, receiver_keypair.public_key());
879 match &envelope.kind {
880 MessageKind::Message {
881 blocks: None, body, ..
882 } => {
883 assert_eq!(body, "hello inproc");
884 }
885 _ => panic!("expected Message kind"),
886 }
887 assert!(envelope.verify());
889 }
890 _ => panic!("expected External inbox item"),
891 }
892 }
893
894 #[tokio::test]
895 async fn test_registry_send_peer_not_found() {
896 let registry = InprocRegistry::new();
897 let sender_keypair = make_keypair();
898 let unknown = make_keypair().public_key();
899
900 let result = registry
901 .send_to_pubkey_in_namespace_with_id_wait(
902 "",
903 &sender_keypair,
904 &unknown,
905 Uuid::new_v4(),
906 MessageKind::Message {
907 content_taint: None,
908 blocks: None,
909 body: "hello".to_string(),
910 handling_mode: None,
911 },
912 true,
913 )
914 .await;
915
916 assert!(matches!(result, Err(InprocSendError::PeerNotFound(_))));
917 }
918
919 #[tokio::test]
920 async fn test_registry_send_inbox_closed() {
921 let registry = InprocRegistry::new();
922
923 let receiver_keypair = make_keypair();
925 let (inbox, sender) = classified_inbox();
926 registry.register("receiver", receiver_keypair.public_key(), sender);
927 drop(inbox); let sender_keypair = make_keypair();
930
931 let result = registry
932 .send_to_pubkey_in_namespace_with_id_wait(
933 "",
934 &sender_keypair,
935 &receiver_keypair.public_key(),
936 Uuid::new_v4(),
937 MessageKind::Message {
938 content_taint: None,
939 blocks: None,
940 body: "hello".to_string(),
941 handling_mode: None,
942 },
943 true,
944 )
945 .await;
946
947 assert!(matches!(result, Err(InprocSendError::InboxClosed)));
948 }
949
950 #[tokio::test]
951 async fn test_registry_namespace_isolation_for_lookup_and_send() {
952 let registry = InprocRegistry::new();
953 let receiver_keypair = make_keypair();
954 let (mut inbox, sender) = classified_inbox();
955 registry.register_with_meta_in_namespace(
956 "realm-a",
957 "receiver",
958 receiver_keypair.public_key(),
959 sender,
960 PeerMeta::default(),
961 );
962
963 assert!(
965 registry
966 .get_by_pubkey(&receiver_keypair.public_key())
967 .is_none()
968 );
969 assert!(
970 registry
971 .get_by_pubkey_in_namespace("realm-a", &receiver_keypair.public_key())
972 .is_some()
973 );
974
975 let sender_keypair = make_keypair();
976
977 let ok = registry
979 .send_to_pubkey_in_namespace_with_id_wait(
980 "realm-a",
981 &sender_keypair,
982 &receiver_keypair.public_key(),
983 Uuid::new_v4(),
984 MessageKind::Message {
985 content_taint: None,
986 blocks: None,
987 body: "hello scoped".to_string(),
988 handling_mode: None,
989 },
990 true,
991 )
992 .await;
993 assert!(ok.is_ok());
994
995 let wrong_ns = registry
997 .send_to_pubkey_in_namespace_with_id_wait(
998 "realm-b",
999 &sender_keypair,
1000 &receiver_keypair.public_key(),
1001 Uuid::new_v4(),
1002 MessageKind::Message {
1003 content_taint: None,
1004 blocks: None,
1005 body: "should not deliver".to_string(),
1006 handling_mode: None,
1007 },
1008 true,
1009 )
1010 .await;
1011 assert!(matches!(wrong_ns, Err(InprocSendError::PeerNotFound(_))));
1012
1013 let items = inbox.try_drain_classified();
1014 assert_eq!(items.len(), 1);
1015 }
1016
1017 #[tokio::test]
1018 async fn test_send_to_pubkey_in_namespace_ignores_display_name_collision() {
1019 let registry = InprocRegistry::new();
1020 let target_keypair = make_keypair();
1021 let target_pubkey = target_keypair.public_key();
1022 let shadow_keypair = make_keypair();
1023 let shadow_pubkey = shadow_keypair.public_key();
1024 let (mut target_inbox, target_sender) = classified_inbox();
1025 let (mut shadow_inbox, shadow_sender) = classified_inbox();
1026
1027 registry.register_with_meta_in_namespace(
1028 "",
1029 "canonical-target",
1030 target_pubkey,
1031 target_sender,
1032 PeerMeta::default(),
1033 );
1034 registry.register_with_meta_in_namespace(
1035 "",
1036 "shared-display-name",
1037 shadow_pubkey,
1038 shadow_sender,
1039 PeerMeta::default(),
1040 );
1041
1042 let sender_keypair = make_keypair();
1043 let result = registry
1044 .send_to_pubkey_in_namespace_with_id_wait(
1045 "",
1046 &sender_keypair,
1047 &target_pubkey,
1048 Uuid::new_v4(),
1049 MessageKind::Message {
1050 content_taint: None,
1051 blocks: None,
1052 body: "hello canonical".to_string(),
1053 handling_mode: None,
1054 },
1055 true,
1056 )
1057 .await;
1058 assert!(result.is_ok());
1059
1060 assert_eq!(shadow_inbox.try_drain_classified().len(), 0);
1061 let items = target_inbox.try_drain_classified();
1062 assert_eq!(items.len(), 1);
1063 let InboxItem::External { envelope } = &items[0].item else {
1064 panic!("expected external envelope");
1065 };
1066 assert_eq!(envelope.to, target_pubkey);
1067 }
1068
1069 #[tokio::test]
1070 async fn test_send_to_pubkey_any_namespace_rejects_ambiguous_identity() {
1071 let registry = InprocRegistry::new();
1072 let sender_keypair = make_keypair();
1073 let target_keypair = make_keypair();
1074 let target_pubkey = target_keypair.public_key();
1075 let (mut alpha_inbox, alpha_sender) = classified_inbox();
1076 let (mut beta_inbox, beta_sender) = classified_inbox();
1077
1078 registry.register_with_meta_in_namespace(
1079 "realm-alpha",
1080 "alpha-target",
1081 target_pubkey,
1082 alpha_sender,
1083 PeerMeta::default(),
1084 );
1085 registry.register_with_meta_in_namespace(
1086 "realm-beta",
1087 "beta-target",
1088 target_pubkey,
1089 beta_sender,
1090 PeerMeta::default(),
1091 );
1092
1093 let result = registry
1094 .send_to_pubkey_any_namespace_with_id_wait(
1095 &sender_keypair,
1096 &target_pubkey,
1097 Uuid::new_v4(),
1098 MessageKind::Message {
1099 content_taint: None,
1100 blocks: None,
1101 body: "ambiguous identity".to_string(),
1102 handling_mode: None,
1103 },
1104 true,
1105 )
1106 .await;
1107
1108 assert!(matches!(result, Err(InprocSendError::PeerNotFound(_))));
1109 assert!(alpha_inbox.try_drain_classified().is_empty());
1110 assert!(beta_inbox.try_drain_classified().is_empty());
1111 }
1112
1113 #[test]
1114 fn test_registry_same_name_can_exist_in_different_namespaces() {
1115 let registry = InprocRegistry::new();
1116 let kp_a = make_keypair();
1117 let kp_b = make_keypair();
1118 let (_, sender_a) = classified_inbox();
1119 let (_, sender_b) = classified_inbox();
1120
1121 registry.register_with_meta_in_namespace(
1122 "realm-a",
1123 "shared-name",
1124 kp_a.public_key(),
1125 sender_a,
1126 PeerMeta::default(),
1127 );
1128 registry.register_with_meta_in_namespace(
1129 "realm-b",
1130 "shared-name",
1131 kp_b.public_key(),
1132 sender_b,
1133 PeerMeta::default(),
1134 );
1135
1136 assert_eq!(
1137 registry
1138 .get_name_by_pubkey_in_namespace("realm-a", &kp_a.public_key())
1139 .as_deref(),
1140 Some("shared-name")
1141 );
1142 assert_eq!(
1143 registry
1144 .get_name_by_pubkey_in_namespace("realm-b", &kp_b.public_key())
1145 .as_deref(),
1146 Some("shared-name")
1147 );
1148 assert_ne!(kp_a.public_key(), kp_b.public_key());
1149 assert!(
1150 !registry.contains_name("shared-name"),
1151 "default namespace must not see namespaced registrations"
1152 );
1153 }
1154
1155 #[test]
1156 fn test_global_registry() {
1157 let registry = InprocRegistry::global();
1159
1160 registry.clear();
1162
1163 let keypair = make_keypair();
1165 let (_, sender) = classified_inbox();
1166 registry.register("global-test", keypair.public_key(), sender);
1167
1168 assert!(registry.contains_name("global-test"));
1170
1171 registry.unregister(&keypair.public_key());
1173 }
1174
1175 #[test]
1176 fn test_registry_register_with_meta() {
1177 let registry = InprocRegistry::new();
1178 let keypair = make_keypair();
1179 let pubkey = keypair.public_key();
1180 let (_, sender) = classified_inbox();
1181
1182 let meta = PeerMeta::default()
1183 .with_description("Reviews code for style issues")
1184 .with_label("lang", "rust");
1185
1186 registry.register_with_meta_in_namespace(
1187 DEFAULT_NAMESPACE,
1188 "reviewer",
1189 pubkey,
1190 sender,
1191 meta.clone(),
1192 );
1193
1194 let peers = registry.peers();
1195 assert_eq!(peers.len(), 1);
1196 assert_eq!(peers[0].name, "reviewer");
1197 assert_eq!(peers[0].pubkey, pubkey);
1198 assert_eq!(peers[0].meta, meta);
1199 }
1200
1201 #[test]
1202 fn test_registry_peers_returns_default_meta_for_plain_register() {
1203 let registry = InprocRegistry::new();
1204 let keypair = make_keypair();
1205 let pubkey = keypair.public_key();
1206 let (_, sender) = classified_inbox();
1207
1208 registry.register("plain-agent", pubkey, sender);
1209
1210 let peers = registry.peers();
1211 assert_eq!(peers.len(), 1);
1212 assert_eq!(peers[0].meta, PeerMeta::default());
1213 }
1214}