Skip to main content

meerkat_comms/
inproc.rs

1//! In-process message transport for peer communication within one runtime.
2//!
3//! This module provides a process-global registry that allows agents within
4//! the same process to communicate without network sockets. Messages are
5//! delivered directly via in-memory channels.
6//!
7//! # Usage
8//!
9//! ```text
10//! // Register an agent's inbox
11//! let (inbox, sender) = Inbox::new();
12//! InprocRegistry::global().register("my-agent", pubkey, sender);
13//!
14//! // Delivery is pubkey-keyed: the Router resolves a trusted peer's
15//! // signing key and delivers through the namespace-scoped
16//! // send_to_pubkey_*_wait owners.
17//!
18//! // Unregister when done
19//! InprocRegistry::global().unregister(&pubkey);
20//! ```
21
22use 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/// Snapshot of an inproc peer returned by [`InprocRegistry::peers()`].
36#[derive(Debug, Clone)]
37pub struct InprocPeerInfo {
38    pub name: String,
39    pub pubkey: PubKey,
40    pub meta: PeerMeta,
41}
42
43/// Why a registration was rejected without mutating the registry.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum RegistrationRejection {
46    /// The supplied pubkey was the all-zero key, which can never identify a
47    /// distinct peer and is refused fail-closed.
48    ZeroPubkey,
49}
50
51/// Typed result of registering an inproc peer.
52///
53/// Registration is not always a clean insert: re-registering an existing
54/// pubkey under a new name evicts the old name mapping, and re-registering an
55/// existing name with a new pubkey evicts the old pubkey entry. Both evictions
56/// can also happen at once. Callers (runtime constructors, metadata refresh)
57/// must observe these facts rather than assume a clean success.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum RegistrationOutcome {
60    /// The peer was inserted without displacing any existing route.
61    Registered,
62    /// This pubkey was already registered under a different name; the old name
63    /// mapping was removed and replaced with the new name.
64    ReplacedPubkey { evicted_name: String },
65    /// This name was already bound to a different pubkey; the old pubkey entry
66    /// was evicted so the stale key is no longer reachable.
67    EvictedName { evicted_pubkey: PubKey },
68    /// Both evictions happened: this pubkey's old name was removed AND this
69    /// name's old pubkey was evicted in the same registration.
70    ReplacedPubkeyAndEvictedName {
71        evicted_name: String,
72        evicted_pubkey: PubKey,
73    },
74    /// The registration was refused without mutating the registry.
75    Rejected { reason: RegistrationRejection },
76}
77
78impl RegistrationOutcome {
79    /// Whether the registration displaced an existing route (either eviction).
80    pub fn displaced_existing(&self) -> bool {
81        matches!(
82            self,
83            Self::ReplacedPubkey { .. }
84                | Self::EvictedName { .. }
85                | Self::ReplacedPubkeyAndEvictedName { .. }
86        )
87    }
88
89    /// Whether the registration was rejected (no mutation occurred).
90    pub fn is_rejected(&self) -> bool {
91        matches!(self, Self::Rejected { .. })
92    }
93}
94
95/// Global inproc registry instance.
96static GLOBAL_REGISTRY: OnceLock<InprocRegistry> = OnceLock::new();
97
98/// Registry entry for an inproc peer.
99#[derive(Clone)]
100struct InprocPeer {
101    name: String,
102    pubkey: PubKey,
103    sender: InboxSender,
104    meta: PeerMeta,
105}
106
107/// Internal namespace state protected by a single lock to prevent deadlocks.
108#[derive(Default)]
109struct NamespaceState {
110    /// Map from pubkey to peer entry.
111    peers: HashMap<PubKey, InprocPeer>,
112    /// Map from name to pubkey for name-based lookup.
113    names: HashMap<String, PubKey>,
114}
115
116/// Internal registry state keyed by namespace.
117struct 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
139/// Process-global registry for in-process peer communication.
140///
141/// This registry maps agent pubkeys to their inbox senders, allowing
142/// direct message delivery without network transport.
143///
144/// # Thread Safety
145///
146/// All operations are protected by a single RwLock to ensure consistent
147/// state and prevent deadlocks.
148pub struct InprocRegistry {
149    state: RwLock<RegistryState>,
150}
151
152impl InprocRegistry {
153    /// Create a new empty registry.
154    pub fn new() -> Self {
155        Self {
156            state: RwLock::new(RegistryState {
157                namespaces: HashMap::new(),
158            }),
159        }
160    }
161
162    /// Get the global registry instance.
163    ///
164    /// This creates the registry on first access.
165    pub fn global() -> &'static InprocRegistry {
166        GLOBAL_REGISTRY.get_or_init(InprocRegistry::new)
167    }
168
169    /// Register an agent's inbox for inproc communication.
170    ///
171    /// Returns a typed [`RegistrationOutcome`] describing whether the insert was
172    /// clean or displaced an existing route (see
173    /// [`register_with_meta_in_namespace`](Self::register_with_meta_in_namespace)).
174    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    /// Register an agent's inbox within an explicit namespace.
190    ///
191    /// Returns a typed [`RegistrationOutcome`] that surfaces route displacement
192    /// explicitly: re-registering an existing pubkey under a new name evicts
193    /// the old name mapping ([`RegistrationOutcome::ReplacedPubkey`]); a name
194    /// rebound to a new pubkey evicts the old pubkey entry
195    /// ([`RegistrationOutcome::EvictedName`]); both can happen at once
196    /// ([`RegistrationOutcome::ReplacedPubkeyAndEvictedName`]). A zero pubkey is
197    /// refused without mutation ([`RegistrationOutcome::Rejected`]). Callers
198    /// must observe displacement/rejection rather than assume a clean success.
199    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        // If this pubkey was registered under a different name, remove old name mapping
229        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        // If this name was registered to a different pubkey, remove the old pubkey entry
239        // This prevents stale pubkeys from remaining reachable
240        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    /// Unregister an agent by pubkey.
266    ///
267    /// Returns true if the agent was found and removed.
268    pub fn unregister(&self, pubkey: &PubKey) -> bool {
269        self.unregister_in_namespace(DEFAULT_NAMESPACE, pubkey)
270    }
271
272    /// Unregister an agent by pubkey from an explicit namespace.
273    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    /// Look up an inproc peer by pubkey.
285    pub fn get_by_pubkey(&self, pubkey: &PubKey) -> Option<InboxSender> {
286        self.get_by_pubkey_in_namespace(DEFAULT_NAMESPACE, pubkey)
287    }
288
289    /// Look up an inproc peer by pubkey in an explicit namespace.
290    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    /// Look up an inproc peer by pubkey across all namespaces.
307    ///
308    /// Cross-namespace delivery has no typed target namespace. If the same
309    /// canonical identity is live in more than one namespace, fail closed
310    /// rather than choosing whichever namespace the map happens to yield first.
311    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    /// Look up an inproc peer name by public key.
329    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    /// Look up an inproc peer name by public key in an explicit namespace.
334    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    /// Check if a peer is registered.
351    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    /// Check if a peer name is registered.
359    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    /// Get the number of registered peers.
367    pub fn len(&self) -> usize {
368        self.state.read().namespace_len(DEFAULT_NAMESPACE)
369    }
370
371    /// Check if the registry is empty.
372    pub fn is_empty(&self) -> bool {
373        self.state.read().namespace_is_empty(DEFAULT_NAMESPACE)
374    }
375
376    /// Clear all registrations (primarily for testing).
377    pub fn clear(&self) {
378        self.state.write().namespaces.clear();
379    }
380
381    /// Backpressured pubkey-keyed delivery across all namespaces.
382    ///
383    /// Runtime-originated peer sends should await receiver capacity instead of
384    /// turning a transient full inbox into semantic message loss.
385    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    /// Namespace-scoped variant of
409    /// [`Self::send_to_pubkey_any_namespace_with_id_wait`]: the destination is
410    /// resolved exactly once, *inside* `namespace`, and that resolved sender is
411    /// the delivery target.
412    ///
413    /// This is the single-resolution send for namespace-isolated routers. The
414    /// namespace is the delivery authority, so the destination must not be
415    /// re-derived from the global registry between an isolation check and the
416    /// inbox handoff — a second any-namespace lookup would open a window where
417    /// the peer re-registers elsewhere and delivery crosses the namespace
418    /// boundary.
419    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    /// List all registered peer names in an explicit namespace.
480    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    /// List all registered peers.
488    pub fn peers(&self) -> Vec<InprocPeerInfo> {
489        self.peers_in_namespace(DEFAULT_NAMESPACE)
490    }
491
492    /// List all registered peers in an explicit namespace.
493    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/// Errors that can occur during inproc send operations.
517#[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        // Name is display metadata; routing lookups are pubkey-keyed.
570        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        // Unregister non-existent returns false
632        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        // Register with first name
645        registry.register("agent-v1", pubkey, sender1);
646        assert!(registry.contains_name("agent-v1"));
647
648        // Re-register same pubkey with different name
649        registry.register("agent-v2", pubkey, sender2);
650
651        // Old name should be removed, new name should exist
652        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        // Register first agent
668        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        // Re-register same name with different pubkey
674        registry.register("my-agent", pubkey2, sender2);
675
676        // Old pubkey should be evicted, new pubkey should exist
677        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        // The name maps to the new identity.
683        assert_eq!(
684            registry.get_name_by_pubkey(&pubkey2).as_deref(),
685            Some("my-agent")
686        );
687    }
688
689    /// ROW #292 gate: registration returns a typed [`RegistrationOutcome`] that
690    /// surfaces route displacement and zero-pubkey rejection, instead of
691    /// silently evicting and returning `()`.
692    #[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        // Clean first insert.
702        assert_eq!(
703            registry.register("agent-v1", pubkey, sender1),
704            RegistrationOutcome::Registered
705        );
706
707        // Re-registering the SAME pubkey under a NEW name evicts the old name
708        // and reports it typed.
709        assert_eq!(
710            registry.register("agent-v2", pubkey, sender2),
711            RegistrationOutcome::ReplacedPubkey {
712                evicted_name: "agent-v1".to_string()
713            }
714        );
715
716        // Re-registering an existing NAME with a NEW pubkey evicts the old
717        // pubkey and reports it typed.
718        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        // A zero pubkey is refused fail-closed with a typed rejection, no
728        // mutation.
729        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 that the ABA scenario is handled correctly:
741    /// When a new agent registers with the same name, the old agent's
742    /// unregister call (on Drop) should be a safe no-op.
743    #[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        // Step 1: Old runtime registers
754        registry.register("agent", pubkey_old, sender_old);
755        assert!(registry.contains(&pubkey_old));
756
757        // Step 2: New runtime registers same name (evicts old)
758        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        // Step 3: Old runtime drops and calls unregister(pubkey_old)
766        // This should be a no-op since pubkey_old was already evicted
767        let removed = registry.unregister(&pubkey_old);
768        assert!(!removed, "unregister of evicted pubkey should return false");
769
770        // New agent should still be registered (not affected by old unregister)
771        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        // The name maps to the new identity.
781        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        // Set up receiver
846        let receiver_keypair = make_keypair();
847        let (mut inbox, sender) = classified_inbox();
848        registry.register("receiver", receiver_keypair.public_key(), sender);
849
850        // Set up sender
851        let sender_keypair = make_keypair();
852
853        // Send a message (pubkey-keyed delivery)
854        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        // Verify message was received
872        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                // Verify signature
888                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        // Set up receiver but drop the inbox
924        let receiver_keypair = make_keypair();
925        let (inbox, sender) = classified_inbox();
926        registry.register("receiver", receiver_keypair.public_key(), sender);
927        drop(inbox); // Close the inbox
928
929        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        // Default namespace cannot see realm-a registrations.
964        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        // Matching namespace succeeds.
978        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        // Different namespace cannot route to receiver.
996        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        // Access global registry
1158        let registry = InprocRegistry::global();
1159
1160        // Clear any existing state (from other tests)
1161        registry.clear();
1162
1163        // Register a peer
1164        let keypair = make_keypair();
1165        let (_, sender) = classified_inbox();
1166        registry.register("global-test", keypair.public_key(), sender);
1167
1168        // Verify it's accessible
1169        assert!(registry.contains_name("global-test"));
1170
1171        // Clean up
1172        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}