Skip to main content

meerkat_comms/runtime/
comms_runtime.rs

1//! CommsRuntime - Full lifecycle manager for agent-to-agent communication.
2
3#[cfg(not(target_arch = "wasm32"))]
4use super::comms_config::ResolvedCommsConfig;
5#[cfg(not(target_arch = "wasm32"))]
6use crate::InboxSender;
7use crate::agent::types::CommsMessage;
8#[cfg(not(target_arch = "wasm32"))]
9use crate::handle_connection;
10#[cfg(target_arch = "wasm32")]
11use crate::tokio;
12use crate::trust::{TrustEntry, TrustStore};
13use crate::{InprocRegistry, Keypair, PubKey, Router, TrustedPeersView};
14use async_trait::async_trait;
15#[cfg(not(target_arch = "wasm32"))]
16use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
17use futures::Stream;
18use futures::task::{Context, Poll};
19use meerkat_core::agent::CommsRuntime as CoreCommsRuntime;
20use meerkat_core::comms::{
21    CommsCommand, CommsTrustMutation, CommsTrustMutationResult, EventStream,
22    GeneratedCommsTrustAuthoritySourceKind, InputStreamMode, PeerCapabilitySet, PeerDirectoryEntry,
23    PeerDirectorySource, PeerId, PeerName, PeerRoute, PeerSendability, SendAndStreamError,
24    SendError, SendReceipt, StreamError, StreamScope, TrustedPeerDescriptor,
25};
26use meerkat_core::config::PlainEventSource;
27#[cfg(not(target_arch = "wasm32"))]
28use meerkat_core::handles::{SessionClaim, SessionClaimError, SessionClaimHandle};
29use meerkat_core::hydrate_content_blocks;
30use meerkat_core::time_compat::Instant;
31use meerkat_core::{BlobStore, MissingBlobBehavior, SessionId};
32use parking_lot::Mutex;
33use sha2::{Digest, Sha256};
34use std::collections::{HashMap, HashSet};
35#[cfg(not(target_arch = "wasm32"))]
36use std::path::Path;
37use std::pin::Pin;
38use std::sync::Arc;
39use std::sync::atomic::{AtomicBool, Ordering};
40use thiserror::Error;
41#[cfg(not(target_arch = "wasm32"))]
42use tokio::net::TcpListener;
43use tokio::sync::Mutex as AsyncMutex;
44use tokio::sync::mpsc;
45use tokio::sync::mpsc::Receiver;
46#[cfg(not(target_arch = "wasm32"))]
47use tokio::task::JoinHandle;
48#[cfg(not(target_arch = "wasm32"))]
49use tokio::{
50    io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
51    net::TcpStream,
52};
53use uuid::Uuid;
54
55/// Default reservation TTL (time from `Reserved` to `Expired` if not attached).
56const RESERVATION_TTL: meerkat_core::time_compat::Duration =
57    meerkat_core::time_compat::Duration::from_secs(30);
58
59/// Channels-only projection of an interaction stream.
60///
61/// Semantic peer request streams store `Machine` and keep lifecycle truth
62/// (Reserved/Attached/Completed/Expired/ClosedEarly) in the MeerkatMachine DSL
63/// `interaction_streams` map (U6 / dogma #5). Local input streams store
64/// `LocalTransportOnly`, making their channel cleanup an explicit typed
65/// transport boundary with no semantic stream lifecycle twin.
66#[derive(Debug)]
67struct StreamRegistryEntry {
68    _sender: mpsc::Sender<meerkat_core::AgentEvent>,
69    receiver: Option<Receiver<meerkat_core::AgentEvent>>,
70    created_at: Instant,
71    lifecycle_authority: InteractionStreamLifecycleAuthority,
72}
73
74impl StreamRegistryEntry {
75    fn new(
76        sender: mpsc::Sender<meerkat_core::AgentEvent>,
77        receiver: Receiver<meerkat_core::AgentEvent>,
78        lifecycle_authority: InteractionStreamLifecycleAuthority,
79    ) -> Self {
80        Self {
81            _sender: sender,
82            receiver: Some(receiver),
83            created_at: Instant::now(),
84            lifecycle_authority,
85        }
86    }
87}
88
89type InteractionStreamRegistry = Arc<Mutex<HashMap<Uuid, StreamRegistryEntry>>>;
90type PeerRequestResponseAuthorityHandles = (
91    Arc<dyn meerkat_core::handles::PeerInteractionHandle>,
92    Arc<dyn meerkat_core::handles::InteractionStreamHandle>,
93);
94
95#[derive(Clone)]
96pub struct PeerRequestResponseAuthority {
97    peer_interaction: Arc<dyn meerkat_core::handles::PeerInteractionHandle>,
98    interaction_stream: Arc<dyn meerkat_core::handles::InteractionStreamHandle>,
99}
100
101impl PeerRequestResponseAuthority {
102    pub fn new(
103        peer_interaction: Arc<dyn meerkat_core::handles::PeerInteractionHandle>,
104        interaction_stream: Arc<dyn meerkat_core::handles::InteractionStreamHandle>,
105    ) -> Self {
106        Self {
107            peer_interaction,
108            interaction_stream,
109        }
110    }
111}
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114enum InteractionStreamLifecycleAuthority {
115    Machine,
116    LocalTransportOnly,
117}
118
119enum InteractionStreamReservationAuthority {
120    Machine(Arc<dyn meerkat_core::handles::InteractionStreamHandle>),
121    LocalTransportOnly,
122}
123
124struct InteractionStream {
125    id: Uuid,
126    receiver: Option<Receiver<meerkat_core::AgentEvent>>,
127    /// Shared stream-lifecycle DSL handle. `None` is valid only for
128    /// transport-only local input streams; semantic peer request/response
129    /// reservations require machine authority before they can be created.
130    stream_handle: Option<Arc<dyn meerkat_core::handles::InteractionStreamHandle>>,
131    /// Channels-only projection registry. With a handle installed, the cleanup
132    /// observer drops the entry under the same authority lock as the DSL
133    /// transition. Without a handle, the stream is a local transport-only
134    /// channel and owns direct cleanup.
135    registry: InteractionStreamRegistry,
136    source: meerkat_core::EventSourceIdentity,
137    seq: u64,
138}
139
140struct ResolvedPeer {
141    name: PeerName,
142    peer_id: meerkat_core::comms::PeerId,
143    address: meerkat_core::comms::PeerAddress,
144    source: PeerDirectorySource,
145    meta: crate::PeerMeta,
146}
147
148#[cfg(not(target_arch = "wasm32"))]
149const PAIRING_VERSION: u32 = 1;
150#[cfg(not(target_arch = "wasm32"))]
151const PAIRING_CHALLENGE_KIND: &str = "meerkat_pairing_challenge";
152#[cfg(not(target_arch = "wasm32"))]
153const PAIRING_COMPLETE_KIND: &str = "meerkat_pairing_complete";
154#[cfg(not(target_arch = "wasm32"))]
155const PAIRING_CLASSIFY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
156
157/// Typed identity-kind for a pairing caller.
158///
159/// Deserialization fails closed on any kind other than the single supported
160/// `ed25519_public_key`, replacing the previous free-string `kind` field +
161/// `== "ed25519_public_key"` equality check.
162#[cfg(not(target_arch = "wasm32"))]
163#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
164enum PairingIdentityKind {
165    #[serde(rename = "ed25519_public_key")]
166    Ed25519PublicKey,
167}
168
169/// Typed pairing public key.
170///
171/// Deserializes once from the wire `"ed25519:<base64>"` string and validates
172/// it into a [`PubKey`], failing closed on a malformed key. The original
173/// canonical string is retained because the password proof is computed over
174/// the exact wire form supplied by the caller.
175#[cfg(not(target_arch = "wasm32"))]
176#[derive(Debug, Clone)]
177struct PairingPublicKey {
178    raw: String,
179    key: PubKey,
180}
181
182#[cfg(not(target_arch = "wasm32"))]
183impl<'de> serde::Deserialize<'de> for PairingPublicKey {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: serde::Deserializer<'de>,
187    {
188        let raw = String::deserialize(deserializer)?;
189        let key = PubKey::from_pubkey_string(&raw).map_err(serde::de::Error::custom)?;
190        Ok(Self { raw, key })
191    }
192}
193
194/// Typed pairing peer address newtype (transparent over the wire string).
195#[cfg(not(target_arch = "wasm32"))]
196#[derive(Debug, Clone, serde::Deserialize)]
197#[serde(transparent)]
198struct PairingAddress(String);
199
200#[cfg(not(target_arch = "wasm32"))]
201#[derive(Debug, serde::Deserialize)]
202struct PairingPeerIdentity {
203    kind: PairingIdentityKind,
204    public_key: PairingPublicKey,
205}
206
207#[cfg(not(target_arch = "wasm32"))]
208#[derive(Debug, serde::Deserialize)]
209struct PairingPeer {
210    name: String,
211    address: PairingAddress,
212    identity: PairingPeerIdentity,
213}
214
215/// Typed pairing-client wire messages, parsed once at ingress.
216///
217/// Deserialization fails closed on an unknown `kind`, a missing `version`, a
218/// missing `password_proof`, or a malformed `caller` — replacing the previous
219/// field-by-field raw-JSON `pairing_field` reads.
220#[cfg(not(target_arch = "wasm32"))]
221#[derive(Debug, serde::Deserialize)]
222#[serde(tag = "kind")]
223enum PairingClientMessage {
224    #[serde(rename = "meerkat_pairing_hello")]
225    Hello { version: u32 },
226    #[serde(rename = "meerkat_pairing_proof")]
227    Proof {
228        caller: PairingPeer,
229        password_proof: String,
230    },
231}
232
233#[cfg(not(target_arch = "wasm32"))]
234fn comms_pairing_password_proof(
235    password: &str,
236    challenge: &str,
237    caller_public_key: &str,
238    caller_address: &str,
239) -> String {
240    let mut hasher = Sha256::new();
241    hasher.update(b"meerkat-comms-pairing-v1");
242    hasher.update([0]);
243    hasher.update(password.as_bytes());
244    hasher.update([0]);
245    hasher.update(challenge.as_bytes());
246    hasher.update([0]);
247    hasher.update(caller_public_key.as_bytes());
248    hasher.update([0]);
249    hasher.update(caller_address.as_bytes());
250    BASE64_STANDARD.encode(hasher.finalize())
251}
252
253#[cfg(not(target_arch = "wasm32"))]
254fn constant_time_str_eq(left: &str, right: &str) -> bool {
255    let left = left.as_bytes();
256    let right = right.as_bytes();
257    let mut diff = left.len() ^ right.len();
258    let max = left.len().max(right.len());
259    for idx in 0..max {
260        let left_byte = left.get(idx).copied().unwrap_or_default();
261        let right_byte = right.get(idx).copied().unwrap_or_default();
262        diff |= usize::from(left_byte ^ right_byte);
263    }
264    diff == 0
265}
266
267#[cfg(not(target_arch = "wasm32"))]
268fn validate_pairing_secret(secret: &str) -> Result<(), std::io::Error> {
269    if secret.len() < 32 {
270        return Err(std::io::Error::new(
271            std::io::ErrorKind::InvalidInput,
272            "comms pairing secret must be at least 32 bytes; use a random one-time bootstrap token",
273        ));
274    }
275    Ok(())
276}
277
278/// Derive a stable [`meerkat_core::comms::PeerId`] from a signing pubkey.
279///
280/// Wave-B V5 dogma: `PeerId` is the runtime routing identity and distinct
281/// from the cryptographic pubkey. Until a runtime-managed UUID directory is
282/// wired (Wave-C), we derive the UUID deterministically from the pubkey
283/// bytes via UUID v5 so every surface that resolves the same peer sees the
284/// same `PeerId`, and callers can go round-trip pubkey → PeerId → pubkey by
285/// looking up in the trust store.
286///
287/// Delegates to the router's [`crate::router::peer_id_from_pubkey`] so both
288/// the router and the runtime agree on the UUIDv5 derivation — trust
289/// lookups by `PeerId` stay stable round-trip.
290fn peer_id_from_pubkey(pubkey: &crate::identity::PubKey) -> meerkat_core::comms::PeerId {
291    crate::router::peer_id_from_pubkey(pubkey)
292}
293
294// Only the native TCP listener path and test helpers parse raw addresses;
295// the wasm build has no listener surface.
296#[cfg(any(not(target_arch = "wasm32"), test))]
297fn parse_peer_address(raw: &str) -> Result<meerkat_core::comms::PeerAddress, String> {
298    meerkat_core::comms::PeerAddress::parse(raw).map_err(|err| err.to_string())
299}
300
301/// Convert a core-seam [`TrustedPeerDescriptor`] into the comms-internal
302/// [`TrustEntry`] (which carries a richer typed `PubKey`).
303///
304/// The descriptor carries the Ed25519 pubkey bytes; this helper consumes
305/// them and validates that the derived [`PeerId`] matches the descriptor's
306/// stated routing id (guards against hand-assembled descriptors where the
307/// two identities disagree).
308fn descriptor_to_trust_entry(descriptor: TrustedPeerDescriptor) -> Result<TrustEntry, SendError> {
309    let pubkey = PubKey::new(descriptor.pubkey);
310    if descriptor.has_zero_pubkey() {
311        return Err(SendError::Validation(
312            "TrustedPeerDescriptor.pubkey must be non-zero for trust registration".to_string(),
313        ));
314    }
315    let derived = pubkey.to_peer_id();
316    if derived != descriptor.peer_id {
317        return Err(SendError::Validation(format!(
318            "TrustedPeerDescriptor.peer_id {} does not match pubkey-derived id {}",
319            descriptor.peer_id, derived
320        )));
321    }
322    Ok(TrustEntry {
323        peer_id: descriptor.peer_id,
324        name: descriptor.name,
325        pubkey,
326        address: descriptor.address,
327        meta: crate::PeerMeta::default(),
328    })
329}
330
331impl InteractionStream {
332    fn finish(&mut self) {
333        if let Some(mut receiver) = self.receiver.take() {
334            receiver.close();
335        }
336        match self.stream_handle.as_ref() {
337            Some(handle) => {
338                // DSL-owned path (U6): fire `closed_early`. The transition's
339                // `is_attached` guard rejects when the DSL has already moved
340                // past Attached (e.g., `Completed` won the race), which is
341                // the correct CAS semantics; the cleanup observer drops the
342                // shell-side channel entry under the same authority lock.
343                let corr_id = meerkat_core::PeerCorrelationId::from_uuid(self.id);
344                if let Err(err) = handle.closed_early(corr_id) {
345                    tracing::trace!(
346                        interaction_id = %self.id,
347                        error = %err,
348                        "InteractionStreamHandle::closed_early rejected (likely terminal won race)"
349                    );
350                }
351            }
352            None => {
353                // Transport-only local stream: no semantic lifecycle exists,
354                // so the channel projection owns direct cleanup.
355                self.registry.lock().remove(&self.id);
356            }
357        }
358    }
359}
360
361fn map_peer_ingress_phase(
362    phase: crate::peer_types::PeerIngressState,
363) -> meerkat_core::PeerIngressAuthorityPhase {
364    match phase {
365        crate::peer_types::PeerIngressState::Absent => {
366            meerkat_core::PeerIngressAuthorityPhase::Absent
367        }
368        crate::peer_types::PeerIngressState::Received => {
369            meerkat_core::PeerIngressAuthorityPhase::Received
370        }
371        crate::peer_types::PeerIngressState::Dropped => {
372            meerkat_core::PeerIngressAuthorityPhase::Dropped
373        }
374        crate::peer_types::PeerIngressState::Delivered => {
375            meerkat_core::PeerIngressAuthorityPhase::Delivered
376        }
377    }
378}
379
380impl Stream for InteractionStream {
381    type Item = meerkat_core::EventEnvelope<meerkat_core::AgentEvent>;
382
383    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
384        let this = self.get_mut();
385        match this.receiver.as_mut() {
386            Some(receiver) => match Pin::new(receiver).poll_recv(cx) {
387                Poll::Ready(None) => {
388                    this.finish();
389                    Poll::Ready(None)
390                }
391                Poll::Ready(Some(event)) => {
392                    this.seq = this.seq.saturating_add(1);
393                    let envelope = meerkat_core::EventEnvelope::new_with_source(
394                        this.source.clone(),
395                        this.seq,
396                        None,
397                        event,
398                    );
399                    Poll::Ready(Some(envelope))
400                }
401                Poll::Pending => Poll::Pending,
402            },
403            None => Poll::Ready(None),
404        }
405    }
406}
407
408impl Drop for InteractionStream {
409    fn drop(&mut self) {
410        self.finish();
411    }
412}
413
414#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
415#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
416impl CoreCommsRuntime for CommsRuntime {
417    fn set_outbound_content_taint(
418        &self,
419        taint: Option<meerkat_core::comms::SenderContentTaint>,
420    ) -> Result<(), meerkat_core::comms::SendError> {
421        // Inherent method (host-set carrier config on the router); the dyn
422        // trait surface is how mob/runtime layers holding
423        // `Arc<dyn CoreCommsRuntime>` reach it per member.
424        CommsRuntime::set_outbound_content_taint(self, taint);
425        Ok(())
426    }
427
428    async fn drain_messages(&self) -> Vec<String> {
429        // Delegate through classified drain so messages from the classified
430        // inbox (the sole consumer since 0.4.10) are returned.
431        self.drain_inbox_interactions()
432            .await
433            .into_iter()
434            .map(|i| i.rendered_text)
435            .collect()
436    }
437    fn inbox_notify(&self) -> Arc<tokio::sync::Notify> {
438        self.inbox_notify.clone()
439    }
440
441    fn peer_id(&self) -> Option<PeerId> {
442        Some(self.peer_id)
443    }
444
445    fn public_key(&self) -> Option<String> {
446        Some(self.public_key.to_pubkey_string())
447    }
448
449    fn public_key_bytes(&self) -> Option<[u8; 32]> {
450        Some(*self.public_key.as_bytes())
451    }
452
453    fn comms_name(&self) -> Option<String> {
454        Some(self.participant_name().to_string())
455    }
456
457    fn advertised_address(&self) -> Option<String> {
458        #[cfg(not(target_arch = "wasm32"))]
459        {
460            Some(crate::runtime::comms_runtime::CommsRuntime::advertised_address(self))
461        }
462        #[cfg(target_arch = "wasm32")]
463        {
464            Some(format!("inproc://{}", self.participant_name()))
465        }
466    }
467
468    fn bridge_bootstrap_token(&self) -> Option<String> {
469        Some(self.bridge_bootstrap_token.clone())
470    }
471
472    async fn apply_trust_mutation(
473        &self,
474        mutation: CommsTrustMutation,
475    ) -> Result<CommsTrustMutationResult, SendError> {
476        match mutation {
477            CommsTrustMutation::AddTrustedPeer { peer, authority } => {
478                self.validate_generated_trust_authority_owner(&authority)?;
479                authority
480                    .validate_public_add(self.peer_id(), &peer)
481                    .map_err(SendError::Validation)?;
482                let created = self
483                    .apply_trusted_peer_descriptor(peer, authority.trust_row_owner_kind())
484                    .await?;
485                Ok(CommsTrustMutationResult::Added { created })
486            }
487            CommsTrustMutation::RemoveTrustedPeer { peer_id, authority } => {
488                self.validate_generated_trust_authority_owner(&authority)?;
489                let parsed_peer_id = meerkat_core::comms::PeerId::parse(&peer_id)
490                    .map_err(|err| SendError::Validation(err.to_string()))?;
491                authority
492                    .validate_public_remove(self.peer_id(), parsed_peer_id)
493                    .map_err(SendError::Validation)?;
494                let removed = self
495                    .apply_trusted_peer_removal(&peer_id, authority.trust_row_owner_kind())
496                    .await?;
497                Ok(CommsTrustMutationResult::Removed { removed })
498            }
499            CommsTrustMutation::AddPrivateTrustedPeer { peer, authority } => {
500                self.validate_generated_trust_authority_owner(&authority)?;
501                authority
502                    .validate_private_add(self.peer_id(), &peer)
503                    .map_err(SendError::Validation)?;
504                let created = self
505                    .apply_private_trusted_peer_descriptor(peer, authority.trust_row_owner_kind())
506                    .await?;
507                Ok(CommsTrustMutationResult::Added { created })
508            }
509            CommsTrustMutation::RemovePrivateTrustedPeer { peer_id, authority } => {
510                self.validate_generated_trust_authority_owner(&authority)?;
511                let parsed_peer_id = meerkat_core::comms::PeerId::parse(&peer_id)
512                    .map_err(|err| SendError::Validation(err.to_string()))?;
513                authority
514                    .validate_private_remove(self.peer_id(), parsed_peer_id)
515                    .map_err(SendError::Validation)?;
516                let removed = self
517                    .apply_private_trusted_peer_removal(&peer_id, authority.trust_row_owner_kind())
518                    .await?;
519                Ok(CommsTrustMutationResult::Removed { removed })
520            }
521        }
522    }
523
524    async fn install_generated_mob_trust_owner(
525        &self,
526        owner: Arc<dyn std::any::Any + Send + Sync>,
527    ) -> Result<(), SendError> {
528        let mut expected = self.mob_machine_trust_owner.write();
529        if let Some(existing) = expected.as_ref() {
530            if Arc::ptr_eq(existing, &owner) {
531                return Ok(());
532            }
533            return Err(SendError::Validation(
534                "target runtime is already bound to a different generated MobMachine trust owner"
535                    .to_string(),
536            ));
537        }
538        *expected = Some(owner);
539        Ok(())
540    }
541
542    async fn validate_recovered_generated_mob_trust_owner(
543        &self,
544        owner: Arc<dyn std::any::Any + Send + Sync>,
545    ) -> Result<(), SendError> {
546        let expected = self.mob_machine_trust_owner.read();
547        if let Some(existing) = expected.as_ref()
548            && !Arc::ptr_eq(existing, &owner)
549        {
550            return Err(SendError::Validation(
551                "target runtime is already bound to a different generated MobMachine trust owner"
552                    .to_string(),
553            ));
554        }
555        Ok(())
556    }
557
558    async fn install_recovered_generated_mob_trust_owner(
559        &self,
560        owner: Arc<dyn std::any::Any + Send + Sync>,
561    ) -> Result<(), SendError> {
562        let mut expected = self.mob_machine_trust_owner.write();
563        if let Some(existing) = expected.as_ref() {
564            if Arc::ptr_eq(existing, &owner) {
565                return Ok(());
566            }
567            return Err(SendError::Validation(
568                "target runtime is already bound to a different generated MobMachine trust owner"
569                    .to_string(),
570            ));
571        }
572        *expected = Some(owner);
573        Ok(())
574    }
575
576    fn event_injector(&self) -> Option<Arc<dyn meerkat_core::EventInjector>> {
577        Some(self.event_injector())
578    }
579
580    fn interaction_event_injector(
581        &self,
582    ) -> Option<Arc<dyn meerkat_core::event_injector::SubscribableInjector>> {
583        Some(self.interaction_event_injector())
584    }
585
586    fn stream(&self, scope: StreamScope) -> Result<EventStream, StreamError> {
587        match scope {
588            StreamScope::Session(session_id) => {
589                Err(StreamError::NotFound(format!("session {session_id}")))
590            }
591            StreamScope::Interaction(interaction_id) => {
592                let id = interaction_id.0;
593                let corr_id = meerkat_core::PeerCorrelationId::from_uuid(id);
594
595                // TTL check is shell-owned mechanics: the DSL holds the
596                // lifecycle state for machine-authoritative reservations, but
597                // `created_at` belongs with the channel (DSL state has no
598                // time). Inspect under the registry lock, then fire the typed
599                // cleanup path for the reservation authority that created it.
600                let (expired, lifecycle_authority) = {
601                    let registry = self.interaction_stream_registry.lock();
602                    match registry.get(&id) {
603                        Some(entry) => (
604                            entry.created_at.elapsed() > RESERVATION_TTL,
605                            entry.lifecycle_authority,
606                        ),
607                        None => return Err(StreamError::NotReserved(interaction_id)),
608                    }
609                };
610
611                if expired {
612                    match lifecycle_authority {
613                        InteractionStreamLifecycleAuthority::Machine => {
614                            if let Some(handle) = self.interaction_stream_handle() {
615                                // DSL fires expired → cleanup observer drops
616                                // the shell entry under the same authority
617                                // lock. Ignore guard rejections (state already
618                                // terminal): the shell still surfaces the
619                                // timeout to the caller.
620                                let _ = handle.expired(corr_id);
621                            }
622                        }
623                        InteractionStreamLifecycleAuthority::LocalTransportOnly => {
624                            // Transport-only local stream: no DSL twin exists,
625                            // so the channel projection owns timeout cleanup.
626                            self.interaction_stream_registry.lock().remove(&id);
627                        }
628                    }
629                    // W1-A: if the expired reservation corresponds to a
630                    // live outbound peer-request DSL entry, fire the
631                    // timeout transition so the peer-correlation authority
632                    // state matches reality.
633                    if let Some(handle) = self.peer_interaction_handle()
634                        && handle.outbound_state(corr_id).is_some()
635                    {
636                        let _ = handle.request_timed_out(corr_id);
637                    }
638                    return Err(StreamError::Timeout(format!(
639                        "reservation expired for interaction {}",
640                        interaction_id.0
641                    )));
642                }
643
644                let stream_handle = match lifecycle_authority {
645                    InteractionStreamLifecycleAuthority::Machine => {
646                        Some(self.interaction_stream_handle().ok_or_else(|| {
647                            StreamError::Internal(
648                                "machine interaction stream authority missing for reserved stream"
649                                    .to_string(),
650                            )
651                        })?)
652                    }
653                    InteractionStreamLifecycleAuthority::LocalTransportOnly => None,
654                };
655
656                // Transition to Attached via the DSL handle only for
657                // machine-authoritative reservations; local input streams
658                // consume the receiver as transport-only channels.
659                if let Some(handle) = stream_handle.as_ref() {
660                    handle.attached(corr_id).map_err(|err| {
661                        // The DSL guard distinguishes "not Reserved": if
662                        // state is already Attached it's a duplicate
663                        // attach, otherwise the reservation has terminated.
664                        match handle.state(corr_id) {
665                            Some(meerkat_core::InteractionStreamState::Attached) => {
666                                StreamError::AlreadyAttached(interaction_id)
667                            }
668                            None => StreamError::NotReserved(interaction_id),
669                            _ => StreamError::Internal(format!(
670                                "unexpected interaction stream state for {}: {err}",
671                                interaction_id.0
672                            )),
673                        }
674                    })?;
675                }
676
677                let mut registry = self.interaction_stream_registry.lock();
678                let entry = registry
679                    .get_mut(&id)
680                    .ok_or(StreamError::NotReserved(interaction_id))?;
681                let receiver = entry.receiver.take().ok_or_else(|| {
682                    // Transport-only local stream: no DSL guard ran, so
683                    // double-attach surfaces here as a missing receiver.
684                    if lifecycle_authority == InteractionStreamLifecycleAuthority::Machine {
685                        StreamError::Internal("interaction stream receiver missing".to_string())
686                    } else {
687                        StreamError::AlreadyAttached(interaction_id)
688                    }
689                })?;
690                drop(registry);
691
692                Ok(Box::pin(InteractionStream {
693                    id,
694                    receiver: Some(receiver),
695                    stream_handle,
696                    registry: self.interaction_stream_registry.clone(),
697                    source: meerkat_core::EventSourceIdentity::interaction(
698                        meerkat_core::InteractionId(id),
699                    ),
700                    seq: 0,
701                }))
702            }
703        }
704    }
705
706    async fn send(&self, cmd: CommsCommand) -> Result<SendReceipt, SendError> {
707        match cmd {
708            CommsCommand::Input {
709                body,
710                source,
711                stream,
712                allow_self_session,
713                session_id: _,
714                blocks,
715                handling_mode,
716            } => {
717                // Self-input guard: when allow_self_session is false, this runtime's
718                // inbox is the target, which is a self-loop. Reject unless explicitly
719                // opted in. MCP never exposes this flag.
720                if !allow_self_session {
721                    return Err(SendError::Validation(
722                        "self-session input rejected: set allow_self_session=true to override"
723                            .into(),
724                    ));
725                }
726                match stream {
727                    InputStreamMode::None => {
728                        // Generate the interaction id *first* and stamp it onto the
729                        // injected event so the returned public handle correlates
730                        // with the event — not a fresh unrelated UUID (#269).
731                        let interaction_id = Uuid::new_v4();
732                        let injector = self.event_injector_concrete();
733                        let content = match blocks {
734                            Some(blocks) => meerkat_core::types::ContentInput::Blocks(blocks),
735                            None => meerkat_core::types::ContentInput::Text(body),
736                        };
737                        injector
738                            .inject_with_interaction_id(
739                                interaction_id,
740                                content,
741                                PlainEventSource::from(source),
742                                handling_mode,
743                                None,
744                            )
745                            .map_err(map_event_injector_error)?;
746                        Ok(SendReceipt::InputAccepted {
747                            interaction_id: meerkat_core::InteractionId(interaction_id),
748                            stream_reserved: false,
749                        })
750                    }
751                    InputStreamMode::ReserveInteraction => {
752                        let interaction_id = Uuid::new_v4();
753                        self.register_interaction_stream(
754                            interaction_id,
755                            body,
756                            blocks,
757                            source,
758                            handling_mode,
759                        )?;
760                        Ok(SendReceipt::InputAccepted {
761                            interaction_id: meerkat_core::InteractionId(interaction_id),
762                            stream_reserved: true,
763                        })
764                    }
765                }
766            }
767            CommsCommand::PeerMessage {
768                to,
769                body,
770                blocks,
771                content_taint,
772                handling_mode,
773            } => self
774                .send_peer_command(
775                    &to,
776                    crate::types::MessageKind::Message {
777                        body,
778                        blocks,
779                        content_taint: None,
780                        handling_mode: Some(handling_mode),
781                    },
782                    content_taint,
783                )
784                .await
785                .map(|outcome| SendReceipt::PeerMessageSent {
786                    envelope_id: outcome.envelope_id,
787                    // Verified ACK truth from the delivery path: `true` only
788                    // when a peer ACK was received and verified over a stream
789                    // transport; inproc handoffs report `false`.
790                    acked: outcome.acked,
791                }),
792            CommsCommand::PeerLifecycle { to, kind, params } => self
793                .send_peer_command(
794                    &to,
795                    crate::types::MessageKind::Lifecycle { kind, params },
796                    None,
797                )
798                .await
799                .map(|outcome| SendReceipt::PeerLifecycleSent {
800                    envelope_id: outcome.envelope_id,
801                }),
802            CommsCommand::PeerRequest {
803                to,
804                intent,
805                params,
806                blocks,
807                content_taint,
808                handling_mode,
809                stream,
810            } => {
811                let interaction_id = Uuid::new_v4();
812                let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
813                let stream_reserved = stream == InputStreamMode::ReserveInteraction;
814                let (peer_handle, stream_authority) =
815                    self.require_peer_request_response_authority("PeerRequest")?;
816                let stream_handle = if stream_reserved {
817                    Some(stream_authority)
818                } else {
819                    None
820                };
821
822                if let Err(err) = peer_handle.request_sent(corr_id) {
823                    tracing::warn!(
824                        error = %err,
825                        corr_id = %corr_id,
826                        "PeerInteractionHandle::request_sent rejected — refusing to send"
827                    );
828                    return Err(SendError::Validation(format!(
829                        "DSL rejected PeerRequestSent for corr_id {corr_id}: {err}"
830                    )));
831                }
832
833                if let Some(stream_handle) = stream_handle
834                    && let Err(err) = self.reserve_interaction_stream_channels(
835                        corr_id,
836                        interaction_id,
837                        InteractionStreamReservationAuthority::Machine(stream_handle),
838                    )
839                {
840                    let _ = peer_handle.request_send_failed(corr_id);
841                    return Err(err);
842                }
843
844                let envelope_id = match self
845                    .send_peer_command_with_id(
846                        &to,
847                        interaction_id,
848                        crate::types::MessageKind::Request {
849                            intent,
850                            params,
851                            blocks,
852                            content_taint: None,
853                            handling_mode: Some(handling_mode),
854                        },
855                        content_taint,
856                    )
857                    .await
858                {
859                    Ok(outcome) => outcome.envelope_id,
860                    Err(e) => {
861                        if stream_reserved {
862                            self.expire_interaction_stream_on_send_failure(corr_id, interaction_id);
863                        }
864                        // A send failure is NOT a timeout: the typed `SendError`
865                        // `e` is returned to the caller verbatim (peer offline /
866                        // transport / admission-drop), never laundered into a
867                        // timeout receipt. The DSL cleanup below removes the
868                        // pending entry; the authoritative send-failure
869                        // disposition is owned by the typed error returned here.
870                        let _ = peer_handle.request_send_failed(corr_id);
871                        return Err(e);
872                    }
873                };
874
875                Ok(SendReceipt::PeerRequestSent {
876                    envelope_id,
877                    interaction_id: meerkat_core::InteractionId(interaction_id),
878                    stream_reserved,
879                })
880            }
881            CommsCommand::PeerResponse {
882                to,
883                in_reply_to,
884                status,
885                result,
886                blocks,
887                content_taint,
888                handling_mode,
889            } => {
890                let (peer_handle, _stream_authority) =
891                    self.require_peer_request_response_authority("PeerResponse")?;
892                let corr_id = meerkat_core::PeerCorrelationId::from_uuid(in_reply_to.0);
893                let core_status = status;
894                let status = crate::Status::from(status);
895                if peer_handle.inbound_state(corr_id).is_none() {
896                    return Err(SendError::Validation(format!(
897                        "PeerResponse requires machine inbound peer request state for corr_id {corr_id}"
898                    )));
899                }
900                let reply_terminality =
901                    peer_handle
902                        .classify_response_reply(core_status)
903                        .map_err(|err| {
904                            SendError::Validation(format!(
905                                "DSL rejected PeerResponse reply classification for corr_id {corr_id}: {err}"
906                            ))
907                        })?;
908                let is_terminal_reply = matches!(
909                    reply_terminality,
910                    meerkat_core::TerminalityClass::Terminal { .. }
911                );
912                if !is_terminal_reply && handling_mode.is_some() {
913                    return Err(SendError::Validation(
914                        "handling_mode is forbidden on progress peer responses".to_string(),
915                    ));
916                }
917                let effective_handling_mode = if is_terminal_reply {
918                    // Preflight the inbound lifecycle STATE before the
919                    // irreversible transport send. The terminal `response_replied`
920                    // transition applied after the send commits a
921                    // `PeerResponseReplied` guarded on the `inbound_peer_requests`
922                    // state being `Received` — a DIFFERENT substate than the
923                    // handling-mode lane (`inbound_peer_request_lanes`) checked
924                    // below. Without this state check, a recorded lane could pass
925                    // the lane preflight while the request is no longer repliable,
926                    // so the post-send `response_replied` would reject AFTER the
927                    // remote has already seen the terminal response — a split
928                    // where the remote considers the request answered but local
929                    // authority reports rejection. Validating repliability here
930                    // keeps the send and the authority transition consistent
931                    // (the post-send transition cannot reject under the
932                    // single-owner session task).
933                    if peer_handle.inbound_state(corr_id)
934                        != Some(meerkat_core::InboundPeerRequestState::Received)
935                    {
936                        return Err(SendError::Validation(format!(
937                            "PeerResponse terminal reply for corr_id {corr_id} is not in a repliable inbound state; response not sent"
938                        )));
939                    }
940                    let resolved =
941                        handling_mode.or_else(|| peer_handle.inbound_handling_mode(corr_id));
942                    if resolved.is_none() {
943                        return Err(SendError::Validation(format!(
944                            "PeerResponse requires machine inbound peer request handling mode for corr_id {corr_id}"
945                        )));
946                    }
947                    resolved
948                } else {
949                    None
950                };
951                let envelope_id = self
952                    .send_peer_command(
953                        &to,
954                        crate::types::MessageKind::Response {
955                            in_reply_to: in_reply_to.0,
956                            status,
957                            result,
958                            blocks,
959                            content_taint: None,
960                            handling_mode: effective_handling_mode,
961                        },
962                        content_taint,
963                    )
964                    .await?
965                    .envelope_id;
966
967                // The terminal inbound transition is coupled to the actual
968                // response send outcome: route/transport failure leaves the
969                // request in `Received` so the responder can retry.
970                if is_terminal_reply && let Err(err) = peer_handle.response_replied(corr_id) {
971                    tracing::warn!(
972                        error = %err,
973                        corr_id = %corr_id,
974                        "PeerInteractionHandle::response_replied rejected"
975                    );
976                    return Err(SendError::Validation(format!(
977                        "DSL rejected PeerResponseReplied for corr_id {corr_id}: {err}"
978                    )));
979                }
980                Ok(SendReceipt::PeerResponseSent {
981                    envelope_id,
982                    in_reply_to,
983                })
984            }
985        }
986    }
987
988    async fn send_and_stream(
989        &self,
990        cmd: CommsCommand,
991    ) -> Result<(SendReceipt, EventStream), SendAndStreamError> {
992        match cmd {
993            CommsCommand::Input {
994                body,
995                source,
996                stream: InputStreamMode::ReserveInteraction,
997                allow_self_session,
998                session_id: _,
999                blocks,
1000                handling_mode,
1001            } => {
1002                if !allow_self_session {
1003                    return Err(SendAndStreamError::Send(SendError::Validation(
1004                        "self-session input rejected: set allow_self_session=true to override"
1005                            .into(),
1006                    )));
1007                }
1008                let interaction_id = Uuid::new_v4();
1009                self.register_interaction_stream(
1010                    interaction_id,
1011                    body,
1012                    blocks,
1013                    source,
1014                    handling_mode,
1015                )?;
1016                let receipt = SendReceipt::InputAccepted {
1017                    interaction_id: meerkat_core::InteractionId(interaction_id),
1018                    stream_reserved: true,
1019                };
1020                let stream = self
1021                    .stream(StreamScope::Interaction(meerkat_core::InteractionId(
1022                        interaction_id,
1023                    )))
1024                    .map_err(|error| SendAndStreamError::StreamAttach {
1025                        receipt: receipt.clone(),
1026                        error,
1027                    })?;
1028                Ok((receipt, stream))
1029            }
1030            CommsCommand::PeerRequest {
1031                to,
1032                intent,
1033                params,
1034                blocks,
1035                content_taint,
1036                handling_mode,
1037                stream: InputStreamMode::ReserveInteraction,
1038            } => {
1039                // Reserve under the canonical request envelope id so the same
1040                // id can be used for stream attachment and reply correlation.
1041                let interaction_id = Uuid::new_v4();
1042                let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
1043                let (peer_handle, stream_handle) =
1044                    self.require_peer_request_response_authority("PeerRequest")?;
1045
1046                if let Err(err) = peer_handle.request_sent(corr_id) {
1047                    tracing::warn!(
1048                        error = %err,
1049                        corr_id = %corr_id,
1050                        "PeerInteractionHandle::request_sent rejected — refusing to send"
1051                    );
1052                    return Err(SendAndStreamError::Send(SendError::Validation(format!(
1053                        "DSL rejected PeerRequestSent for corr_id {corr_id}: {err}"
1054                    ))));
1055                }
1056
1057                if let Err(err) = self.reserve_interaction_stream_channels(
1058                    corr_id,
1059                    interaction_id,
1060                    InteractionStreamReservationAuthority::Machine(stream_handle),
1061                ) {
1062                    let _ = peer_handle.request_send_failed(corr_id);
1063                    return Err(SendAndStreamError::Send(err));
1064                }
1065
1066                let envelope_id = match self
1067                    .send_peer_command_with_id(
1068                        &to,
1069                        interaction_id,
1070                        crate::types::MessageKind::Request {
1071                            intent,
1072                            params,
1073                            blocks,
1074                            content_taint: None,
1075                            handling_mode: Some(handling_mode),
1076                        },
1077                        content_taint,
1078                    )
1079                    .await
1080                {
1081                    Ok(outcome) => outcome.envelope_id,
1082                    Err(e) => {
1083                        self.expire_interaction_stream_on_send_failure(corr_id, interaction_id);
1084                        // Send failure returns the typed `SendError` verbatim;
1085                        // it is never laundered into a timeout receipt. DSL
1086                        // cleanup removes the pending entry.
1087                        let _ = peer_handle.request_send_failed(corr_id);
1088                        return Err(SendAndStreamError::Send(e));
1089                    }
1090                };
1091
1092                let receipt = SendReceipt::PeerRequestSent {
1093                    envelope_id,
1094                    interaction_id: meerkat_core::InteractionId(interaction_id),
1095                    stream_reserved: true,
1096                };
1097                let event_stream = self
1098                    .stream(StreamScope::Interaction(meerkat_core::InteractionId(
1099                        interaction_id,
1100                    )))
1101                    .map_err(|error| SendAndStreamError::StreamAttach {
1102                        receipt: receipt.clone(),
1103                        error,
1104                    })?;
1105                Ok((receipt, event_stream))
1106            }
1107            other => {
1108                let receipt = self.send(other).await?;
1109                Err(SendAndStreamError::StreamAttach {
1110                    receipt,
1111                    error: StreamError::NotFound("command is not streamable".to_string()),
1112                })
1113            }
1114        }
1115    }
1116
1117    async fn peers(&self) -> Vec<PeerDirectoryEntry> {
1118        self.resolve_peer_directory().await
1119    }
1120
1121    async fn peer_count(&self) -> usize {
1122        self.resolve_peer_count().await
1123    }
1124
1125    async fn drain_inbox_interactions(&self) -> Vec<meerkat_core::InboxInteraction> {
1126        // Delegate to classified drain and strip classification metadata.
1127        // This ensures a single drain path: classified queue is the sole consumer.
1128        self.drain_classified_inbox_interactions()
1129            .await
1130            .unwrap_or_default()
1131            .into_iter()
1132            .map(|ci| ci.interaction)
1133            .collect()
1134    }
1135
1136    fn interaction_subscriber(
1137        &self,
1138        id: &meerkat_core::InteractionId,
1139    ) -> Option<tokio::sync::mpsc::Sender<meerkat_core::AgentEvent>> {
1140        let sender = self.subscriber_registry.lock().remove(&id.0);
1141        sender.as_ref()?;
1142
1143        // If still Reserved (consumer never attached), expire the reservation
1144        // through the same typed authority that created it.
1145        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(id.0);
1146        let lifecycle_authority = self
1147            .interaction_stream_registry
1148            .lock()
1149            .get(&id.0)
1150            .map(|entry| entry.lifecycle_authority);
1151        match lifecycle_authority {
1152            Some(InteractionStreamLifecycleAuthority::Machine) => {
1153                if let Some(handle) = self.interaction_stream_handle()
1154                    && handle.state(corr_id) == Some(meerkat_core::InteractionStreamState::Reserved)
1155                {
1156                    let _ = handle.expired(corr_id);
1157                }
1158            }
1159            Some(InteractionStreamLifecycleAuthority::LocalTransportOnly) => {
1160                // Transport-only local stream: expire the unclaimed channel
1161                // directly because no semantic lifecycle was created.
1162                let mut registry = self.interaction_stream_registry.lock();
1163                if let Some(entry) = registry.get(&id.0)
1164                    && entry.receiver.is_some()
1165                {
1166                    registry.remove(&id.0);
1167                }
1168            }
1169            None => {}
1170        }
1171        sender
1172    }
1173
1174    fn mark_interaction_complete(&self, id: &meerkat_core::InteractionId) {
1175        self.mark_interaction_complete(id.0);
1176    }
1177
1178    fn peer_interaction_handle(
1179        &self,
1180    ) -> Option<Arc<dyn meerkat_core::handles::PeerInteractionHandle>> {
1181        self.peer_interaction_handle()
1182    }
1183
1184    fn peer_request_response_authority_handle(
1185        &self,
1186    ) -> Option<Arc<dyn meerkat_core::handles::PeerInteractionHandle>> {
1187        let peer_handle = self.peer_interaction_handle();
1188        if peer_handle.is_some() && self.interaction_stream_handle().is_some() {
1189            peer_handle
1190        } else {
1191            None
1192        }
1193    }
1194
1195    async fn drain_classified_inbox_interactions(
1196        &self,
1197    ) -> Result<Vec<meerkat_core::ClassifiedInboxInteraction>, meerkat_core::CommsCapabilityError>
1198    {
1199        use crate::agent::types::MessageIntent;
1200        use crate::types::MessageKind;
1201
1202        let mut inbox = self.inbox.lock().await;
1203        let classified_entries = inbox.try_drain_classified();
1204
1205        if classified_entries.is_empty() {
1206            return Ok(Vec::new());
1207        }
1208
1209        // Use stored classification metadata — NO trust re-check.
1210        // Snapshot semantics: classification was fixed at enqueue time.
1211        Ok(classified_entries
1212            .into_iter()
1213            .filter_map(|entry| {
1214                let ingress = entry.ingress_fact;
1215                let lifecycle_peer = entry.lifecycle_peer;
1216                let from_peer = entry.from_peer.unwrap_or_else(|| "unknown".to_string());
1217                let from_peer_id = ingress.route.as_ref().map(|route| route.peer_id);
1218                let rendered_text = entry.text_projection.clone();
1219
1220                match entry.item {
1221                    crate::types::InboxItem::External { envelope } => {
1222                        // Runtime lifecycle dismissal does NOT flow through a
1223                        // peer message body. A peer-controlled string can never
1224                        // stop the local executor; dismissal must arrive as a
1225                        // typed lifecycle signal owned by the runtime authority.
1226                        // Extract handling_mode before consuming the kind.
1227                        // `send_response` may omit this field, so the sender
1228                        // side defaults it from the original request mode.
1229                        let envelope_handling_mode = match &envelope.kind {
1230                            MessageKind::Message {
1231                                handling_mode: Some(mode),
1232                                ..
1233                            }
1234                            | MessageKind::Request {
1235                                handling_mode: Some(mode),
1236                                ..
1237                            }
1238                            | MessageKind::Response {
1239                                handling_mode: Some(mode),
1240                                ..
1241                            } => *mode,
1242                            MessageKind::Response {
1243                                handling_mode: None,
1244                                ..
1245                            }
1246                            | MessageKind::Message {
1247                                handling_mode: None,
1248                                ..
1249                            }
1250                            | MessageKind::Request {
1251                                handling_mode: None,
1252                                ..
1253                            }
1254                            | MessageKind::Lifecycle { .. }
1255                            | MessageKind::Ack { .. } => meerkat_core::types::HandlingMode::Queue,
1256                        };
1257                        // Sender-declared content taint travels with the
1258                        // content facts (signed-when-present on the envelope);
1259                        // extract it before the kind is consumed below.
1260                        let sender_taint = envelope.kind.content_taint();
1261                        let content = match envelope.kind {
1262                            MessageKind::Message {
1263                                body,
1264                                blocks,
1265                                content_taint: _,
1266                                handling_mode: _,
1267                            } => meerkat_core::InteractionContent::Message { body, blocks },
1268                            MessageKind::Request {
1269                                intent,
1270                                params,
1271                                blocks,
1272                                content_taint: _,
1273                                handling_mode: _,
1274                            } => {
1275                                let typed_intent = MessageIntent::from(intent.as_str());
1276                                meerkat_core::InteractionContent::Request {
1277                                    intent: typed_intent.to_string(),
1278                                    params,
1279                                    blocks,
1280                                }
1281                            }
1282                            MessageKind::Lifecycle { kind, params } => {
1283                                meerkat_core::InteractionContent::Request {
1284                                    intent: kind.to_string(),
1285                                    params,
1286                                    blocks: None,
1287                                }
1288                            }
1289                            MessageKind::Response {
1290                                in_reply_to,
1291                                status,
1292                                result,
1293                                blocks,
1294                                content_taint: _,
1295                                handling_mode: _,
1296                            } => {
1297                                let core_status = match status {
1298                                    crate::types::Status::Accepted => {
1299                                        meerkat_core::ResponseStatus::Accepted
1300                                    }
1301                                    crate::types::Status::Completed => {
1302                                        meerkat_core::ResponseStatus::Completed
1303                                    }
1304                                    crate::types::Status::Failed => {
1305                                        meerkat_core::ResponseStatus::Failed
1306                                    }
1307                                };
1308                                meerkat_core::InteractionContent::Response {
1309                                    in_reply_to: meerkat_core::InteractionId(in_reply_to),
1310                                    status: core_status,
1311                                    result,
1312                                    blocks,
1313                                }
1314                            }
1315                            MessageKind::Ack { .. } => {
1316                                // Acks should never reach classified drain
1317                                return None;
1318                            }
1319                        };
1320
1321                        Some(meerkat_core::ClassifiedInboxInteraction {
1322                            interaction: meerkat_core::InboxInteraction {
1323                                id: meerkat_core::InteractionId(envelope.id),
1324                                from_route: from_peer_id,
1325                                from: from_peer,
1326                                content,
1327                                rendered_text,
1328                                handling_mode: envelope_handling_mode,
1329                                render_metadata: None,
1330                                sender_taint,
1331                            },
1332                            ingress,
1333                            lifecycle_peer,
1334                            response_terminality: entry.response_terminality,
1335                        })
1336                    }
1337                    crate::types::InboxItem::PlainEvent {
1338                        body,
1339                        source,
1340                        handling_mode,
1341                        interaction_id,
1342                        blocks,
1343                        render_metadata,
1344                        ..
1345                    } => Some(meerkat_core::ClassifiedInboxInteraction {
1346                        interaction: meerkat_core::InboxInteraction {
1347                            id: meerkat_core::InteractionId(
1348                                interaction_id.unwrap_or_else(uuid::Uuid::new_v4),
1349                            ),
1350                            from_route: None,
1351                            from: format!("event:{source}"),
1352                            content: meerkat_core::InteractionContent::Message { body, blocks },
1353                            rendered_text,
1354                            handling_mode,
1355                            render_metadata,
1356                            // Plain events are host-injected, not peer
1357                            // envelopes: no sender declaration exists.
1358                            sender_taint: None,
1359                        },
1360                        ingress,
1361                        lifecycle_peer,
1362                        response_terminality: entry.response_terminality,
1363                    }),
1364                }
1365            })
1366            .collect())
1367    }
1368
1369    async fn peer_ingress_queue_snapshot(
1370        &self,
1371    ) -> Result<meerkat_core::PeerIngressQueueSnapshot, meerkat_core::CommsCapabilityError> {
1372        let inbox = self.inbox.lock().await;
1373        inbox.classified_snapshot().ok_or_else(|| {
1374            meerkat_core::CommsCapabilityError::Unsupported(
1375                "peer_ingress_queue_snapshot: classified inbox not initialized".to_string(),
1376            )
1377        })
1378    }
1379
1380    async fn peer_ingress_runtime_snapshot(
1381        &self,
1382    ) -> Result<meerkat_core::PeerIngressRuntimeSnapshot, meerkat_core::CommsCapabilityError> {
1383        let (queue, authority_phase, submission_queue_len) = {
1384            let inbox = self.inbox.lock().await;
1385            inbox.peer_runtime_snapshot().ok_or_else(|| {
1386                meerkat_core::CommsCapabilityError::Unsupported(
1387                    "peer_ingress_runtime_snapshot: classified inbox not initialized".to_string(),
1388                )
1389            })?
1390        };
1391        let trusted_peers = self.trusted_peer_descriptors(true);
1392
1393        Ok(meerkat_core::PeerIngressRuntimeSnapshot {
1394            self_peer_id: peer_id_from_pubkey(&self.public_key),
1395            auth_required: self.require_peer_auth,
1396            authority_phase: map_peer_ingress_phase(authority_phase),
1397            trusted_peers,
1398            submission_queue_len,
1399            queue,
1400        })
1401    }
1402
1403    async fn public_trusted_peer_projection_snapshot(
1404        &self,
1405    ) -> Result<Vec<TrustedPeerDescriptor>, meerkat_core::CommsCapabilityError> {
1406        Ok(self.trusted_peer_descriptors(false))
1407    }
1408
1409    async fn trusted_peer_projection_snapshot_for_source(
1410        &self,
1411        source_kind: GeneratedCommsTrustAuthoritySourceKind,
1412    ) -> Result<Vec<TrustedPeerDescriptor>, meerkat_core::CommsCapabilityError> {
1413        Ok(self.trusted_peer_descriptors_for_source(false, source_kind))
1414    }
1415
1416    fn actionable_input_notify(
1417        &self,
1418    ) -> Result<Arc<tokio::sync::Notify>, meerkat_core::CommsCapabilityError> {
1419        self.actionable_notify.clone().ok_or_else(|| {
1420            meerkat_core::CommsCapabilityError::Unsupported(
1421                "actionable_input_notify: classified inbox not initialized".to_string(),
1422            )
1423        })
1424    }
1425}
1426
1427fn map_event_injector_error(error: meerkat_core::event_injector::EventInjectorError) -> SendError {
1428    match error {
1429        meerkat_core::event_injector::EventInjectorError::Full => {
1430            SendError::Validation("input queue full".into())
1431        }
1432        meerkat_core::event_injector::EventInjectorError::Closed => SendError::InputClosed,
1433    }
1434}
1435
1436#[derive(Debug, Error)]
1437pub enum CommsRuntimeError {
1438    #[error("Identity error: {0}")]
1439    IdentityError(String),
1440    #[error("Session identity already active: {0}")]
1441    SessionIdentityInUse(SessionId),
1442    #[error("Trust load error: {0}")]
1443    TrustLoadError(String),
1444    #[error("Listener error: {0}")]
1445    ListenerError(#[from] std::io::Error),
1446    #[error("Listeners already started")]
1447    AlreadyStarted,
1448    #[error("Unsafe binding: {0}")]
1449    UnsafeBinding(String),
1450    #[error("Inproc registration rejected: {0:?}")]
1451    InprocRegistrationRejected(crate::RegistrationRejection),
1452}
1453
1454/// Observe a constructor-time inproc registration outcome and fail closed on
1455/// rejection.
1456///
1457/// A freshly constructed runtime binds its own (non-zero) keypair under its
1458/// participant name. A clean [`RegistrationOutcome::Registered`] is the
1459/// expected result. A name/pubkey displacement is surfaced as a typed warning
1460/// (legitimate during a same-name restart), and a hard rejection fails the
1461/// constructor closed rather than returning a runtime whose route was never
1462/// installed.
1463fn observe_inproc_registration(
1464    namespace: &str,
1465    name: &str,
1466    outcome: crate::RegistrationOutcome,
1467) -> Result<(), CommsRuntimeError> {
1468    use crate::RegistrationOutcome;
1469    match outcome {
1470        RegistrationOutcome::Registered => Ok(()),
1471        RegistrationOutcome::ReplacedPubkey { evicted_name } => {
1472            tracing::warn!(
1473                inproc_namespace = %namespace,
1474                peer_name = %name,
1475                %evicted_name,
1476                "inproc registration replaced an existing route under this pubkey"
1477            );
1478            Ok(())
1479        }
1480        RegistrationOutcome::EvictedName { evicted_pubkey } => {
1481            tracing::warn!(
1482                inproc_namespace = %namespace,
1483                peer_name = %name,
1484                evicted_pubkey = %evicted_pubkey.to_pubkey_string(),
1485                "inproc registration evicted a stale pubkey bound to this name"
1486            );
1487            Ok(())
1488        }
1489        RegistrationOutcome::ReplacedPubkeyAndEvictedName {
1490            evicted_name,
1491            evicted_pubkey,
1492        } => {
1493            tracing::warn!(
1494                inproc_namespace = %namespace,
1495                peer_name = %name,
1496                %evicted_name,
1497                evicted_pubkey = %evicted_pubkey.to_pubkey_string(),
1498                "inproc registration displaced both a name and a pubkey route"
1499            );
1500            Ok(())
1501        }
1502        RegistrationOutcome::Rejected { reason } => {
1503            Err(CommsRuntimeError::InprocRegistrationRejected(reason))
1504        }
1505    }
1506}
1507
1508#[cfg(not(target_arch = "wasm32"))]
1509impl From<SessionClaimError> for CommsRuntimeError {
1510    fn from(err: SessionClaimError) -> Self {
1511        match err {
1512            SessionClaimError::SessionIdentityInUse(sid) => Self::SessionIdentityInUse(sid),
1513        }
1514    }
1515}
1516
1517/// Material needed by the facade's comms tool composition helper.
1518///
1519/// Bundles the concrete router/trust/key access so the facade doesn't
1520/// reach into CommsRuntime internals directly. Construct via
1521/// [`CommsRuntime::tool_material()`].
1522pub struct CommsToolMaterial {
1523    router: Arc<Router>,
1524    trusted_peers: TrustedPeersView,
1525    self_pubkey: crate::identity::PubKey,
1526    runtime: Arc<dyn meerkat_core::agent::CommsRuntime>,
1527}
1528
1529impl CommsToolMaterial {
1530    /// Router for tool dispatch.
1531    pub fn router(&self) -> &Arc<Router> {
1532        &self.router
1533    }
1534    /// Live trust state for peer availability gating.
1535    pub fn trusted_peers(&self) -> &TrustedPeersView {
1536        &self.trusted_peers
1537    }
1538    /// Owned clone of the read-only trust view (for availability closures).
1539    pub fn trusted_peers_shared(&self) -> TrustedPeersView {
1540        self.trusted_peers.clone()
1541    }
1542    /// This runtime's public key.
1543    pub fn self_pubkey(&self) -> crate::identity::PubKey {
1544        self.self_pubkey
1545    }
1546    /// Trait-erased runtime for the comms tool surface.
1547    pub fn into_runtime(self) -> Arc<dyn meerkat_core::agent::CommsRuntime> {
1548        self.runtime
1549    }
1550}
1551
1552#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
1553pub struct CommsRuntime {
1554    public_key: PubKey,
1555    peer_id: PeerId,
1556    router: Arc<Router>,
1557    trusted_peers: Arc<parking_lot::RwLock<TrustStore>>,
1558    inbox: Arc<AsyncMutex<crate::Inbox>>,
1559    inbox_notify: Arc<tokio::sync::Notify>,
1560    #[cfg(not(target_arch = "wasm32"))]
1561    config: ResolvedCommsConfig,
1562    /// Participant name (wasm32-only; on native this comes from `config.name`).
1563    #[cfg(target_arch = "wasm32")]
1564    name: String,
1565    /// Inproc namespace (wasm32-only; on native this comes from `config.inproc_namespace`).
1566    #[cfg(target_arch = "wasm32")]
1567    inproc_namespace: Option<String>,
1568    #[cfg(not(target_arch = "wasm32"))]
1569    listener_handles: Mutex<Vec<ListenerHandle>>,
1570    #[cfg(not(target_arch = "wasm32"))]
1571    listeners_started: bool,
1572    #[cfg(not(target_arch = "wasm32"))]
1573    _session_identity_claim: Option<SessionClaim>,
1574    keypair: Arc<Keypair>,
1575    bridge_bootstrap_token: String,
1576    require_peer_auth: bool,
1577    subscriber_registry: crate::event_injector::SubscriberRegistry,
1578    interaction_stream_registry: InteractionStreamRegistry,
1579    peer_comms_handle: crate::classify::PeerCommsHandleSlot,
1580    meerkat_machine_trust_owner:
1581        parking_lot::RwLock<Option<meerkat_core::comms::GeneratedPeerCommsOwnerToken>>,
1582    mob_machine_trust_owner: parking_lot::RwLock<Option<Arc<dyn std::any::Any + Send + Sync>>>,
1583    require_peer_comms_machine_authority: Arc<AtomicBool>,
1584    /// Narrow notify that fires only for actionable peer input (messages/requests).
1585    /// Set during construction when classified inbox is used.
1586    actionable_notify: Option<Arc<tokio::sync::Notify>>,
1587    blob_store: Option<Arc<dyn BlobStore>>,
1588    /// Session-scoped peer-interaction DSL handle (W1-A). When this is absent,
1589    /// the runtime is explicitly transport-only for semantic peer
1590    /// request/response: it may carry one-way peer messages, but it must not
1591    /// emit `PeerRequestSent` or `PeerResponseSent` receipts.
1592    peer_interaction_handle:
1593        parking_lot::RwLock<Option<Arc<dyn meerkat_core::handles::PeerInteractionHandle>>>,
1594    /// Session-scoped interaction-stream lifecycle DSL handle (U6 / dogma #5).
1595    ///
1596    /// When `Some`, the shell's `interaction_stream_registry` is a pure
1597    /// projection of the DSL `interaction_streams` map: stream(),
1598    /// mark_interaction_complete, reap_expired_reservations, and the
1599    /// `InteractionStream::finish` close path all fire DSL inputs; the
1600    /// installed cleanup observer drops registry entries on terminal
1601    /// transitions. When `None`, only local transport-only input streams may
1602    /// use the registry directly; peer request/response stream reservations
1603    /// require this handle.
1604    interaction_stream_handle:
1605        parking_lot::RwLock<Option<Arc<dyn meerkat_core::handles::InteractionStreamHandle>>>,
1606}
1607
1608impl CommsRuntime {
1609    fn derive_bridge_bootstrap_token(keypair: &Keypair) -> String {
1610        let mut digest = Sha256::new();
1611        digest.update(b"meerkat.supervisor-bridge.bootstrap-token.v1");
1612        digest.update(keypair.secret_bytes());
1613        base64::Engine::encode(
1614            &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1615            digest.finalize(),
1616        )
1617    }
1618
1619    #[cfg(not(target_arch = "wasm32"))]
1620    fn reject_persisted_trusted_peers_seed(
1621        path: &std::path::Path,
1622    ) -> Result<(), CommsRuntimeError> {
1623        let peers = TrustStore::load_or_default(path)
1624            .map_err(|err| CommsRuntimeError::TrustLoadError(err.to_string()))?;
1625        if peers.has_peers() {
1626            return Err(CommsRuntimeError::TrustLoadError(format!(
1627                "persisted trusted peer seed at {} is not authoritative; generated comms trust mutation authority required",
1628                path.display()
1629            )));
1630        }
1631        Ok(())
1632    }
1633
1634    #[cfg(not(target_arch = "wasm32"))]
1635    pub async fn new(config: ResolvedCommsConfig) -> Result<Self, CommsRuntimeError> {
1636        Self::new_with_silent_intents(config, Arc::new(HashSet::new())).await
1637    }
1638
1639    #[cfg(not(target_arch = "wasm32"))]
1640    pub async fn new_with_silent_intents(
1641        config: ResolvedCommsConfig,
1642        silent_intents: Arc<HashSet<String>>,
1643    ) -> Result<Self, CommsRuntimeError> {
1644        Self::new_with_silent_intents_and_machine_authority_requirement(
1645            config,
1646            silent_intents,
1647            false,
1648        )
1649        .await
1650    }
1651
1652    #[cfg(not(target_arch = "wasm32"))]
1653    pub async fn new_machine_authority_required_with_silent_intents(
1654        config: ResolvedCommsConfig,
1655        silent_intents: Arc<HashSet<String>>,
1656    ) -> Result<Self, CommsRuntimeError> {
1657        Self::new_with_silent_intents_and_machine_authority_requirement(
1658            config,
1659            silent_intents,
1660            true,
1661        )
1662        .await
1663    }
1664
1665    #[cfg(not(target_arch = "wasm32"))]
1666    async fn new_with_silent_intents_and_machine_authority_requirement(
1667        config: ResolvedCommsConfig,
1668        _silent_intents: Arc<HashSet<String>>,
1669        require_machine_authority: bool,
1670    ) -> Result<Self, CommsRuntimeError> {
1671        // Always load keypair regardless of auth mode. Trust rows are not
1672        // seeded from config; live trust changes must arrive through generated
1673        // machine/composition authority.
1674        let keypair = Keypair::load_or_generate(&config.identity_dir)
1675            .await
1676            .map_err(|e| CommsRuntimeError::IdentityError(e.to_string()))?;
1677        Self::reject_persisted_trusted_peers_seed(&config.trusted_peers_path)?;
1678        let trusted_peers = TrustStore::new();
1679        let public_key = keypair.public_key();
1680        // Single source of truth for trust state — shared internally by the
1681        // Router and IngressClassificationContext.
1682        let trusted_peers = Arc::new(parking_lot::RwLock::new(trusted_peers));
1683
1684        // Build classified inbox using the same trusted_peers Arc
1685        let peer_comms_handle = Arc::new(parking_lot::RwLock::new(None));
1686        let require_peer_comms_machine_authority =
1687            Arc::new(AtomicBool::new(require_machine_authority));
1688        let classification_context = Arc::new(crate::classify::IngressClassificationContext {
1689            require_peer_auth: config.require_peer_auth,
1690            trusted_peers: trusted_peers.clone(),
1691            peer_comms_handle: peer_comms_handle.clone(),
1692            inproc_namespace: config.inproc_namespace.clone(),
1693        });
1694        let (inbox, inbox_sender) = crate::Inbox::new_classified(classification_context);
1695        let inbox_notify = inbox.notify();
1696        let actionable_notify = inbox.classified_actionable_notify();
1697
1698        let router = Router::with_shared_peers(
1699            keypair.clone(),
1700            trusted_peers.clone(),
1701            config.comms_config.clone(),
1702            inbox_sender.clone(),
1703            config.require_peer_auth,
1704        )
1705        .with_inproc_namespace(config.inproc_namespace.clone());
1706
1707        let runtime = Self {
1708            public_key,
1709            peer_id: public_key.to_peer_id(),
1710            router: Arc::new(router),
1711            trusted_peers,
1712            inbox: Arc::new(AsyncMutex::new(inbox)),
1713            inbox_notify,
1714            config: config.clone(),
1715            listener_handles: Mutex::new(Vec::new()),
1716            listeners_started: false,
1717            _session_identity_claim: None,
1718            bridge_bootstrap_token: Self::derive_bridge_bootstrap_token(&keypair),
1719            keypair: Arc::new(keypair),
1720            require_peer_auth: config.require_peer_auth,
1721            subscriber_registry: crate::event_injector::new_subscriber_registry(),
1722            interaction_stream_registry: Arc::new(Mutex::new(HashMap::new())),
1723            peer_comms_handle,
1724            meerkat_machine_trust_owner: parking_lot::RwLock::new(None),
1725            mob_machine_trust_owner: parking_lot::RwLock::new(None),
1726            require_peer_comms_machine_authority,
1727            actionable_notify,
1728            blob_store: None,
1729            peer_interaction_handle: parking_lot::RwLock::new(None),
1730            interaction_stream_handle: parking_lot::RwLock::new(None),
1731        };
1732        let namespace = config.inproc_namespace.clone().unwrap_or_default();
1733        let name = config.name.clone();
1734        let outcome = InprocRegistry::global().register_with_meta_in_namespace(
1735            &namespace,
1736            &name,
1737            runtime.public_key,
1738            inbox_sender,
1739            crate::PeerMeta::default(),
1740        );
1741        observe_inproc_registration(&namespace, &name, outcome)?;
1742        Ok(runtime)
1743    }
1744
1745    pub fn inproc_only(name: &str) -> Result<Self, CommsRuntimeError> {
1746        Self::inproc_only_with_silent_intents(name, None, Arc::new(HashSet::new()))
1747    }
1748
1749    pub fn inproc_only_scoped(
1750        name: &str,
1751        namespace: Option<String>,
1752    ) -> Result<Self, CommsRuntimeError> {
1753        Self::inproc_only_with_silent_intents(name, namespace, Arc::new(HashSet::new()))
1754    }
1755
1756    pub fn inproc_only_with_silent_intents(
1757        name: &str,
1758        namespace: Option<String>,
1759        silent_intents: Arc<HashSet<String>>,
1760    ) -> Result<Self, CommsRuntimeError> {
1761        let keypair = Keypair::generate();
1762        Self::inproc_only_with_keypair_and_silent_intents(name, namespace, keypair, silent_intents)
1763    }
1764
1765    pub fn inproc_only_with_keypair_and_silent_intents(
1766        name: &str,
1767        namespace: Option<String>,
1768        keypair: Keypair,
1769        _silent_intents: Arc<HashSet<String>>,
1770    ) -> Result<Self, CommsRuntimeError> {
1771        let public_key = keypair.public_key();
1772        // Single source of truth — same Arc shared by Router, classification, and callers.
1773        let trusted_peers = Arc::new(parking_lot::RwLock::new(TrustStore::new()));
1774
1775        let peer_comms_handle = Arc::new(parking_lot::RwLock::new(None));
1776        let require_peer_comms_machine_authority = Arc::new(AtomicBool::new(false));
1777        let classification_context = Arc::new(crate::classify::IngressClassificationContext {
1778            require_peer_auth: true,
1779            trusted_peers: trusted_peers.clone(),
1780            peer_comms_handle: peer_comms_handle.clone(),
1781            inproc_namespace: namespace.clone(),
1782        });
1783        let (inbox, inbox_sender) = crate::Inbox::new_classified(classification_context);
1784        let inbox_notify = inbox.notify();
1785        let actionable_notify = inbox.classified_actionable_notify();
1786        let comms_config = crate::CommsConfig::default();
1787        #[cfg(not(target_arch = "wasm32"))]
1788        let config = ResolvedCommsConfig {
1789            enabled: true,
1790            name: name.to_string(),
1791            inproc_namespace: namespace.clone(),
1792            identity_dir: std::path::PathBuf::new(),
1793            trusted_peers_path: std::path::PathBuf::new(),
1794            listen_uds: None,
1795            listen_tcp: None,
1796            advertise_address: None,
1797            event_listen_tcp: None,
1798            #[cfg(unix)]
1799            event_listen_uds: None,
1800            comms_config: comms_config.clone(),
1801            auth: meerkat_core::CommsAuthMode::Open,
1802            require_peer_auth: true,
1803            allow_external_unauthenticated: false,
1804            pairing_password: None,
1805        };
1806        let router = Router::with_shared_peers(
1807            keypair.clone(),
1808            trusted_peers.clone(),
1809            comms_config,
1810            inbox_sender.clone(),
1811            true,
1812        )
1813        .with_inproc_namespace(namespace.clone());
1814        let runtime = Self {
1815            public_key,
1816            peer_id: public_key.to_peer_id(),
1817            router: Arc::new(router),
1818            trusted_peers,
1819            inbox: Arc::new(AsyncMutex::new(inbox)),
1820            inbox_notify,
1821            #[cfg(not(target_arch = "wasm32"))]
1822            config,
1823            #[cfg(target_arch = "wasm32")]
1824            name: name.to_string(),
1825            #[cfg(target_arch = "wasm32")]
1826            inproc_namespace: namespace.clone(),
1827            #[cfg(not(target_arch = "wasm32"))]
1828            listener_handles: Mutex::new(Vec::new()),
1829            #[cfg(not(target_arch = "wasm32"))]
1830            listeners_started: false,
1831            #[cfg(not(target_arch = "wasm32"))]
1832            _session_identity_claim: None,
1833            bridge_bootstrap_token: Self::derive_bridge_bootstrap_token(&keypair),
1834            keypair: Arc::new(keypair),
1835            require_peer_auth: true,
1836            subscriber_registry: crate::event_injector::new_subscriber_registry(),
1837            interaction_stream_registry: Arc::new(Mutex::new(HashMap::new())),
1838            peer_comms_handle,
1839            meerkat_machine_trust_owner: parking_lot::RwLock::new(None),
1840            mob_machine_trust_owner: parking_lot::RwLock::new(None),
1841            require_peer_comms_machine_authority,
1842            actionable_notify,
1843            blob_store: None,
1844            peer_interaction_handle: parking_lot::RwLock::new(None),
1845            interaction_stream_handle: parking_lot::RwLock::new(None),
1846        };
1847        let namespace_ref = namespace.as_deref().unwrap_or("");
1848        let outcome = InprocRegistry::global().register_with_meta_in_namespace(
1849            namespace_ref,
1850            name,
1851            runtime.public_key,
1852            inbox_sender,
1853            crate::PeerMeta::default(),
1854        );
1855        observe_inproc_registration(namespace_ref, name, outcome)?;
1856        Ok(runtime)
1857    }
1858
1859    #[cfg(not(target_arch = "wasm32"))]
1860    pub fn set_listen_tcp_for_unstarted_runtime(
1861        &mut self,
1862        listen_tcp: std::net::SocketAddr,
1863    ) -> Result<(), CommsRuntimeError> {
1864        if self.listeners_started {
1865            return Err(CommsRuntimeError::AlreadyStarted);
1866        }
1867        self.config.listen_tcp = Some(listen_tcp);
1868        Ok(())
1869    }
1870
1871    #[cfg(not(target_arch = "wasm32"))]
1872    pub fn set_advertise_address_for_unstarted_runtime(
1873        &mut self,
1874        advertise_address: String,
1875    ) -> Result<(), CommsRuntimeError> {
1876        if self.listeners_started {
1877            return Err(CommsRuntimeError::AlreadyStarted);
1878        }
1879        self.config.advertise_address = Some(advertise_address);
1880        Ok(())
1881    }
1882
1883    #[cfg(not(target_arch = "wasm32"))]
1884    pub async fn inproc_only_session_scoped_with_silent_intents(
1885        name: &str,
1886        namespace: Option<String>,
1887        identity_root: std::path::PathBuf,
1888        session_id: &meerkat_core::SessionId,
1889        _silent_intents: Arc<HashSet<String>>,
1890        session_claim_handle: Arc<dyn SessionClaimHandle>,
1891    ) -> Result<Self, CommsRuntimeError> {
1892        let claim = session_claim_handle
1893            .try_acquire(session_id)
1894            .map_err(CommsRuntimeError::from)?;
1895        let identity_dir = identity_root.join(session_id.to_string());
1896        let keypair = Keypair::load_or_generate(&identity_dir)
1897            .await
1898            .map_err(|e| CommsRuntimeError::IdentityError(e.to_string()))?;
1899        let public_key = keypair.public_key();
1900        let trusted_peers = Arc::new(parking_lot::RwLock::new(TrustStore::new()));
1901
1902        let peer_comms_handle = Arc::new(parking_lot::RwLock::new(None));
1903        let require_peer_comms_machine_authority = Arc::new(AtomicBool::new(true));
1904        let classification_context = Arc::new(crate::classify::IngressClassificationContext {
1905            require_peer_auth: true,
1906            trusted_peers: trusted_peers.clone(),
1907            peer_comms_handle: peer_comms_handle.clone(),
1908            inproc_namespace: namespace.clone(),
1909        });
1910        let (inbox, inbox_sender) = crate::Inbox::new_classified(classification_context);
1911        let inbox_notify = inbox.notify();
1912        let actionable_notify = inbox.classified_actionable_notify();
1913        let comms_config = crate::CommsConfig::default();
1914        let config = ResolvedCommsConfig {
1915            enabled: true,
1916            name: name.to_string(),
1917            inproc_namespace: namespace.clone(),
1918            identity_dir: identity_dir.clone(),
1919            trusted_peers_path: identity_dir.join("trusted_peers.json"),
1920            listen_uds: None,
1921            listen_tcp: None,
1922            advertise_address: None,
1923            event_listen_tcp: None,
1924            #[cfg(unix)]
1925            event_listen_uds: None,
1926            comms_config: comms_config.clone(),
1927            auth: meerkat_core::CommsAuthMode::Open,
1928            require_peer_auth: true,
1929            allow_external_unauthenticated: false,
1930            pairing_password: None,
1931        };
1932        let router = Router::with_shared_peers(
1933            keypair.clone(),
1934            trusted_peers.clone(),
1935            comms_config,
1936            inbox_sender.clone(),
1937            true,
1938        )
1939        .with_inproc_namespace(namespace.clone());
1940        let runtime = Self {
1941            public_key,
1942            peer_id: public_key.to_peer_id(),
1943            router: Arc::new(router),
1944            trusted_peers,
1945            inbox: Arc::new(AsyncMutex::new(inbox)),
1946            inbox_notify,
1947            config,
1948            listener_handles: Mutex::new(Vec::new()),
1949            listeners_started: false,
1950            _session_identity_claim: Some(claim),
1951            bridge_bootstrap_token: Self::derive_bridge_bootstrap_token(&keypair),
1952            keypair: Arc::new(keypair),
1953            require_peer_auth: true,
1954            subscriber_registry: crate::event_injector::new_subscriber_registry(),
1955            interaction_stream_registry: Arc::new(Mutex::new(HashMap::new())),
1956            peer_comms_handle,
1957            meerkat_machine_trust_owner: parking_lot::RwLock::new(None),
1958            mob_machine_trust_owner: parking_lot::RwLock::new(None),
1959            require_peer_comms_machine_authority,
1960            actionable_notify,
1961            blob_store: None,
1962            peer_interaction_handle: parking_lot::RwLock::new(None),
1963            interaction_stream_handle: parking_lot::RwLock::new(None),
1964        };
1965        let namespace_ref = namespace.as_deref().unwrap_or("");
1966        let outcome = InprocRegistry::global().register_with_meta_in_namespace(
1967            namespace_ref,
1968            name,
1969            runtime.public_key,
1970            inbox_sender,
1971            crate::PeerMeta::default(),
1972        );
1973        observe_inproc_registration(namespace_ref, name, outcome)?;
1974        Ok(runtime)
1975    }
1976
1977    pub fn participant_name(&self) -> &str {
1978        #[cfg(not(target_arch = "wasm32"))]
1979        {
1980            &self.config.name
1981        }
1982        #[cfg(target_arch = "wasm32")]
1983        {
1984            &self.name
1985        }
1986    }
1987
1988    /// Set the blob store used to resolve blob-backed image blocks before
1989    /// transport send. Comms stays byte-oriented; refs never cross the peer
1990    /// boundary.
1991    pub fn set_blob_store(&mut self, blob_store: Arc<dyn BlobStore>) {
1992        self.blob_store = Some(blob_store);
1993    }
1994
1995    #[cfg(test)]
1996    fn install_peer_comms_handle(&self, handle: Arc<dyn meerkat_core::handles::PeerCommsHandle>) {
1997        *self.meerkat_machine_trust_owner.write() = None;
1998        *self.peer_comms_handle.write() = Some(handle);
1999    }
2000
2001    fn validate_generated_trust_authority_owner(
2002        &self,
2003        authority: &meerkat_core::comms::CommsTrustMutationAuthority,
2004    ) -> Result<(), SendError> {
2005        let expected_meerkat = self.meerkat_machine_trust_owner.read();
2006        let expected_mob = self.mob_machine_trust_owner.read();
2007        authority
2008            .validate_target_source_owner_token(expected_meerkat.as_ref(), expected_mob.as_ref())
2009            .map_err(SendError::Validation)
2010    }
2011
2012    /// Mark this runtime as requiring peer-comms machine authority.
2013    /// Missing `PeerCommsHandle` already fails closed for classified ingress;
2014    /// this flag remains as an observable session-mode marker for callers.
2015    pub fn require_peer_comms_machine_authority(&self) {
2016        self.require_peer_comms_machine_authority
2017            .store(true, Ordering::SeqCst);
2018    }
2019
2020    pub fn peer_comms_machine_authority_required(&self) -> bool {
2021        self.require_peer_comms_machine_authority
2022            .load(Ordering::SeqCst)
2023    }
2024
2025    pub fn peer_comms_handle(&self) -> Option<Arc<dyn meerkat_core::handles::PeerCommsHandle>> {
2026        self.peer_comms_handle.read().clone()
2027    }
2028
2029    /// Install both machine handles required for semantic peer request/response.
2030    ///
2031    /// This is the typed boundary between transport-only comms and semantic
2032    /// request/response. Without this authority, `PeerRequest` and
2033    /// `PeerResponse` sends fail closed before transport send or receipt
2034    /// emission.
2035    pub fn install_peer_request_response_authority(
2036        self: &Arc<Self>,
2037        authority: PeerRequestResponseAuthority,
2038    ) {
2039        self.install_peer_interaction_handle(authority.peer_interaction);
2040        self.install_interaction_stream_handle(authority.interaction_stream);
2041    }
2042
2043    /// Install the session's peer-interaction DSL handle (W1-A).
2044    ///
2045    /// Called by the surface after the session's `SessionRuntimeBindings`
2046    /// land. Two things happen here:
2047    ///
2048    /// 1. The runtime stores the handle so outbound `PeerRequest` sends
2049    ///    fire `PeerInteractionHandle::request_sent` (DSL authority).
2050    /// 2. The runtime registers *itself* as the handle's cleanup observer,
2051    ///    so every DSL `PeerInteractionCleanup` effect drops the matching
2052    ///    entries in `subscriber_registry` / `interaction_stream_registry`.
2053    ///    That closes the "terminal transition → effect → cleanup" causal
2054    ///    chain — the shell registries are a projection of DSL truth, not
2055    ///    shadow state updated by lexical adjacency.
2056    pub fn install_peer_interaction_handle(
2057        self: &Arc<Self>,
2058        handle: Arc<dyn meerkat_core::handles::PeerInteractionHandle>,
2059    ) {
2060        let observer: Arc<dyn meerkat_core::handles::PeerInteractionCleanupObserver> =
2061            Arc::clone(self) as Arc<dyn meerkat_core::handles::PeerInteractionCleanupObserver>;
2062        handle.install_cleanup_observer(observer);
2063        *self.peer_interaction_handle.write() = Some(handle);
2064    }
2065
2066    /// Read-side accessor for the installed peer-interaction handle.
2067    pub fn peer_interaction_handle(
2068        &self,
2069    ) -> Option<Arc<dyn meerkat_core::handles::PeerInteractionHandle>> {
2070        self.peer_interaction_handle.read().clone()
2071    }
2072
2073    /// Install the session's interaction-stream lifecycle DSL handle (U6).
2074    ///
2075    /// Two things happen here:
2076    ///
2077    /// 1. The runtime stores the handle so machine-authoritative reservation,
2078    ///    attach, completion, expiry, and consumer-drop transitions flow
2079    ///    through the DSL.
2080    /// 2. The runtime registers itself as the handle's cleanup observer, so
2081    ///    every `InteractionStreamCleanup` effect drops the matching entry
2082    ///    in `interaction_stream_registry` (and the paired
2083    ///    `subscriber_registry` entry under the same lock).
2084    ///
2085    /// For machine-authoritative entries, the registry holds channels only and
2086    /// lifecycle truth (Reserved/Attached/Completed/Expired/ClosedEarly) lives
2087    /// in the DSL. Local input streams stay transport-only.
2088    pub fn install_interaction_stream_handle(
2089        self: &Arc<Self>,
2090        handle: Arc<dyn meerkat_core::handles::InteractionStreamHandle>,
2091    ) {
2092        let observer: Arc<dyn meerkat_core::handles::InteractionStreamCleanupObserver> =
2093            Arc::clone(self) as Arc<dyn meerkat_core::handles::InteractionStreamCleanupObserver>;
2094        handle.install_cleanup_observer(observer);
2095        *self.interaction_stream_handle.write() = Some(handle);
2096    }
2097
2098    /// Read-side accessor for the installed interaction-stream handle.
2099    pub fn interaction_stream_handle(
2100        &self,
2101    ) -> Option<Arc<dyn meerkat_core::handles::InteractionStreamHandle>> {
2102        self.interaction_stream_handle.read().clone()
2103    }
2104
2105    fn require_peer_interaction_authority(
2106        &self,
2107        command: &'static str,
2108    ) -> Result<Arc<dyn meerkat_core::handles::PeerInteractionHandle>, SendError> {
2109        self.peer_interaction_handle().ok_or_else(|| {
2110            SendError::Validation(format!(
2111                "{command} requires machine peer interaction authority; this CommsRuntime is transport-only for semantic peer request/response"
2112            ))
2113        })
2114    }
2115
2116    fn require_interaction_stream_authority(
2117        &self,
2118        command: &'static str,
2119    ) -> Result<Arc<dyn meerkat_core::handles::InteractionStreamHandle>, SendError> {
2120        self.interaction_stream_handle().ok_or_else(|| {
2121            SendError::Validation(format!(
2122                "{command} requires machine interaction stream authority; this CommsRuntime is transport-only for semantic peer request/response streams"
2123            ))
2124        })
2125    }
2126
2127    fn require_peer_request_response_authority(
2128        &self,
2129        command: &'static str,
2130    ) -> Result<PeerRequestResponseAuthorityHandles, SendError> {
2131        let peer_handle = self.require_peer_interaction_authority(command)?;
2132        let stream_handle = self.require_interaction_stream_authority(command)?;
2133        Ok((peer_handle, stream_handle))
2134    }
2135
2136    fn peer_request_response_sendable_kinds(&self) -> Vec<PeerSendability> {
2137        let mut kinds = vec![PeerSendability::PeerMessage];
2138        if self.peer_interaction_handle().is_some() && self.interaction_stream_handle().is_some() {
2139            kinds.push(PeerSendability::PeerRequest);
2140            kinds.push(PeerSendability::PeerResponse);
2141        }
2142        kinds
2143    }
2144
2145    async fn hydrate_message_kind_for_transport(
2146        &self,
2147        kind: crate::types::MessageKind,
2148    ) -> Result<crate::types::MessageKind, SendError> {
2149        match kind {
2150            crate::types::MessageKind::Message {
2151                body,
2152                blocks: Some(mut blocks),
2153                content_taint,
2154                handling_mode,
2155            } => {
2156                self.hydrate_blocks_for_transport(&mut blocks, "comms message")
2157                    .await?;
2158                Ok(crate::types::MessageKind::Message {
2159                    body,
2160                    blocks: Some(blocks),
2161                    content_taint,
2162                    handling_mode,
2163                })
2164            }
2165            crate::types::MessageKind::Request {
2166                intent,
2167                params,
2168                blocks: Some(mut blocks),
2169                content_taint,
2170                handling_mode,
2171            } => {
2172                self.hydrate_blocks_for_transport(&mut blocks, "comms request")
2173                    .await?;
2174                Ok(crate::types::MessageKind::Request {
2175                    intent,
2176                    params,
2177                    blocks: Some(blocks),
2178                    content_taint,
2179                    handling_mode,
2180                })
2181            }
2182            crate::types::MessageKind::Response {
2183                in_reply_to,
2184                status,
2185                result,
2186                blocks: Some(mut blocks),
2187                content_taint,
2188                handling_mode,
2189            } => {
2190                self.hydrate_blocks_for_transport(&mut blocks, "comms response")
2191                    .await?;
2192                Ok(crate::types::MessageKind::Response {
2193                    in_reply_to,
2194                    status,
2195                    result,
2196                    blocks: Some(blocks),
2197                    content_taint,
2198                    handling_mode,
2199                })
2200            }
2201            other => Ok(other),
2202        }
2203    }
2204
2205    async fn hydrate_blocks_for_transport(
2206        &self,
2207        blocks: &mut [meerkat_core::types::ContentBlock],
2208        label: &str,
2209    ) -> Result<(), SendError> {
2210        if !blocks.iter().any(|block| {
2211            matches!(
2212                block,
2213                meerkat_core::types::ContentBlock::Image {
2214                    data: meerkat_core::types::ImageData::Blob { .. },
2215                    ..
2216                }
2217            )
2218        }) {
2219            return Ok(());
2220        }
2221        let blob_store = self.blob_store.as_ref().ok_or_else(|| {
2222            SendError::Internal(format!("blob-backed {label} requires blob store"))
2223        })?;
2224        hydrate_content_blocks(blob_store.as_ref(), blocks, MissingBlobBehavior::Error)
2225            .await
2226            .map_err(|err| SendError::Internal(err.to_string()))
2227    }
2228
2229    pub fn inproc_namespace(&self) -> Option<&str> {
2230        #[cfg(not(target_arch = "wasm32"))]
2231        {
2232            self.config.inproc_namespace.as_deref()
2233        }
2234        #[cfg(target_arch = "wasm32")]
2235        {
2236            self.inproc_namespace.as_deref()
2237        }
2238    }
2239
2240    #[cfg(not(target_arch = "wasm32"))]
2241    pub fn advertised_address(&self) -> String {
2242        if let Some(ref advertised) = self.config.advertise_address {
2243            return advertised.clone();
2244        }
2245        if let Some(ref uds) = self.config.listen_uds {
2246            return format!("uds://{}", uds.display());
2247        }
2248        if let Some(ref tcp) = self.config.listen_tcp {
2249            return format!("tcp://{tcp}");
2250        }
2251        format!("inproc://{}", self.config.name)
2252    }
2253
2254    #[cfg(not(target_arch = "wasm32"))]
2255    pub async fn start_listeners(&mut self) -> Result<(), CommsRuntimeError> {
2256        if self.listeners_started {
2257            return Err(CommsRuntimeError::AlreadyStarted);
2258        }
2259
2260        let inbox_sender = self.router.inbox_sender().clone();
2261        let router = self.router.clone();
2262        let max_line_length = self.config.comms_config.max_message_bytes as usize;
2263
2264        // === Signed (Ed25519) listeners — ALWAYS run for agent-to-agent comms ===
2265        #[cfg(unix)]
2266        if let Some(ref path) = self.config.listen_uds {
2267            let handle = spawn_uds_listener(
2268                path,
2269                self.keypair.clone(),
2270                self.require_peer_auth,
2271                inbox_sender.clone(),
2272            )
2273            .await?;
2274            self.listener_handles.lock().push(handle);
2275        }
2276
2277        if let Some(ref addr) = self.config.listen_tcp {
2278            let handle = spawn_tcp_listener(TcpListenerConfig {
2279                addr: addr.to_string(),
2280                keypair: self.keypair.clone(),
2281                router,
2282                trusted: self.trusted_peers.clone(),
2283                require_peer_auth: self.require_peer_auth,
2284                inbox_sender: inbox_sender.clone(),
2285                pairing_password: self.config.pairing_password.clone(),
2286                advertise_address: self.config.advertise_address.clone(),
2287                trusted_peers_path: self.config.trusted_peers_path.clone(),
2288                participant_name: self.config.name.clone(),
2289                bridge_bootstrap_token: self.bridge_bootstrap_token.clone(),
2290            })
2291            .await?;
2292            if let Some(local_addr) = handle.local_addr {
2293                self.config.listen_tcp = Some(local_addr);
2294            }
2295            self.listener_handles.lock().push(handle);
2296        }
2297
2298        // === Plain event listeners — run ADDITIONALLY when auth=Open ===
2299        if self.config.auth == meerkat_core::CommsAuthMode::Open {
2300            // Enforce loopback-only on plain TCP listener unless explicitly overridden
2301            if let Some(addr) = &self.config.event_listen_tcp
2302                && !addr.ip().is_loopback()
2303                && !self.config.allow_external_unauthenticated
2304            {
2305                return Err(CommsRuntimeError::UnsafeBinding(
2306                    "Plain event listener on non-loopback address is a prompt injection \
2307                     vector; set allow_external_unauthenticated=true to override"
2308                        .to_string(),
2309                ));
2310            }
2311
2312            if let Some(ref addr) = self.config.event_listen_tcp {
2313                let handle = spawn_plain_tcp_listener(
2314                    &addr.to_string(),
2315                    inbox_sender.clone(),
2316                    max_line_length,
2317                )
2318                .await?;
2319                self.listener_handles.lock().push(handle);
2320            }
2321
2322            #[cfg(unix)]
2323            if let Some(ref path) = self.config.event_listen_uds {
2324                let handle =
2325                    spawn_plain_uds_listener(path, inbox_sender.clone(), max_line_length).await?;
2326                self.listener_handles.lock().push(handle);
2327            }
2328        }
2329
2330        self.listeners_started = true;
2331        Ok(())
2332    }
2333
2334    pub fn public_key(&self) -> PubKey {
2335        self.public_key
2336    }
2337
2338    pub fn bridge_bootstrap_token(&self) -> &str {
2339        &self.bridge_bootstrap_token
2340    }
2341
2342    pub fn router(&self) -> &Router {
2343        &self.router
2344    }
2345
2346    /// Install (or clear) the host-owned outbound content-taint declaration.
2347    ///
2348    /// Host-set config, not machine state: the declaration is stamped onto
2349    /// every content-bearing envelope at the router's single outbound choke
2350    /// point unless a per-send [`meerkat_core::comms::SendTaintOverride`]
2351    /// says otherwise. `None` clears the declaration (subsequent sends carry
2352    /// no taint field).
2353    pub fn set_outbound_content_taint(
2354        &self,
2355        taint: Option<meerkat_core::comms::SenderContentTaint>,
2356    ) {
2357        self.router.set_outbound_content_taint(taint);
2358    }
2359
2360    /// The currently-installed host-owned outbound content-taint declaration.
2361    pub fn outbound_content_taint(&self) -> Option<meerkat_core::comms::SenderContentTaint> {
2362        self.router.outbound_content_taint()
2363    }
2364
2365    fn trusted_peer_descriptors(&self, include_private: bool) -> Vec<TrustedPeerDescriptor> {
2366        self.trusted_peer_descriptors_filtered(include_private, None)
2367    }
2368
2369    fn trusted_peer_descriptors_for_source(
2370        &self,
2371        include_private: bool,
2372        source_kind: GeneratedCommsTrustAuthoritySourceKind,
2373    ) -> Vec<TrustedPeerDescriptor> {
2374        self.trusted_peer_descriptors_filtered(include_private, Some(source_kind))
2375    }
2376
2377    fn trusted_peer_descriptors_filtered(
2378        &self,
2379        include_private: bool,
2380        source_kind: Option<GeneratedCommsTrustAuthoritySourceKind>,
2381    ) -> Vec<TrustedPeerDescriptor> {
2382        let peer_rows: Vec<(meerkat_core::comms::PeerId, TrustEntry)> =
2383            if let Some(source_kind) = source_kind {
2384                self.router.trusted_peers_for_source(source_kind)
2385            } else {
2386                self.trusted_peers
2387                    .read()
2388                    .entries()
2389                    .map(|entry| (entry.peer_id, entry.clone()))
2390                    .collect()
2391            };
2392        // Trust entries are typed, validated material: zero pubkeys, invalid
2393        // names, and invalid addresses are structurally impossible here.
2394        let mut trusted_peers: Vec<TrustedPeerDescriptor> = peer_rows
2395            .into_iter()
2396            .filter_map(|(peer_id, entry)| {
2397                if !include_private && self.router.is_private_peer_id(&peer_id) {
2398                    return None;
2399                }
2400                Some(TrustedPeerDescriptor {
2401                    peer_id,
2402                    name: entry.name,
2403                    address: entry.address,
2404                    pubkey: *entry.pubkey.as_bytes(),
2405                })
2406            })
2407            .collect();
2408        trusted_peers.sort_by(|left, right| {
2409            left.name
2410                .as_str()
2411                .cmp(right.name.as_str())
2412                .then_with(|| left.peer_id.cmp(&right.peer_id))
2413                .then_with(|| left.address.to_string().cmp(&right.address.to_string()))
2414        });
2415        trusted_peers
2416    }
2417
2418    async fn apply_trusted_peer_descriptor(
2419        &self,
2420        descriptor: TrustedPeerDescriptor,
2421        source_kind: GeneratedCommsTrustAuthoritySourceKind,
2422    ) -> Result<bool, SendError> {
2423        let entry = descriptor_to_trust_entry(descriptor)?;
2424        let created = self
2425            .router
2426            .add_trusted_peer_for_source(entry, source_kind, false)
2427            .map_err(|err| SendError::Validation(err.to_string()))?;
2428        Ok(created)
2429    }
2430
2431    async fn apply_trusted_peer_removal(
2432        &self,
2433        peer_id: &str,
2434        source_kind: GeneratedCommsTrustAuthoritySourceKind,
2435    ) -> Result<bool, SendError> {
2436        let peer_id = meerkat_core::comms::PeerId::parse(peer_id)
2437            .map_err(|err| SendError::Validation(err.to_string()))?;
2438        if self.router.is_private_peer_id(&peer_id)
2439            && !self.router.has_trust_source(&peer_id, source_kind)
2440        {
2441            return Err(SendError::Validation(format!(
2442                "public trust authority cannot remove private trusted peer {peer_id}"
2443            )));
2444        }
2445        Ok(self
2446            .router
2447            .remove_trusted_peer_for_source(&peer_id, source_kind))
2448    }
2449
2450    async fn apply_private_trusted_peer_descriptor(
2451        &self,
2452        descriptor: TrustedPeerDescriptor,
2453        source_kind: GeneratedCommsTrustAuthoritySourceKind,
2454    ) -> Result<bool, SendError> {
2455        let entry = descriptor_to_trust_entry(descriptor)?;
2456        let created = self
2457            .router
2458            .add_trusted_peer_for_source(entry, source_kind, true)
2459            .map_err(|err| SendError::Validation(err.to_string()))?;
2460        Ok(created)
2461    }
2462
2463    async fn apply_private_trusted_peer_removal(
2464        &self,
2465        peer_id: &str,
2466        source_kind: GeneratedCommsTrustAuthoritySourceKind,
2467    ) -> Result<bool, SendError> {
2468        let peer_id = meerkat_core::comms::PeerId::parse(peer_id)
2469            .map_err(|err| SendError::Validation(err.to_string()))?;
2470        Ok(self
2471            .router
2472            .remove_trusted_peer_for_source(&peer_id, source_kind))
2473    }
2474
2475    /// Update the inproc registry entry with friendly metadata.
2476    ///
2477    /// Call this after construction to advertise [`PeerMeta`] to other
2478    /// in-process agents via [`InprocRegistry::peers()`].
2479    ///
2480    /// Internally re-registers with [`InprocRegistry::register_with_meta`],
2481    /// which replaces the full entry (including inbox sender). This is safe
2482    /// because the sender is cloned from the same router and pending
2483    /// deliveries are unaffected.
2484    pub fn set_peer_meta(&self, meta: crate::PeerMeta) {
2485        let namespace = self.inproc_namespace().unwrap_or("");
2486        let name = self.participant_name();
2487        let outcome = InprocRegistry::global().register_with_meta_in_namespace(
2488            namespace,
2489            name,
2490            self.public_key,
2491            self.router.inbox_sender().clone(),
2492            meta,
2493        );
2494        // A metadata refresh re-registers the same name+pubkey and expects a
2495        // clean `Registered`. Surface anything else explicitly rather than
2496        // assuming success — a rejection means the meta update never landed,
2497        // and a displacement means we clobbered an unexpected route.
2498        if outcome.is_rejected() {
2499            tracing::warn!(
2500                inproc_namespace = %namespace,
2501                peer_name = %name,
2502                ?outcome,
2503                "inproc metadata refresh rejected; peer meta was not updated"
2504            );
2505        } else if outcome.displaced_existing() {
2506            tracing::warn!(
2507                inproc_namespace = %namespace,
2508                peer_name = %name,
2509                ?outcome,
2510                "inproc metadata refresh displaced an unexpected route"
2511            );
2512        }
2513    }
2514
2515    /// Canonical peer resolver.
2516    ///
2517    /// Discovery and sendability are derived from trusted peers, with in-proc peers
2518    /// included when auth is disabled.
2519    async fn resolve_peer_directory(&self) -> Vec<PeerDirectoryEntry> {
2520        let resolved = self.resolved_peers_snapshot().await;
2521        let sendable_kinds = self.peer_request_response_sendable_kinds();
2522        let mut peers = Vec::new();
2523        for peer in resolved {
2524            peers.push(PeerDirectoryEntry {
2525                name: peer.name,
2526                peer_id: peer.peer_id,
2527                address: peer.address,
2528                source: peer.source,
2529                sendable_kinds: sendable_kinds.clone(),
2530                capabilities: PeerCapabilitySet::default(),
2531                meta: peer.meta,
2532            });
2533        }
2534        peers
2535    }
2536
2537    /// Resolve peer count using the same filtering/dedup rules as
2538    /// `resolve_peer_directory`, without materializing directory entries.
2539    async fn resolve_peer_count(&self) -> usize {
2540        self.resolved_peers_snapshot().await.len()
2541    }
2542
2543    async fn resolved_peers_snapshot(&self) -> Vec<ResolvedPeer> {
2544        let mut peers = Vec::new();
2545        self.for_each_resolved_peer(|peer| peers.push(peer)).await;
2546        peers
2547    }
2548
2549    async fn for_each_resolved_peer<F>(&self, mut on_peer: F) -> usize
2550    where
2551        F: FnMut(ResolvedPeer),
2552    {
2553        let participant_name = self.participant_name().to_string();
2554        let private_peer_ids = self.router.private_peer_ids();
2555        let mut emitted = 0usize;
2556
2557        {
2558            // Trust entries are typed, validated material keyed by canonical
2559            // PeerId: duplicate identities, zero pubkeys, and invalid names
2560            // or addresses are structurally impossible here.
2561            let trusted = self.trusted_peers.read();
2562            for entry in trusted.entries() {
2563                if entry.name.as_str() == participant_name || entry.pubkey == self.public_key {
2564                    continue;
2565                }
2566                if private_peer_ids.contains(&entry.peer_id) {
2567                    continue;
2568                }
2569                on_peer(ResolvedPeer {
2570                    name: entry.name.clone(),
2571                    peer_id: entry.peer_id,
2572                    address: entry.address.clone(),
2573                    source: PeerDirectorySource::Trusted,
2574                    meta: entry.meta.clone(),
2575                });
2576                emitted += 1;
2577            }
2578        }
2579
2580        emitted
2581    }
2582
2583    /// Canonical send path: destination is a typed [`PeerRoute`] whose
2584    /// [`PeerId`] is already resolved at the surface boundary.
2585    async fn send_peer_command(
2586        &self,
2587        route: &PeerRoute,
2588        kind: crate::types::MessageKind,
2589        content_taint: Option<meerkat_core::comms::SendTaintOverride>,
2590    ) -> Result<crate::router::SendOutcome, SendError> {
2591        self.send_peer_command_with_id(route, Uuid::new_v4(), kind, content_taint)
2592            .await
2593    }
2594
2595    /// Send a peer command and return the typed delivery outcome.
2596    ///
2597    /// The returned [`crate::router::SendOutcome`] carries the verified ACK
2598    /// truth (`acked`) so public receipts reflect whether the peer actually
2599    /// acknowledged the envelope rather than hardcoding `false`.
2600    ///
2601    /// `content_taint` is the per-send tri-state taint override, resolved
2602    /// against the runtime-level declaration at the router's single outbound
2603    /// choke point ([`crate::router::Router::send_with_id`]).
2604    async fn send_peer_command_with_id(
2605        &self,
2606        route: &PeerRoute,
2607        envelope_id: Uuid,
2608        kind: crate::types::MessageKind,
2609        content_taint: Option<meerkat_core::comms::SendTaintOverride>,
2610    ) -> Result<crate::router::SendOutcome, SendError> {
2611        let dest_peer_id = route.peer_id;
2612        let kind = self.hydrate_message_kind_for_transport(kind).await?;
2613        let result = self
2614            .router
2615            .send_with_id(dest_peer_id, envelope_id, kind, content_taint)
2616            .await;
2617        match result {
2618            Ok(outcome) => Ok(outcome),
2619            Err(crate::router::SendError::PeerNotFound(peer_id)) => {
2620                Err(SendError::PeerNotFound(peer_id.to_string()))
2621            }
2622            Err(crate::router::SendError::PeerOffline) => Err(SendError::PeerOffline),
2623            Err(crate::router::SendError::AdmissionDropped { reason }) => {
2624                // Transport worked; the receiver's ingress admission policy
2625                // rejected us. Surface the typed reason all the way to the
2626                // public API error payload so clients see e.g.
2627                // `untrusted_sender` rather than the old `PeerOffline` lie.
2628                Err(SendError::AdmissionDropped {
2629                    reason: reason.into(),
2630                })
2631            }
2632            Err(
2633                error @ (crate::router::SendError::Transport(_) | crate::router::SendError::Io(_)),
2634            ) => Err(SendError::Transport(error.to_string())),
2635        }
2636    }
2637
2638    /// Drop the registry entries for a correlation id unconditionally.
2639    ///
2640    /// Invoked from the `PeerInteractionCleanup` DSL effect observer
2641    /// (W1-A): the DSL has already decided the peer-interaction lifecycle
2642    /// is terminal, so the shell-side projection unconditionally matches.
2643    /// Callers that want the stream-lifecycle CAS (Reserved/Attached gate)
2644    /// should use [`mark_interaction_complete`].
2645    fn drop_peer_interaction_projection(&self, interaction_id: Uuid) {
2646        let removed_sender = self.subscriber_registry.lock().remove(&interaction_id);
2647        let removed_stream = self
2648            .interaction_stream_registry
2649            .lock()
2650            .remove(&interaction_id);
2651        if removed_sender.is_some() || removed_stream.is_some() {
2652            tracing::debug!(
2653                interaction_id = %interaction_id,
2654                "peer interaction projection dropped via DSL cleanup effect"
2655            );
2656        }
2657    }
2658
2659    /// Mark an interaction stream as completed (terminal event received).
2660    ///
2661    /// Machine-authoritative streams complete through the DSL `is_attached`
2662    /// guard: if the consumer already closed the stream, the transition is
2663    /// rejected and this call is a no-op. Local transport-only streams have no
2664    /// semantic lifecycle twin, so completion drops their channel projection.
2665    pub fn mark_interaction_complete(&self, interaction_id: Uuid) {
2666        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
2667        let lifecycle_authority = self
2668            .interaction_stream_registry
2669            .lock()
2670            .get(&interaction_id)
2671            .map(|entry| entry.lifecycle_authority);
2672        match lifecycle_authority {
2673            Some(InteractionStreamLifecycleAuthority::Machine) => {
2674                if let Some(handle) = self.interaction_stream_handle()
2675                    && let Err(err) = handle.completed(corr_id)
2676                {
2677                    tracing::trace!(
2678                        interaction_id = %interaction_id,
2679                        error = %err,
2680                        "InteractionStreamHandle::completed rejected (likely closed-early won race)"
2681                    );
2682                }
2683            }
2684            Some(InteractionStreamLifecycleAuthority::LocalTransportOnly) | None => {
2685                // Transport-only local stream: drop the channel and
2686                // subscriber entries directly. Without a DSL twin there's no
2687                // CAS, so double-completion is a benign no-op against the maps.
2688                self.subscriber_registry.lock().remove(&interaction_id);
2689                self.interaction_stream_registry
2690                    .lock()
2691                    .remove(&interaction_id);
2692            }
2693        }
2694    }
2695
2696    /// Reap expired reservations that were never attached within the TTL.
2697    ///
2698    /// TTL is shell-owned mechanics (`created_at` lives with the channel
2699    /// projection, not the DSL). Machine-authoritative reservations fire
2700    /// `InteractionStreamHandle::expired`; local transport-only reservations
2701    /// remove the unclaimed channel projection directly. If the reservation
2702    /// also corresponds to a live outbound peer-request DSL entry, fires
2703    /// `PeerRequestTimedOut` so the peer-correlation lifecycle matches reality.
2704    pub fn reap_expired_reservations(&self) {
2705        let candidates: Vec<(Uuid, InteractionStreamLifecycleAuthority)> = {
2706            let registry = self.interaction_stream_registry.lock();
2707            registry
2708                .iter()
2709                .filter_map(|(id, entry)| {
2710                    (entry.created_at.elapsed() > RESERVATION_TTL)
2711                        .then_some((*id, entry.lifecycle_authority))
2712                })
2713                .collect()
2714        };
2715        if candidates.is_empty() {
2716            return;
2717        }
2718        let stream_handle = self.interaction_stream_handle();
2719        let peer_handle = self.peer_interaction_handle();
2720        for (id, lifecycle_authority) in candidates {
2721            let corr_id = meerkat_core::PeerCorrelationId::from_uuid(id);
2722            // Stream lifecycle: only `Reserved` reservations expire. The
2723            // DSL guard rejects `Attached` (the stream is live and will
2724            // complete or close-early on its own).
2725            match lifecycle_authority {
2726                InteractionStreamLifecycleAuthority::Machine => {
2727                    let Some(handle) = stream_handle.as_ref() else {
2728                        continue;
2729                    };
2730                    if handle.state(corr_id) == Some(meerkat_core::InteractionStreamState::Reserved)
2731                    {
2732                        tracing::debug!(interaction_id = %id, "reservation expired (TTL)");
2733                        let _ = handle.expired(corr_id);
2734                    }
2735                }
2736                InteractionStreamLifecycleAuthority::LocalTransportOnly => {
2737                    // Transport-only local stream: drop the entry if the
2738                    // receiver is still present (consumer never attached).
2739                    let mut registry = self.interaction_stream_registry.lock();
2740                    if let Some(entry) = registry.get(&id)
2741                        && entry.receiver.is_some()
2742                    {
2743                        tracing::debug!(interaction_id = %id, "reservation expired (TTL)");
2744                        registry.remove(&id);
2745                    }
2746                }
2747            }
2748            // Mirror into the peer-interaction lifecycle when the
2749            // correlation id names a live outbound entry.
2750            if let Some(handle) = peer_handle.as_ref()
2751                && handle.outbound_state(corr_id).is_some()
2752            {
2753                let _ = handle.request_timed_out(corr_id);
2754            }
2755        }
2756    }
2757
2758    pub fn router_arc(&self) -> Arc<Router> {
2759        self.router.clone()
2760    }
2761    pub fn trusted_peers_shared(&self) -> TrustedPeersView {
2762        TrustedPeersView::new(self.trusted_peers.clone())
2763    }
2764    pub fn inbox_notify(&self) -> Arc<tokio::sync::Notify> {
2765        self.inbox_notify.clone()
2766    }
2767
2768    /// Extract the material needed for comms tool composition.
2769    ///
2770    /// The facade uses this to compose comms tools without reaching into
2771    /// router/trust/key internals directly.
2772    pub fn tool_material(self: &Arc<Self>) -> CommsToolMaterial {
2773        CommsToolMaterial {
2774            router: self.router_arc(),
2775            trusted_peers: self.trusted_peers_shared(),
2776            self_pubkey: self.public_key,
2777            runtime: Arc::clone(self) as Arc<dyn meerkat_core::agent::CommsRuntime>,
2778        }
2779    }
2780
2781    /// Get an event injector for this runtime's inbox.
2782    pub fn event_injector(&self) -> Arc<dyn meerkat_core::EventInjector> {
2783        Arc::new(self.event_injector_concrete())
2784    }
2785
2786    /// Get the concrete event injector for this runtime's inbox.
2787    ///
2788    /// Returns the in-crate [`crate::CommsEventInjector`] so callers can reach
2789    /// inherent methods (e.g. `inject_with_interaction_id`) that are not part of
2790    /// the cross-crate `EventInjector` trait surface.
2791    fn event_injector_concrete(&self) -> crate::CommsEventInjector {
2792        crate::CommsEventInjector::new(
2793            self.router.inbox_sender().clone(),
2794            self.subscriber_registry.clone(),
2795        )
2796    }
2797
2798    #[doc(hidden)]
2799    pub fn interaction_event_injector(
2800        &self,
2801    ) -> Arc<dyn meerkat_core::event_injector::SubscribableInjector> {
2802        Arc::new(crate::CommsEventInjector::new(
2803            self.router.inbox_sender().clone(),
2804            self.subscriber_registry.clone(),
2805        ))
2806    }
2807
2808    fn register_interaction_stream(
2809        &self,
2810        interaction_id: Uuid,
2811        body: String,
2812        blocks: Option<Vec<meerkat_core::types::ContentBlock>>,
2813        source: meerkat_core::InputSource,
2814        handling_mode: meerkat_core::types::HandlingMode,
2815    ) -> Result<(), SendError> {
2816        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
2817        self.reserve_interaction_stream_channels(
2818            corr_id,
2819            interaction_id,
2820            InteractionStreamReservationAuthority::LocalTransportOnly,
2821        )?;
2822
2823        if let crate::inbox::AdmissionOutcome::Dropped { reason } = self
2824            .router
2825            .inbox_sender()
2826            .send_classified(crate::types::InboxItem::PlainEvent {
2827                body,
2828                source: PlainEventSource::from(source),
2829                handling_mode,
2830                interaction_id: Some(interaction_id),
2831                blocks,
2832                render_metadata: None,
2833            })
2834        {
2835            self.expire_interaction_stream_on_send_failure(corr_id, interaction_id);
2836            return Err(match reason {
2837                crate::inbox::DropReason::InboxFull => {
2838                    SendError::Validation("input queue full".into())
2839                }
2840                crate::inbox::DropReason::SessionClosed => SendError::InputClosed,
2841                crate::inbox::DropReason::UntrustedSender
2842                | crate::inbox::DropReason::ClassificationRejected => {
2843                    SendError::Validation(format!("input rejected at ingress: {reason:?}"))
2844                }
2845            });
2846        }
2847
2848        Ok(())
2849    }
2850
2851    /// Reserve channel slots for an interaction stream.
2852    ///
2853    /// Semantic peer request/response reservations pass `Machine` so the
2854    /// DSL records lifecycle truth before the shell projection is inserted.
2855    /// Local input streams pass `LocalTransportOnly`, making the direct
2856    /// channel cleanup boundary explicit instead of acting as a semantic
2857    /// lifecycle owner.
2858    fn reserve_interaction_stream_channels(
2859        &self,
2860        corr_id: meerkat_core::PeerCorrelationId,
2861        interaction_id: Uuid,
2862        authority: InteractionStreamReservationAuthority,
2863    ) -> Result<(), SendError> {
2864        let lifecycle_authority = match &authority {
2865            InteractionStreamReservationAuthority::Machine(_) => {
2866                InteractionStreamLifecycleAuthority::Machine
2867            }
2868            InteractionStreamReservationAuthority::LocalTransportOnly => {
2869                InteractionStreamLifecycleAuthority::LocalTransportOnly
2870            }
2871        };
2872        match authority {
2873            InteractionStreamReservationAuthority::Machine(handle) => {
2874                if let Err(err) = handle.reserved(corr_id) {
2875                    return Err(SendError::Validation(format!(
2876                        "DSL rejected InteractionStreamReserved for corr_id {corr_id}: {err}"
2877                    )));
2878                }
2879            }
2880            InteractionStreamReservationAuthority::LocalTransportOnly => {}
2881        }
2882        let (sender, receiver) = mpsc::channel::<meerkat_core::AgentEvent>(4096);
2883        self.subscriber_registry
2884            .lock()
2885            .insert(interaction_id, sender.clone());
2886        self.interaction_stream_registry.lock().insert(
2887            interaction_id,
2888            StreamRegistryEntry::new(sender, receiver, lifecycle_authority),
2889        );
2890        Ok(())
2891    }
2892
2893    /// Tear down an interaction stream reservation when the inbound send /
2894    /// admission step fails.
2895    ///
2896    /// Machine-authoritative streams fire `InteractionStreamHandle::expired`
2897    /// so the DSL transitions terminal and the cleanup observer drops the
2898    /// channel/subscriber entries. Local transport-only streams remove the
2899    /// projection directly.
2900    fn expire_interaction_stream_on_send_failure(
2901        &self,
2902        corr_id: meerkat_core::PeerCorrelationId,
2903        interaction_id: Uuid,
2904    ) {
2905        let lifecycle_authority = self
2906            .interaction_stream_registry
2907            .lock()
2908            .get(&interaction_id)
2909            .map(|entry| entry.lifecycle_authority);
2910        match lifecycle_authority {
2911            Some(InteractionStreamLifecycleAuthority::Machine) => {
2912                if let Some(handle) = self.interaction_stream_handle() {
2913                    let _ = handle.expired(corr_id);
2914                }
2915            }
2916            Some(InteractionStreamLifecycleAuthority::LocalTransportOnly) | None => {
2917                self.interaction_stream_registry
2918                    .lock()
2919                    .remove(&interaction_id);
2920                self.subscriber_registry.lock().remove(&interaction_id);
2921            }
2922        }
2923    }
2924
2925    pub async fn drain_messages(&self) -> Vec<CommsMessage> {
2926        // Drain from the classified queue (sole consumer since 0.4.10).
2927        // Convert classified entries back to CommsMessage using the
2928        // ingress-stored metadata (from_peer, class) rather than
2929        // re-resolving against live trust state. This preserves the
2930        // ingress-snapshot guarantee: a message accepted at ingress
2931        // cannot disappear or change from_peer if the peer is
2932        // removed/renamed before drain.
2933        let mut inbox = self.inbox.lock().await;
2934        let entries = inbox.try_drain_classified();
2935        entries
2936            .into_iter()
2937            .filter_map(|entry| CommsMessage::from_classified_entry(&entry))
2938            .collect()
2939    }
2940
2941    pub async fn recv_message(&self) -> Option<CommsMessage> {
2942        loop {
2943            // Register the waiter BEFORE draining so a message that arrives
2944            // between the drain returning empty and the await cannot be lost.
2945            // inbox_notify uses notify_waiters() which only wakes already-
2946            // registered listeners, and a `Notified` future is not registered
2947            // until it is pinned and `enable()`d (or first polled) — so do that
2948            // here, before the drain, not at the trailing `.await`.
2949            let mut notified = std::pin::pin!(self.inbox_notify.notified());
2950            notified.as_mut().enable();
2951            {
2952                let mut inbox = self.inbox.lock().await;
2953                // Consume one entry at a time so back-to-back messages are
2954                // preserved. The previous implementation drained the entire
2955                // classified queue and discarded everything after the first
2956                // decodable message.
2957                while let Some(entry) = inbox.try_recv_one_classified() {
2958                    if let Some(msg) = CommsMessage::from_classified_entry(&entry) {
2959                        return Some(msg);
2960                    }
2961                }
2962            }
2963            notified.as_mut().await;
2964        }
2965    }
2966
2967    pub fn shutdown(&mut self) {
2968        #[cfg(not(target_arch = "wasm32"))]
2969        {
2970            for handle in self.listener_handles.lock().drain(..) {
2971                handle.abort();
2972            }
2973            self.listeners_started = false;
2974        }
2975    }
2976
2977    #[cfg(not(target_arch = "wasm32"))]
2978    pub fn abort_listeners_for_rebind(&self) {
2979        for handle in self.listener_handles.lock().drain(..) {
2980            handle.abort();
2981        }
2982    }
2983
2984    #[cfg(not(target_arch = "wasm32"))]
2985    pub async fn stop_listeners_for_rebind(&self) {
2986        let handles = self.listener_handles.lock().drain(..).collect::<Vec<_>>();
2987        for handle in handles {
2988            handle.abort_and_wait().await;
2989        }
2990    }
2991}
2992
2993impl meerkat_core::handles::PeerCommsInstallTarget for CommsRuntime {
2994    fn install_generated_peer_comms_handle(
2995        &self,
2996        install: meerkat_core::handles::GeneratedPeerCommsInstall,
2997    ) -> Result<(), String> {
2998        let target_peer_id = self.generated_peer_comms_target_endpoint()?.peer_id;
2999        if install.target_peer_id() != target_peer_id {
3000            return Err(format!(
3001                "generated peer-comms install targets peer_id {} but runtime peer_id is {}",
3002                install.target_peer_id(),
3003                target_peer_id
3004            ));
3005        }
3006        let owner_token = install.owner_token();
3007        let mut expected = self.meerkat_machine_trust_owner.write();
3008        if let Some(existing) = expected.as_ref()
3009            && !existing.same_owner(&owner_token)
3010        {
3011            return Err(
3012                "target runtime is already bound to a different generated MeerkatMachine trust owner"
3013                    .to_string(),
3014            );
3015        }
3016        *expected = Some(owner_token);
3017        *self.peer_comms_handle.write() = Some(Arc::clone(install.peer_comms_handle()));
3018        Ok(())
3019    }
3020}
3021
3022impl meerkat_core::handles::PeerInteractionCleanupObserver for CommsRuntime {
3023    fn on_peer_interaction_cleanup(&self, corr_id: meerkat_core::PeerCorrelationId) {
3024        // The DSL decided the peer-interaction lifecycle is terminal; the
3025        // shell registries are a projection — drop the entries keyed on
3026        // the same correlation id.
3027        self.drop_peer_interaction_projection(corr_id.as_uuid());
3028    }
3029}
3030
3031impl meerkat_core::handles::InteractionStreamCleanupObserver for CommsRuntime {
3032    fn on_interaction_stream_cleanup(&self, corr_id: meerkat_core::PeerCorrelationId) {
3033        // The DSL decided the stream lifecycle is terminal; drop both the
3034        // channel entry and any paired subscriber sender. Same projection
3035        // rule as `on_peer_interaction_cleanup` but driven by the
3036        // independent stream-lifecycle effect.
3037        let id = corr_id.as_uuid();
3038        self.subscriber_registry.lock().remove(&id);
3039        self.interaction_stream_registry.lock().remove(&id);
3040    }
3041}
3042
3043impl Drop for CommsRuntime {
3044    fn drop(&mut self) {
3045        self.shutdown();
3046        InprocRegistry::global()
3047            .unregister_in_namespace(self.inproc_namespace().unwrap_or(""), &self.public_key);
3048    }
3049}
3050
3051#[cfg(not(target_arch = "wasm32"))]
3052pub struct ListenerHandle {
3053    handle: JoinHandle<()>,
3054    local_addr: Option<std::net::SocketAddr>,
3055}
3056
3057#[cfg(not(target_arch = "wasm32"))]
3058impl ListenerHandle {
3059    pub fn abort(&self) {
3060        self.handle.abort();
3061    }
3062
3063    async fn abort_and_wait(self) {
3064        self.handle.abort();
3065        let _ = self.handle.await;
3066    }
3067}
3068
3069#[cfg(unix)]
3070async fn spawn_uds_listener(
3071    path: &Path,
3072    keypair: Arc<Keypair>,
3073    require_peer_auth: bool,
3074    inbox_sender: InboxSender,
3075) -> Result<ListenerHandle, std::io::Error> {
3076    use tokio::net::UnixListener;
3077    let path = path.to_path_buf();
3078    if let Err(err) = tokio::fs::remove_file(&path).await
3079        && err.kind() != std::io::ErrorKind::NotFound
3080    {
3081        return Err(err);
3082    }
3083    if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
3084        tokio::fs::create_dir_all(parent).await?;
3085    }
3086    let listener = UnixListener::bind(&path)?;
3087    let handle = tokio::spawn(async move {
3088        while let Ok((stream, _)) = listener.accept().await {
3089            let (keypair, inbox_sender) = (keypair.clone(), inbox_sender.clone());
3090            tokio::spawn(async move {
3091                let _ = handle_connection(stream, require_peer_auth, &keypair, &inbox_sender).await;
3092            });
3093        }
3094    });
3095    Ok(ListenerHandle {
3096        handle,
3097        local_addr: None,
3098    })
3099}
3100
3101#[cfg(not(target_arch = "wasm32"))]
3102struct TcpListenerConfig {
3103    addr: String,
3104    keypair: Arc<Keypair>,
3105    router: Arc<Router>,
3106    trusted: Arc<parking_lot::RwLock<TrustStore>>,
3107    require_peer_auth: bool,
3108    inbox_sender: InboxSender,
3109    pairing_password: Option<String>,
3110    advertise_address: Option<String>,
3111    trusted_peers_path: std::path::PathBuf,
3112    participant_name: String,
3113    bridge_bootstrap_token: String,
3114}
3115
3116#[cfg(not(target_arch = "wasm32"))]
3117async fn spawn_tcp_listener(config: TcpListenerConfig) -> Result<ListenerHandle, std::io::Error> {
3118    let TcpListenerConfig {
3119        addr,
3120        keypair,
3121        router,
3122        trusted,
3123        require_peer_auth,
3124        inbox_sender,
3125        pairing_password,
3126        advertise_address,
3127        trusted_peers_path,
3128        participant_name,
3129        bridge_bootstrap_token,
3130    } = config;
3131    if let Some(secret) = pairing_password.as_deref() {
3132        validate_pairing_secret(secret)?;
3133    }
3134    let listener = TcpListener::bind(&addr).await?;
3135    let local_addr = listener.local_addr()?;
3136    let target_address = if let Some(advertise_address) = advertise_address {
3137        parse_peer_address(&advertise_address).map_err(|error| {
3138            std::io::Error::new(
3139                std::io::ErrorKind::InvalidInput,
3140                format!("invalid comms advertised address '{advertise_address}': {error}"),
3141            )
3142        })?;
3143        advertise_address
3144    } else {
3145        if local_addr.ip().is_unspecified() {
3146            return Err(std::io::Error::new(
3147                std::io::ErrorKind::InvalidInput,
3148                "signed comms listener bound to a wildcard address requires an explicit advertised address",
3149            ));
3150        }
3151        format!("tcp://{local_addr}")
3152    };
3153    let handle = tokio::spawn(async move {
3154        while let Ok((stream, _)) = listener.accept().await {
3155            let (keypair, router, trusted, inbox_sender) = (
3156                keypair.clone(),
3157                router.clone(),
3158                trusted.clone(),
3159                inbox_sender.clone(),
3160            );
3161            let pairing_password = pairing_password.clone();
3162            let trusted_peers_path = trusted_peers_path.clone();
3163            let participant_name = participant_name.clone();
3164            let bridge_bootstrap_token = bridge_bootstrap_token.clone();
3165            let target_address = target_address.clone();
3166            tokio::spawn(async move {
3167                // Share the router-owned trust handle directly — no
3168                // snapshot clone — so the transport trust gate reads the
3169                // same live state the inbox admission gate consults.
3170                let _ = handle_tcp_connection_or_pairing(
3171                    stream,
3172                    require_peer_auth,
3173                    &keypair,
3174                    &router,
3175                    &trusted,
3176                    &inbox_sender,
3177                    pairing_password.as_deref(),
3178                    &trusted_peers_path,
3179                    &participant_name,
3180                    &bridge_bootstrap_token,
3181                    &target_address,
3182                )
3183                .await;
3184            });
3185        }
3186    });
3187    Ok(ListenerHandle {
3188        handle,
3189        local_addr: Some(local_addr),
3190    })
3191}
3192
3193#[cfg(not(target_arch = "wasm32"))]
3194#[allow(clippy::too_many_arguments)]
3195async fn handle_tcp_connection_or_pairing(
3196    stream: TcpStream,
3197    require_peer_auth: bool,
3198    keypair: &Keypair,
3199    router: &Router,
3200    trusted: &Arc<parking_lot::RwLock<TrustStore>>,
3201    inbox_sender: &InboxSender,
3202    pairing_password: Option<&str>,
3203    trusted_peers_path: &Path,
3204    participant_name: &str,
3205    bridge_bootstrap_token: &str,
3206    target_address: &str,
3207) -> Result<(), std::io::Error> {
3208    let mut first = [0_u8; 1];
3209    let is_pairing = pairing_password.is_some()
3210        && matches!(
3211            tokio::time::timeout(PAIRING_CLASSIFY_TIMEOUT, stream.peek(&mut first)).await,
3212            Ok(Ok(1))
3213        )
3214        && first[0] == b'{';
3215    if is_pairing {
3216        let Some(pairing_password) = pairing_password else {
3217            return handle_connection(stream, require_peer_auth, keypair, inbox_sender)
3218                .await
3219                .map_err(|err| std::io::Error::other(err.to_string()));
3220        };
3221        return handle_pairing_connection(
3222            stream,
3223            keypair,
3224            router,
3225            trusted,
3226            pairing_password,
3227            trusted_peers_path,
3228            participant_name,
3229            bridge_bootstrap_token,
3230            target_address,
3231        )
3232        .await;
3233    }
3234
3235    handle_connection(stream, require_peer_auth, keypair, inbox_sender)
3236        .await
3237        .map_err(|err| std::io::Error::other(err.to_string()))
3238}
3239
3240#[cfg(not(target_arch = "wasm32"))]
3241async fn read_pairing_message(
3242    reader: &mut BufReader<TcpStream>,
3243    buf: &mut String,
3244) -> Result<PairingClientMessage, std::io::Error> {
3245    buf.clear();
3246    let bytes = tokio::time::timeout(std::time::Duration::from_secs(10), reader.read_line(buf))
3247        .await
3248        .map_err(|_| {
3249            std::io::Error::new(std::io::ErrorKind::TimedOut, "pairing read timed out")
3250        })??;
3251    if bytes == 0 {
3252        return Err(std::io::Error::new(
3253            std::io::ErrorKind::UnexpectedEof,
3254            "pairing peer closed connection",
3255        ));
3256    }
3257    serde_json::from_str(buf).map_err(|err| {
3258        std::io::Error::new(
3259            std::io::ErrorKind::InvalidData,
3260            format!("invalid pairing message: {err}"),
3261        )
3262    })
3263}
3264
3265#[cfg(not(target_arch = "wasm32"))]
3266async fn write_pairing_json(
3267    writer: &mut TcpStream,
3268    value: serde_json::Value,
3269) -> Result<(), std::io::Error> {
3270    let bytes = serde_json::to_vec(&value).map_err(|err| {
3271        std::io::Error::new(
3272            std::io::ErrorKind::InvalidData,
3273            format!("pairing response encode failed: {err}"),
3274        )
3275    })?;
3276    writer.write_all(&bytes).await?;
3277    writer.write_all(b"\n").await?;
3278    writer.flush().await
3279}
3280
3281#[cfg(not(target_arch = "wasm32"))]
3282#[allow(clippy::too_many_arguments)]
3283async fn handle_pairing_connection(
3284    stream: TcpStream,
3285    keypair: &Keypair,
3286    router: &Router,
3287    trusted: &Arc<parking_lot::RwLock<TrustStore>>,
3288    pairing_password: &str,
3289    trusted_peers_path: &Path,
3290    participant_name: &str,
3291    bridge_bootstrap_token: &str,
3292    target_address: &str,
3293) -> Result<(), std::io::Error> {
3294    let mut reader = BufReader::new(stream);
3295    let mut line = String::new();
3296    let PairingClientMessage::Hello { version } =
3297        read_pairing_message(&mut reader, &mut line).await?
3298    else {
3299        return Err(std::io::Error::new(
3300            std::io::ErrorKind::InvalidData,
3301            "expected meerkat pairing hello",
3302        ));
3303    };
3304    if version != PAIRING_VERSION {
3305        return Err(std::io::Error::new(
3306            std::io::ErrorKind::InvalidData,
3307            "unsupported meerkat pairing version",
3308        ));
3309    }
3310
3311    let challenge = Uuid::new_v4().to_string();
3312    write_pairing_json(
3313        reader.get_mut(),
3314        serde_json::json!({
3315            "kind": PAIRING_CHALLENGE_KIND,
3316            "version": PAIRING_VERSION,
3317            "challenge": challenge,
3318            "target": {
3319                "name": participant_name,
3320                "address": target_address,
3321                "identity": {
3322                    "kind": "ed25519_public_key",
3323                    "public_key": keypair.public_key().to_pubkey_string(),
3324                }
3325            }
3326        }),
3327    )
3328    .await?;
3329
3330    // Typed pairing-proof deserialization fails closed: a malformed public
3331    // key, an identity kind other than `ed25519_public_key`, or a missing
3332    // `password_proof` is rejected here, before any trust row is derived. No
3333    // raw-JSON field reads and no late, second public-key parse downstream.
3334    let PairingClientMessage::Proof {
3335        caller,
3336        password_proof,
3337    } = read_pairing_message(&mut reader, &mut line).await?
3338    else {
3339        return Err(std::io::Error::new(
3340            std::io::ErrorKind::InvalidData,
3341            "expected meerkat pairing proof",
3342        ));
3343    };
3344    let PairingIdentityKind::Ed25519PublicKey = caller.identity.kind;
3345    let expected = comms_pairing_password_proof(
3346        pairing_password,
3347        &challenge,
3348        &caller.identity.public_key.raw,
3349        &caller.address.0,
3350    );
3351    if !constant_time_str_eq(&password_proof, &expected) {
3352        return Err(std::io::Error::new(
3353            std::io::ErrorKind::PermissionDenied,
3354            "invalid pairing password proof",
3355        ));
3356    }
3357
3358    // The trusted peer is built from the typed identity validated at
3359    // deserialization — not from re-parsed free strings. An invalid caller
3360    // name or address is a typed rejection before any trust row is derived.
3361    let pubkey = caller.identity.public_key.key;
3362    let caller_name = PeerName::new(caller.name.clone())
3363        .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
3364    let caller_address = meerkat_core::comms::PeerAddress::parse(&caller.address.0)
3365        .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string()))?;
3366    // Paired external-peer trust is a MobMachine-owned external-peer edge: the
3367    // remote target paired into this mob's control plane. Attribute the trust
3368    // row to the generated MobMachineExternalPeerTrustWiring authority source
3369    // rather than a legacy/unattributed mutation (Dogma Invariant 1).
3370    let paired_peer_id = pubkey.to_peer_id();
3371    router
3372        .add_trusted_peer_for_source(
3373            TrustEntry {
3374                peer_id: paired_peer_id,
3375                name: caller_name,
3376                pubkey,
3377                address: caller_address,
3378                meta: crate::PeerMeta::default(),
3379            },
3380            meerkat_core::comms::GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustWiring,
3381            false,
3382        )
3383        .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string()))?;
3384    let peers_snapshot = trusted.read().clone();
3385    peers_snapshot
3386        .save(trusted_peers_path)
3387        .await
3388        .map_err(|err| std::io::Error::other(format!("persist paired trust: {err}")))?;
3389
3390    write_pairing_json(
3391        reader.get_mut(),
3392        serde_json::json!({
3393            "kind": PAIRING_COMPLETE_KIND,
3394            "version": PAIRING_VERSION,
3395            "binding": {
3396                "kind": "external",
3397                "address": target_address,
3398                "bootstrap_token": bridge_bootstrap_token,
3399                "identity": {
3400                    "kind": "ed25519_public_key",
3401                    "public_key": keypair.public_key().to_pubkey_string(),
3402                }
3403            },
3404            "comms_name": participant_name,
3405        }),
3406    )
3407    .await
3408}
3409
3410/// Max concurrent connections for the plain listener (DoS protection).
3411#[cfg(not(target_arch = "wasm32"))]
3412const PLAIN_LISTENER_MAX_CONCURRENT: usize = 64;
3413
3414#[cfg(not(target_arch = "wasm32"))]
3415async fn spawn_plain_tcp_listener(
3416    addr: &str,
3417    inbox_sender: InboxSender,
3418    max_line_length: usize,
3419) -> Result<ListenerHandle, std::io::Error> {
3420    let listener = TcpListener::bind(addr).await?;
3421    let semaphore = Arc::new(tokio::sync::Semaphore::new(PLAIN_LISTENER_MAX_CONCURRENT));
3422    // Shared typed ingress-fault counters across all connection tasks for this
3423    // listener (codec overflow / inbox-full backpressure).
3424    let faults = Arc::new(crate::plain_listener::PlainIngressFaults::default());
3425    let handle = tokio::spawn(async move {
3426        while let Ok((stream, _peer)) = listener.accept().await {
3427            let sender = inbox_sender.clone();
3428            let sem = semaphore.clone();
3429            let faults = faults.clone();
3430            tokio::spawn(async move {
3431                let _permit = match sem.acquire().await {
3432                    Ok(p) => p,
3433                    Err(_) => return, // Semaphore closed
3434                };
3435                crate::plain_listener::handle_plain_connection(
3436                    stream,
3437                    sender,
3438                    max_line_length,
3439                    meerkat_core::PlainEventSource::Tcp,
3440                    &faults,
3441                )
3442                .await;
3443            });
3444        }
3445    });
3446    Ok(ListenerHandle {
3447        handle,
3448        local_addr: None,
3449    })
3450}
3451
3452#[cfg(unix)]
3453async fn spawn_plain_uds_listener(
3454    path: &std::path::Path,
3455    inbox_sender: InboxSender,
3456    max_line_length: usize,
3457) -> Result<ListenerHandle, std::io::Error> {
3458    use tokio::net::UnixListener;
3459    let path = path.to_path_buf();
3460    if let Err(err) = tokio::fs::remove_file(&path).await
3461        && err.kind() != std::io::ErrorKind::NotFound
3462    {
3463        return Err(err);
3464    }
3465    if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
3466        tokio::fs::create_dir_all(parent).await?;
3467    }
3468    let listener = UnixListener::bind(&path)?;
3469    let semaphore = Arc::new(tokio::sync::Semaphore::new(PLAIN_LISTENER_MAX_CONCURRENT));
3470    // Shared typed ingress-fault counters across all connection tasks for this
3471    // listener (codec overflow / inbox-full backpressure).
3472    let faults = Arc::new(crate::plain_listener::PlainIngressFaults::default());
3473    let handle = tokio::spawn(async move {
3474        while let Ok((stream, _)) = listener.accept().await {
3475            let sender = inbox_sender.clone();
3476            let sem = semaphore.clone();
3477            let faults = faults.clone();
3478            tokio::spawn(async move {
3479                let _permit = match sem.acquire().await {
3480                    Ok(p) => p,
3481                    Err(_) => return,
3482                };
3483                crate::plain_listener::handle_plain_connection(
3484                    stream,
3485                    sender,
3486                    max_line_length,
3487                    meerkat_core::PlainEventSource::Uds,
3488                    &faults,
3489                )
3490                .await;
3491            });
3492        }
3493    });
3494    Ok(ListenerHandle {
3495        handle,
3496        local_addr: None,
3497    })
3498}
3499
3500#[cfg(all(test, not(target_arch = "wasm32")))]
3501#[allow(clippy::unwrap_used, clippy::expect_used)]
3502mod tests {
3503    use super::*;
3504    use crate::classify::test_support;
3505
3506    /// The dyn core-trait surface is how mob/runtime layers holding
3507    /// `Arc<dyn CoreCommsRuntime>` install the host's declaration per member;
3508    /// it must delegate to the concrete router config, not the typed default.
3509    #[tokio::test]
3510    async fn dyn_trait_taint_setter_delegates_to_router_declaration() {
3511        let runtime =
3512            Arc::new(CommsRuntime::inproc_only("dyn-taint-setter").expect("inproc runtime"));
3513        let dyn_runtime: Arc<dyn CoreCommsRuntime> = runtime.clone();
3514
3515        dyn_runtime
3516            .set_outbound_content_taint(Some(meerkat_core::comms::SenderContentTaint::Tainted))
3517            .expect("concrete runtime must accept the declaration");
3518        assert_eq!(
3519            runtime.outbound_content_taint(),
3520            Some(meerkat_core::comms::SenderContentTaint::Tainted)
3521        );
3522
3523        dyn_runtime
3524            .set_outbound_content_taint(None)
3525            .expect("clearing the declaration must succeed");
3526        assert_eq!(runtime.outbound_content_taint(), None);
3527    }
3528    use crate::event_injector::CommsEventInjector;
3529    use crate::identity::Signature;
3530    use crate::types::{Envelope, InboxItem, MessageKind, Status};
3531    use async_trait::async_trait;
3532    use futures::StreamExt;
3533    use meerkat_core::event_injector::SubscribableInjector;
3534    use meerkat_core::{
3535        BlobId, BlobPayload, BlobRef, BlobStore, BlobStoreError, SendError,
3536        comms::{
3537            CommsTrustMutation, CommsTrustMutationResult, InputSource, InputStreamMode,
3538            PeerDirectorySource, PeerId, PeerName, PeerRoute, PeerSendability, StreamError,
3539            StreamScope, TrustedPeerDescriptor,
3540        },
3541        interaction::InteractionId,
3542        types::{ContentBlock, ImageData, SessionId},
3543    };
3544
3545    /// ROW #344 gate: the pairing caller is built from a typed identity that
3546    /// deserializes once and fails closed.
3547    ///
3548    /// - A malformed public key (not `ed25519:<base64>`) is rejected at typed
3549    ///   deserialization, before any trust row could be derived.
3550    /// - An identity `kind` other than `ed25519_public_key` is rejected by the
3551    ///   typed enum — no String equality check downstream.
3552    /// - A well-formed caller yields a typed [`PubKey`] (the trust row is built
3553    ///   from `identity.public_key.key`, not a re-parsed free string).
3554    #[test]
3555    fn pairing_caller_identity_is_typed_and_fails_closed() {
3556        // Malformed public key → typed deserialization rejects it.
3557        let bad_key = serde_json::json!({
3558            "name": "peer",
3559            "address": "tcp://127.0.0.1:4200",
3560            "identity": {
3561                "kind": "ed25519_public_key",
3562                "public_key": "not-a-valid-pubkey",
3563            }
3564        });
3565        assert!(
3566            serde_json::from_value::<PairingPeer>(bad_key).is_err(),
3567            "a malformed pairing public key must be rejected at deserialization"
3568        );
3569
3570        // Wrong identity kind → typed enum rejects it (no String == check).
3571        let valid_pubkey = Keypair::generate().public_key();
3572        let wrong_kind = serde_json::json!({
3573            "name": "peer",
3574            "address": "tcp://127.0.0.1:4200",
3575            "identity": {
3576                "kind": "rsa_public_key",
3577                "public_key": valid_pubkey.to_pubkey_string(),
3578            }
3579        });
3580        assert!(
3581            serde_json::from_value::<PairingPeer>(wrong_kind).is_err(),
3582            "a non-ed25519 identity kind must be rejected at deserialization"
3583        );
3584
3585        // Well-formed caller → typed PubKey is recovered, raw string retained
3586        // for the proof hash.
3587        let good = serde_json::json!({
3588            "name": "peer",
3589            "address": "tcp://127.0.0.1:4200",
3590            "identity": {
3591                "kind": "ed25519_public_key",
3592                "public_key": valid_pubkey.to_pubkey_string(),
3593            }
3594        });
3595        let caller: PairingPeer =
3596            serde_json::from_value(good).expect("well-formed pairing caller deserializes");
3597        assert_eq!(caller.identity.public_key.key, valid_pubkey);
3598        assert_eq!(
3599            caller.identity.public_key.raw,
3600            valid_pubkey.to_pubkey_string()
3601        );
3602        assert_eq!(caller.address.0, "tcp://127.0.0.1:4200");
3603        assert_eq!(caller.identity.kind, PairingIdentityKind::Ed25519PublicKey);
3604    }
3605
3606    /// Pairing ingress gate: the full client wire message parses once into the
3607    /// typed [`PairingClientMessage`] envelope and fails closed — no raw-JSON
3608    /// field-by-field reads remain on the trust-enrollment path.
3609    #[test]
3610    fn pairing_client_messages_parse_typed_and_fail_closed() {
3611        // Unknown kind → rejected at typed deserialization.
3612        assert!(
3613            serde_json::from_value::<PairingClientMessage>(serde_json::json!({
3614                "kind": "meerkat_pairing_evil",
3615                "version": 1,
3616            }))
3617            .is_err(),
3618            "an unknown pairing message kind must be rejected"
3619        );
3620
3621        // Hello missing version → rejected (no default-zero laundering).
3622        assert!(
3623            serde_json::from_value::<PairingClientMessage>(serde_json::json!({
3624                "kind": "meerkat_pairing_hello",
3625            }))
3626            .is_err(),
3627            "a pairing hello without a version must be rejected"
3628        );
3629
3630        // Proof missing password_proof → rejected before any trust derivation.
3631        let pubkey = Keypair::generate().public_key();
3632        assert!(
3633            serde_json::from_value::<PairingClientMessage>(serde_json::json!({
3634                "kind": "meerkat_pairing_proof",
3635                "caller": {
3636                    "name": "peer",
3637                    "address": "tcp://127.0.0.1:4200",
3638                    "identity": {
3639                        "kind": "ed25519_public_key",
3640                        "public_key": pubkey.to_pubkey_string(),
3641                    }
3642                }
3643            }))
3644            .is_err(),
3645            "a pairing proof without a password proof must be rejected"
3646        );
3647
3648        // Well-formed messages parse into the typed variants.
3649        let hello: PairingClientMessage = serde_json::from_value(serde_json::json!({
3650            "kind": "meerkat_pairing_hello",
3651            "version": 1,
3652        }))
3653        .expect("well-formed hello parses");
3654        assert!(matches!(hello, PairingClientMessage::Hello { version: 1 }));
3655        let proof: PairingClientMessage = serde_json::from_value(serde_json::json!({
3656            "kind": "meerkat_pairing_proof",
3657            "password_proof": "proof",
3658            "caller": {
3659                "name": "peer",
3660                "address": "tcp://127.0.0.1:4200",
3661                "identity": {
3662                    "kind": "ed25519_public_key",
3663                    "public_key": pubkey.to_pubkey_string(),
3664                }
3665            }
3666        }))
3667        .expect("well-formed proof parses");
3668        match proof {
3669            PairingClientMessage::Proof {
3670                caller,
3671                password_proof,
3672            } => {
3673                assert_eq!(password_proof, "proof");
3674                assert_eq!(caller.identity.public_key.key, pubkey);
3675            }
3676            PairingClientMessage::Hello { .. } => panic!("expected proof variant"),
3677        }
3678    }
3679
3680    /// Test helper: build a [`TrustedPeerDescriptor`] from raw pubkey +
3681    /// display name + address string. Panics on invalid input — tests are
3682    /// expected to supply valid values.
3683    fn trusted_descriptor(name: &str, pubkey: PubKey, address: &str) -> TrustedPeerDescriptor {
3684        TrustedPeerDescriptor {
3685            peer_id: crate::router::peer_id_from_pubkey(&pubkey),
3686            name: PeerName::new(name.to_string()).expect("valid peer name"),
3687            address: parse_peer_address(address).expect("valid peer address"),
3688            pubkey: *pubkey.as_bytes(),
3689        }
3690    }
3691
3692    fn local_descriptor_for_runtime(runtime: &CommsRuntime) -> TrustedPeerDescriptor {
3693        trusted_descriptor(
3694            runtime.participant_name(),
3695            runtime.public_key(),
3696            &runtime.advertised_address(),
3697        )
3698    }
3699
3700    /// Test helper: build a typed [`TrustEntry`]. Panics on invalid input —
3701    /// tests are expected to supply valid values.
3702    fn test_trust_entry(name: &str, pubkey: PubKey, address: &str) -> TrustEntry {
3703        TrustEntry {
3704            peer_id: crate::router::peer_id_from_pubkey(&pubkey),
3705            name: PeerName::new(name.to_string()).expect("valid peer name"),
3706            pubkey,
3707            address: parse_peer_address(address).expect("valid peer address"),
3708            meta: crate::PeerMeta::default(),
3709        }
3710    }
3711
3712    struct TestProjectionTrustAuthority {
3713        dsl: Arc<meerkat_runtime::HandleDslAuthority>,
3714        authority: meerkat_core::comms::CommsTrustMutationAuthority,
3715    }
3716
3717    impl TestProjectionTrustAuthority {
3718        fn install_owner(&self, runtime: &CommsRuntime) {
3719            meerkat_runtime::RuntimePeerCommsHandle::install_generated_on(
3720                Arc::clone(&self.dsl),
3721                runtime,
3722            )
3723            .expect("install generated peer-comms handle");
3724        }
3725    }
3726
3727    fn try_test_projection_add_authority(
3728        local: &TrustedPeerDescriptor,
3729        peer: &TrustedPeerDescriptor,
3730        epoch: u64,
3731    ) -> Result<TestProjectionTrustAuthority, String> {
3732        let local_endpoint = meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(local);
3733        let endpoint = meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(peer);
3734        let target_epoch = epoch.max(1);
3735        let dsl = Arc::new(meerkat_runtime::HandleDslAuthority::ephemeral());
3736        dsl.apply_signal(
3737            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineSignal::Initialize,
3738            "test_projection_add_authority::initialize",
3739        )
3740        .map_err(|err| err.to_string())?;
3741        dsl.apply_input(
3742            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::RegisterSession {
3743                session_id: meerkat_runtime::meerkat_machine::dsl::SessionId::from(
3744                    "meerkat-comms-runtime-test-projection",
3745                ),
3746            },
3747            "test_projection_add_authority::register_session",
3748        )
3749        .map_err(|err| err.to_string())?;
3750        dsl.apply_input(
3751            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::PrepareBindings {
3752                agent_runtime_id: meerkat_runtime::meerkat_machine::dsl::AgentRuntimeId::from(
3753                    "meerkat-comms-runtime-test-projection-runtime",
3754                ),
3755                fence_token: meerkat_runtime::meerkat_machine::dsl::FenceToken::from(0),
3756                generation: Some(meerkat_runtime::meerkat_machine::dsl::Generation::from(0)),
3757                runtime_epoch_id: None,
3758                session_id: meerkat_runtime::meerkat_machine::dsl::SessionId::from(
3759                    "meerkat-comms-runtime-test-projection",
3760                ),
3761            },
3762            "test_projection_add_authority::prepare_bindings",
3763        )
3764        .map_err(|err| err.to_string())?;
3765        dsl.apply_input(
3766            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::PublishLocalEndpoint {
3767                endpoint: local_endpoint,
3768            },
3769            "test_projection_add_authority::publish_local_endpoint",
3770        )
3771        .map_err(|err| err.to_string())?;
3772        for filler_index in 1..target_epoch {
3773            let filler_key = Keypair::generate().public_key();
3774            let filler = trusted_descriptor(
3775                &format!("filler-add-{filler_index}"),
3776                filler_key,
3777                &format!("inproc://filler-add-{filler_index}"),
3778            );
3779            let filler_endpoint =
3780                meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(&filler);
3781            dsl.apply_input(
3782                meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::AddDirectPeerEndpoint {
3783                    endpoint: filler_endpoint,
3784                },
3785                "test_projection_add_authority::add_filler_endpoint",
3786            )
3787            .map_err(|err| err.to_string())?;
3788        }
3789        let transition = dsl
3790            .apply_input_with_transition(
3791                meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::AddDirectPeerEndpoint {
3792                    endpoint: endpoint.clone(),
3793                },
3794                "test_projection_add_authority::add_endpoint",
3795            )
3796            .map_err(|err| err.to_string())?;
3797        let mut obligations =
3798            meerkat_runtime::protocol_comms_trust_reconcile::extract_obligations_with_freshness(
3799                &transition,
3800                dsl.peer_projection_freshness_authority(),
3801            );
3802        let obligation = obligations
3803            .pop()
3804            .ok_or_else(|| "generated reconcile obligation missing".to_string())?;
3805        let authority = meerkat_runtime::protocol_comms_trust_reconcile::authority_for_endpoint(
3806            &obligation,
3807            &endpoint,
3808        )?;
3809        Ok(TestProjectionTrustAuthority { dsl, authority })
3810    }
3811
3812    fn test_projection_add_authority(
3813        local: &TrustedPeerDescriptor,
3814        peer: &TrustedPeerDescriptor,
3815        epoch: u64,
3816    ) -> TestProjectionTrustAuthority {
3817        try_test_projection_add_authority(local, peer, epoch)
3818            .expect("generated peer projection add authority")
3819    }
3820
3821    fn test_projection_add_authority_with_dsl(
3822        dsl: Arc<meerkat_runtime::HandleDslAuthority>,
3823        peer: &TrustedPeerDescriptor,
3824    ) -> TestProjectionTrustAuthority {
3825        let endpoint = meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(peer);
3826        let transition = dsl
3827            .apply_input_with_transition(
3828                meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::AddDirectPeerEndpoint {
3829                    endpoint: endpoint.clone(),
3830                },
3831                "test_projection_add_authority_with_dsl::add_endpoint",
3832            )
3833            .expect("AddDirectPeerEndpoint input");
3834        let mut obligations =
3835            meerkat_runtime::protocol_comms_trust_reconcile::extract_obligations_with_freshness(
3836                &transition,
3837                dsl.peer_projection_freshness_authority(),
3838            );
3839        let obligation = obligations.pop().expect("generated reconcile obligation");
3840        let authority = meerkat_runtime::protocol_comms_trust_reconcile::authority_for_endpoint(
3841            &obligation,
3842            &endpoint,
3843        )
3844        .expect("generated peer projection add authority");
3845        TestProjectionTrustAuthority { dsl, authority }
3846    }
3847
3848    fn test_projection_remove_authority(
3849        local: &TrustedPeerDescriptor,
3850        peer: &TrustedPeerDescriptor,
3851        epoch: u64,
3852    ) -> TestProjectionTrustAuthority {
3853        let local_endpoint = meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(local);
3854        let endpoint = meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(peer);
3855        let target_epoch = epoch.max(2);
3856        let dsl = Arc::new(meerkat_runtime::HandleDslAuthority::ephemeral());
3857        dsl.apply_signal(
3858            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineSignal::Initialize,
3859            "test_projection_remove_authority::initialize",
3860        )
3861        .expect("Initialize signal");
3862        dsl.apply_input(
3863            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::RegisterSession {
3864                session_id: meerkat_runtime::meerkat_machine::dsl::SessionId::from(
3865                    "meerkat-comms-runtime-test-projection",
3866                ),
3867            },
3868            "test_projection_remove_authority::register_session",
3869        )
3870        .expect("RegisterSession input");
3871        dsl.apply_input(
3872            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::PrepareBindings {
3873                agent_runtime_id: meerkat_runtime::meerkat_machine::dsl::AgentRuntimeId::from(
3874                    "meerkat-comms-runtime-test-projection-runtime",
3875                ),
3876                fence_token: meerkat_runtime::meerkat_machine::dsl::FenceToken::from(0),
3877                generation: Some(meerkat_runtime::meerkat_machine::dsl::Generation::from(0)),
3878                runtime_epoch_id: None,
3879                session_id: meerkat_runtime::meerkat_machine::dsl::SessionId::from(
3880                    "meerkat-comms-runtime-test-projection",
3881                ),
3882            },
3883            "test_projection_remove_authority::prepare_bindings",
3884        )
3885        .expect("PrepareBindings input");
3886        dsl.apply_input(
3887            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::PublishLocalEndpoint {
3888                endpoint: local_endpoint,
3889            },
3890            "test_projection_remove_authority::publish_local_endpoint",
3891        )
3892        .expect("PublishLocalEndpoint input");
3893        dsl.apply_input(
3894            meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::AddDirectPeerEndpoint {
3895                endpoint: endpoint.clone(),
3896            },
3897            "test_projection_remove_authority::add_endpoint",
3898        )
3899        .expect("AddDirectPeerEndpoint input");
3900        for filler_index in 2..target_epoch {
3901            let filler_key = Keypair::generate().public_key();
3902            let filler = trusted_descriptor(
3903                &format!("filler-remove-{filler_index}"),
3904                filler_key,
3905                &format!("inproc://filler-remove-{filler_index}"),
3906            );
3907            let filler_endpoint =
3908                meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(&filler);
3909            dsl.apply_input(
3910                meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::AddDirectPeerEndpoint {
3911                    endpoint: filler_endpoint,
3912                },
3913                "test_projection_remove_authority::add_filler_endpoint",
3914            )
3915            .expect("AddDirectPeerEndpoint filler input");
3916        }
3917        let transition = dsl
3918            .apply_input_with_transition(
3919                meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::RemoveDirectPeerEndpoint {
3920                    endpoint: endpoint.clone(),
3921                },
3922                "test_projection_remove_authority::remove_endpoint",
3923            )
3924            .expect("RemoveDirectPeerEndpoint input");
3925        let mut obligations =
3926            meerkat_runtime::protocol_comms_trust_reconcile::extract_obligations_with_freshness(
3927                &transition,
3928                dsl.peer_projection_freshness_authority(),
3929            );
3930        let obligation = obligations.pop().expect("generated reconcile obligation");
3931        let authority =
3932            meerkat_runtime::protocol_comms_trust_reconcile::removal_authority_for_peer_id(
3933                &obligation,
3934                &endpoint.peer_id.0,
3935            )
3936            .expect("generated peer projection remove authority");
3937        TestProjectionTrustAuthority { dsl, authority }
3938    }
3939
3940    fn test_projection_remove_authority_with_dsl(
3941        dsl: Arc<meerkat_runtime::HandleDslAuthority>,
3942        peer: &TrustedPeerDescriptor,
3943    ) -> TestProjectionTrustAuthority {
3944        let endpoint = meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(peer);
3945        let transition = dsl
3946            .apply_input_with_transition(
3947                meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::RemoveDirectPeerEndpoint {
3948                    endpoint: endpoint.clone(),
3949                },
3950                "test_projection_remove_authority_with_dsl::remove_endpoint",
3951            )
3952            .expect("RemoveDirectPeerEndpoint input");
3953        let mut obligations =
3954            meerkat_runtime::protocol_comms_trust_reconcile::extract_obligations_with_freshness(
3955                &transition,
3956                dsl.peer_projection_freshness_authority(),
3957            );
3958        let obligation = obligations.pop().expect("generated reconcile obligation");
3959        let authority =
3960            meerkat_runtime::protocol_comms_trust_reconcile::removal_authority_for_peer_id(
3961                &obligation,
3962                &endpoint.peer_id.0,
3963            )
3964            .expect("generated peer projection remove authority");
3965        TestProjectionTrustAuthority { dsl, authority }
3966    }
3967
3968    async fn try_add_trusted_peer_with_generated_authority(
3969        runtime: &CommsRuntime,
3970        peer: TrustedPeerDescriptor,
3971    ) -> Result<Arc<meerkat_runtime::HandleDslAuthority>, SendError> {
3972        let projection =
3973            try_test_projection_add_authority(&local_descriptor_for_runtime(runtime), &peer, 0)
3974                .map_err(SendError::Validation)?;
3975        let dsl = Arc::clone(&projection.dsl);
3976        projection.install_owner(runtime);
3977        CoreCommsRuntime::apply_trust_mutation(
3978            runtime,
3979            CommsTrustMutation::AddTrustedPeer {
3980                authority: projection.authority,
3981                peer,
3982            },
3983        )
3984        .await
3985        .map(|_| dsl)
3986    }
3987
3988    async fn add_trusted_peer_with_generated_authority(
3989        runtime: &CommsRuntime,
3990        peer: TrustedPeerDescriptor,
3991    ) -> Arc<meerkat_runtime::HandleDslAuthority> {
3992        try_add_trusted_peer_with_generated_authority(runtime, peer)
3993            .await
3994            .expect("generated add trusted peer should apply")
3995    }
3996
3997    async fn remove_trusted_peer_with_generated_authority(
3998        runtime: &CommsRuntime,
3999        peer: &TrustedPeerDescriptor,
4000    ) -> Result<bool, SendError> {
4001        let projection =
4002            test_projection_remove_authority(&local_descriptor_for_runtime(runtime), peer, 0);
4003        projection.install_owner(runtime);
4004        let result = CoreCommsRuntime::apply_trust_mutation(
4005            runtime,
4006            CommsTrustMutation::RemoveTrustedPeer {
4007                authority: projection.authority,
4008                peer_id: peer.peer_id.to_string(),
4009            },
4010        )
4011        .await?;
4012        match result {
4013            CommsTrustMutationResult::Removed { removed } => Ok(removed),
4014            other => Err(SendError::Validation(format!(
4015                "unexpected generated trust mutation result: {other:?}"
4016            ))),
4017        }
4018    }
4019
4020    #[tokio::test]
4021    async fn generated_peer_projection_trust_requires_canonical_owner() {
4022        let runtime = CommsRuntime::inproc_only("trust-owner-mismatch").expect("runtime");
4023        let local = local_descriptor_for_runtime(&runtime);
4024        let peer_key = Keypair::generate().public_key();
4025        let descriptor = trusted_descriptor("peer", peer_key, "inproc://peer");
4026
4027        let authority = test_projection_add_authority(&local, &descriptor, 3);
4028        let missing_owner = CoreCommsRuntime::apply_trust_mutation(
4029            &runtime,
4030            CommsTrustMutation::AddTrustedPeer {
4031                peer: descriptor.clone(),
4032                authority: authority.authority.clone(),
4033            },
4034        )
4035        .await
4036        .expect_err("generated authority without runtime owner must fail closed");
4037        assert!(
4038            matches!(missing_owner, SendError::Validation(ref message) if message.contains("requires the target runtime's generated owner token")),
4039            "unexpected missing-owner rejection: {missing_owner:?}"
4040        );
4041
4042        let foreign_owner = test_projection_add_authority(&local, &descriptor, 4);
4043        foreign_owner.install_owner(&runtime);
4044        let mismatched_owner = CoreCommsRuntime::apply_trust_mutation(
4045            &runtime,
4046            CommsTrustMutation::AddTrustedPeer {
4047                peer: descriptor,
4048                authority: authority.authority,
4049            },
4050        )
4051        .await
4052        .expect_err("foreign generated owner must fail closed");
4053        assert!(
4054            matches!(mismatched_owner, SendError::Validation(ref message) if message.contains("different generated owner")),
4055            "unexpected owner-mismatch rejection: {mismatched_owner:?}"
4056        );
4057        assert!(
4058            runtime.peers().await.is_empty(),
4059            "owner mismatch must not mutate peer directory projection"
4060        );
4061    }
4062
4063    #[tokio::test]
4064    async fn recovered_mob_trust_owner_cannot_replace_live_owner() {
4065        let runtime = CommsRuntime::inproc_only("mob-trust-owner-recovery-guard").expect("runtime");
4066        let live_owner: Arc<dyn std::any::Any + Send + Sync> = Arc::new("live-owner");
4067        let recovered_owner: Arc<dyn std::any::Any + Send + Sync> = Arc::new("recovered-owner");
4068
4069        CoreCommsRuntime::validate_recovered_generated_mob_trust_owner(
4070            &runtime,
4071            Arc::clone(&recovered_owner),
4072        )
4073        .await
4074        .expect("validation without installed owner should pass without binding");
4075        CoreCommsRuntime::install_generated_mob_trust_owner(&runtime, Arc::clone(&live_owner))
4076            .await
4077            .expect("install live mob owner");
4078        let validation_err = CoreCommsRuntime::validate_recovered_generated_mob_trust_owner(
4079            &runtime,
4080            Arc::clone(&recovered_owner),
4081        )
4082        .await
4083        .expect_err("read-only validation must not have installed recovered owner");
4084        assert!(
4085            matches!(validation_err, SendError::Validation(ref message) if message.contains("different generated MobMachine trust owner")),
4086            "unexpected recovered owner validation rejection: {validation_err:?}"
4087        );
4088        CoreCommsRuntime::install_recovered_generated_mob_trust_owner(
4089            &runtime,
4090            Arc::clone(&live_owner),
4091        )
4092        .await
4093        .expect("same recovered owner is idempotent");
4094        let err = CoreCommsRuntime::install_recovered_generated_mob_trust_owner(
4095            &runtime,
4096            recovered_owner,
4097        )
4098        .await
4099        .expect_err("recovered owner must not replace a different live owner");
4100        assert!(
4101            matches!(err, SendError::Validation(ref message) if message.contains("different generated MobMachine trust owner")),
4102            "unexpected recovered owner rejection: {err:?}"
4103        );
4104    }
4105
4106    #[tokio::test]
4107    async fn generated_trust_mutation_updates_projection() {
4108        let runtime = CommsRuntime::inproc_only("trust-mutator-generated").expect("runtime");
4109        let local = local_descriptor_for_runtime(&runtime);
4110        let peer_key = Keypair::generate().public_key();
4111        let descriptor = trusted_descriptor("peer", peer_key, "inproc://peer");
4112        let peer_id = descriptor.peer_id.to_string();
4113
4114        let add_authority = test_projection_add_authority(&local, &descriptor, 7);
4115        add_authority.install_owner(&runtime);
4116        let add_authority_replay = add_authority.authority.clone();
4117        let add = CoreCommsRuntime::apply_trust_mutation(
4118            &runtime,
4119            CommsTrustMutation::AddTrustedPeer {
4120                peer: descriptor.clone(),
4121                authority: add_authority.authority,
4122            },
4123        )
4124        .await
4125        .expect("generated add should apply");
4126        assert_eq!(add, CommsTrustMutationResult::Added { created: true });
4127        assert_eq!(runtime.peers().await.len(), 1);
4128        let replay = CoreCommsRuntime::apply_trust_mutation(
4129            &runtime,
4130            CommsTrustMutation::AddTrustedPeer {
4131                peer: descriptor.clone(),
4132                authority: add_authority_replay,
4133            },
4134        )
4135        .await
4136        .expect_err("cloned generated add authority must be one-use");
4137        assert!(
4138            matches!(replay, SendError::Validation(ref message) if message.contains("already consumed")),
4139            "unexpected replay rejection: {replay:?}"
4140        );
4141        let wrong_peer_key = Keypair::generate().public_key();
4142        let wrong_peer = trusted_descriptor(
4143            "wrong-operation-peer",
4144            wrong_peer_key,
4145            "inproc://wrong-operation-peer",
4146        );
4147        let wrong_operation_authority =
4148            test_projection_add_authority_with_dsl(Arc::clone(&add_authority.dsl), &wrong_peer);
4149        let wrong_operation = CoreCommsRuntime::apply_trust_mutation(
4150            &runtime,
4151            CommsTrustMutation::RemoveTrustedPeer {
4152                peer_id: peer_id.clone(),
4153                authority: wrong_operation_authority.authority,
4154            },
4155        )
4156        .await
4157        .expect_err("generated add authority must not remove trust");
4158        assert!(
4159            matches!(wrong_operation, SendError::Validation(ref message) if message.contains("PublicAdd") && message.contains("remove a public trusted peer")),
4160            "unexpected wrong-operation rejection: {wrong_operation:?}"
4161        );
4162        assert_eq!(
4163            runtime.peers().await.len(),
4164            1,
4165            "wrong-operation authority must not mutate trust projection",
4166        );
4167
4168        let remove_authority =
4169            test_projection_remove_authority_with_dsl(Arc::clone(&add_authority.dsl), &descriptor);
4170        let remove = CoreCommsRuntime::apply_trust_mutation(
4171            &runtime,
4172            CommsTrustMutation::RemoveTrustedPeer {
4173                peer_id: peer_id.clone(),
4174                authority: remove_authority.authority,
4175            },
4176        )
4177        .await
4178        .expect("generated remove should apply");
4179        assert_eq!(remove, CommsTrustMutationResult::Removed { removed: true });
4180        assert!(runtime.peers().await.is_empty());
4181    }
4182
4183    #[tokio::test]
4184    async fn generated_trust_add_reports_existing_source_without_rewriting_descriptor() {
4185        let runtime = CommsRuntime::inproc_only("trust-mutator-existing-source").expect("runtime");
4186        let peer_key = Keypair::generate().public_key();
4187        let descriptor = trusted_descriptor("peer", peer_key, "inproc://peer");
4188        let peer = descriptor_to_trust_entry(descriptor.clone()).expect("valid descriptor");
4189
4190        runtime
4191            .router
4192            .add_trusted_peer_for_source(
4193                peer,
4194                GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
4195                false,
4196            )
4197            .expect("test setup installs MeerkatMachine-owned trust");
4198
4199        let same_peer = descriptor_to_trust_entry(descriptor.clone()).expect("valid descriptor");
4200        let same = runtime
4201            .router
4202            .add_trusted_peer_for_source(
4203                same_peer,
4204                GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
4205                false,
4206            )
4207            .expect("same generated source row should be idempotent");
4208        assert!(!same);
4209
4210        let conflicting_descriptor =
4211            trusted_descriptor("peer-renamed", peer_key, "inproc://peer-renamed");
4212        let conflicting_peer =
4213            descriptor_to_trust_entry(conflicting_descriptor).expect("valid descriptor");
4214        let conflict = runtime
4215            .router
4216            .add_trusted_peer_for_source(
4217                conflicting_peer,
4218                GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
4219                false,
4220            )
4221            .expect_err("trust repair must not rewrite an existing generated source row");
4222        assert!(
4223            matches!(
4224                conflict,
4225                crate::trust::TrustError::ConflictingGeneratedTrustSource { .. }
4226            ),
4227            "unexpected conflicting-source rejection: {conflict:?}"
4228        );
4229
4230        let public_snapshot = CoreCommsRuntime::public_trusted_peer_projection_snapshot(&runtime)
4231            .await
4232            .expect("public trust snapshot");
4233        assert_eq!(public_snapshot.len(), 1);
4234        assert_eq!(public_snapshot[0].name.as_str(), "peer");
4235        assert_eq!(public_snapshot[0].address.to_string(), "inproc://peer");
4236    }
4237
4238    #[tokio::test]
4239    async fn generated_remove_does_not_remove_trust_owned_by_other_source() {
4240        let runtime =
4241            CommsRuntime::inproc_only("trust-mutator-source-scoped-remove").expect("runtime");
4242        let local = local_descriptor_for_runtime(&runtime);
4243        let peer_key = Keypair::generate().public_key();
4244        let descriptor = trusted_descriptor("mob-owned-peer", peer_key, "inproc://mob-owned-peer");
4245        let peer_id = descriptor.peer_id.to_string();
4246        let peer = descriptor_to_trust_entry(descriptor.clone()).expect("valid descriptor");
4247
4248        runtime
4249            .router
4250            .add_trusted_peer_for_source(
4251                peer,
4252                GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring,
4253                false,
4254            )
4255            .expect("test setup installs mob-owned public trust");
4256
4257        let meerkat_projection = CoreCommsRuntime::trusted_peer_projection_snapshot_for_source(
4258            &runtime,
4259            GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
4260        )
4261        .await
4262        .expect("source-scoped snapshot");
4263        assert!(
4264            meerkat_projection.is_empty(),
4265            "MeerkatMachine peer projection must not see MobMachine-owned trust as its canonical rows",
4266        );
4267
4268        let remove_authority = test_projection_remove_authority(&local, &descriptor, 8);
4269        remove_authority.install_owner(&runtime);
4270        let remove = CoreCommsRuntime::apply_trust_mutation(
4271            &runtime,
4272            CommsTrustMutation::RemoveTrustedPeer {
4273                peer_id,
4274                authority: remove_authority.authority,
4275            },
4276        )
4277        .await
4278        .expect("generated remove for an unowned source should apply as no-op");
4279        assert_eq!(remove, CommsTrustMutationResult::Removed { removed: false });
4280
4281        let public_snapshot = CoreCommsRuntime::public_trusted_peer_projection_snapshot(&runtime)
4282            .await
4283            .expect("public trust snapshot");
4284        assert_eq!(
4285            public_snapshot.len(),
4286            1,
4287            "MobMachine-owned trust must remain after a MeerkatMachine removal no-op",
4288        );
4289        assert_eq!(public_snapshot[0].peer_id, descriptor.peer_id);
4290    }
4291
4292    #[tokio::test]
4293    async fn source_scoped_snapshot_uses_source_owned_descriptor() {
4294        let runtime =
4295            CommsRuntime::inproc_only("trust-mutator-source-scoped-descriptor").expect("runtime");
4296        let peer_key = Keypair::generate().public_key();
4297        let projection_descriptor =
4298            trusted_descriptor("projection-owned", peer_key, "inproc://projection-owned");
4299        let mob_descriptor = trusted_descriptor("mob-owned", peer_key, "inproc://mob-owned");
4300        let peer_id = projection_descriptor.peer_id;
4301        let projection_peer =
4302            descriptor_to_trust_entry(projection_descriptor.clone()).expect("valid descriptor");
4303        let mob_peer = descriptor_to_trust_entry(mob_descriptor.clone()).expect("valid descriptor");
4304
4305        runtime
4306            .router
4307            .add_trusted_peer_for_source(
4308                projection_peer,
4309                GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
4310                false,
4311            )
4312            .expect("test setup installs MeerkatMachine-owned trust");
4313        runtime
4314            .router
4315            .add_trusted_peer_for_source(
4316                mob_peer,
4317                GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring,
4318                false,
4319            )
4320            .expect("test setup installs MobMachine-owned trust");
4321
4322        let projection_snapshot = CoreCommsRuntime::trusted_peer_projection_snapshot_for_source(
4323            &runtime,
4324            GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
4325        )
4326        .await
4327        .expect("projection source snapshot");
4328        assert_eq!(projection_snapshot, vec![projection_descriptor.clone()]);
4329        let mob_snapshot = CoreCommsRuntime::trusted_peer_projection_snapshot_for_source(
4330            &runtime,
4331            GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring,
4332        )
4333        .await
4334        .expect("mob source snapshot");
4335        assert_eq!(mob_snapshot, vec![mob_descriptor.clone()]);
4336
4337        let remove = runtime.router.remove_trusted_peer_for_source(
4338            &peer_id,
4339            GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring,
4340        );
4341        assert!(remove, "test setup removal should remove MobMachine source");
4342        let public_snapshot = CoreCommsRuntime::public_trusted_peer_projection_snapshot(&runtime)
4343            .await
4344            .expect("public trust snapshot");
4345        assert_eq!(
4346            public_snapshot,
4347            vec![projection_descriptor],
4348            "union projection should fall back to the remaining source-owned descriptor",
4349        );
4350    }
4351
4352    #[tokio::test]
4353    async fn generated_add_authority_rejects_descriptor_drift() {
4354        let runtime = CommsRuntime::inproc_only("trust-mutator-descriptor-drift").expect("runtime");
4355        let local = local_descriptor_for_runtime(&runtime);
4356        let peer_key = Keypair::generate().public_key();
4357        let descriptor = trusted_descriptor("peer", peer_key, "inproc://peer");
4358        let drifted_descriptor =
4359            trusted_descriptor("peer", peer_key, "inproc://peer-drifted-address");
4360
4361        let add_authority = test_projection_add_authority(&local, &descriptor, 11);
4362        add_authority.install_owner(&runtime);
4363        let err = CoreCommsRuntime::apply_trust_mutation(
4364            &runtime,
4365            CommsTrustMutation::AddTrustedPeer {
4366                peer: drifted_descriptor,
4367                authority: add_authority.authority,
4368            },
4369        )
4370        .await
4371        .expect_err("generated add authority must bind the full peer descriptor");
4372        assert!(
4373            matches!(err, SendError::Validation(ref message) if message.contains("descriptor")),
4374            "unexpected descriptor drift rejection: {err:?}"
4375        );
4376        assert!(
4377            runtime.peers().await.is_empty(),
4378            "descriptor drift must not mutate public trust"
4379        );
4380    }
4381
4382    #[tokio::test]
4383    async fn public_trust_projection_excludes_and_cannot_remove_private_peer() {
4384        let runtime = CommsRuntime::inproc_only("trust-mutator-private-split").expect("runtime");
4385        let local = local_descriptor_for_runtime(&runtime);
4386        let peer_key = Keypair::generate().public_key();
4387        let descriptor = trusted_descriptor("private-peer", peer_key, "inproc://private-peer");
4388        let peer_id = descriptor.peer_id.to_string();
4389
4390        runtime
4391            .apply_private_trusted_peer_descriptor(
4392                descriptor.clone(),
4393                GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorPublish,
4394            )
4395            .await
4396            .expect("test setup installs private trust");
4397        let public_snapshot = CoreCommsRuntime::public_trusted_peer_projection_snapshot(&runtime)
4398            .await
4399            .expect("public trust snapshot");
4400        assert!(
4401            public_snapshot.is_empty(),
4402            "private trust must not be exported as public reconciliation truth"
4403        );
4404
4405        let remove_authority = test_projection_remove_authority(&local, &descriptor, 12);
4406        remove_authority.install_owner(&runtime);
4407        let err = CoreCommsRuntime::apply_trust_mutation(
4408            &runtime,
4409            CommsTrustMutation::RemoveTrustedPeer {
4410                peer_id,
4411                authority: remove_authority.authority,
4412            },
4413        )
4414        .await
4415        .expect_err("public remove authority must not remove private trust");
4416        assert!(
4417            matches!(err, SendError::Validation(ref message) if message.contains("public trust authority cannot remove private trusted peer")),
4418            "unexpected private removal rejection: {err:?}"
4419        );
4420        let runtime_snapshot = CoreCommsRuntime::peer_ingress_runtime_snapshot(&runtime)
4421            .await
4422            .expect("runtime snapshot");
4423        assert_eq!(
4424            runtime_snapshot.trusted_peers.len(),
4425            1,
4426            "private trust should remain installed for admission"
4427        );
4428    }
4429
4430    fn install_test_peer_comms_handle(runtime: &CommsRuntime) {
4431        runtime.install_peer_comms_handle(test_support::runtime_peer_comms_handle());
4432    }
4433
4434    fn inproc_only_with_test_peer_authority(name: &str) -> CommsRuntime {
4435        let runtime = CommsRuntime::inproc_only(name).unwrap();
4436        install_test_peer_comms_handle(&runtime);
4437        runtime
4438    }
4439
4440    async fn new_with_test_peer_authority(config: ResolvedCommsConfig) -> CommsRuntime {
4441        let runtime = CommsRuntime::new(config).await.unwrap();
4442        install_test_peer_comms_handle(&runtime);
4443        runtime
4444    }
4445
4446    fn peer_route(name: &str, pubkey: PubKey) -> PeerRoute {
4447        PeerRoute::with_display_name(
4448            crate::router::peer_id_from_pubkey(&pubkey),
4449            PeerName::new(name.to_string()).expect("valid peer name"),
4450        )
4451    }
4452
4453    fn missing_peer_route(name: &str) -> PeerRoute {
4454        PeerRoute::with_display_name(
4455            meerkat_core::comms::PeerId::new(),
4456            PeerName::new(name.to_string()).expect("valid peer name"),
4457        )
4458    }
4459    use parking_lot::Mutex;
4460    use std::{collections::HashMap, sync::Arc};
4461    use tokio::time::{Duration, timeout};
4462    use uuid::Uuid;
4463
4464    #[derive(Default)]
4465    struct TestBlobStore {
4466        blobs: Mutex<HashMap<BlobId, BlobPayload>>,
4467    }
4468
4469    #[async_trait]
4470    impl BlobStore for TestBlobStore {
4471        async fn put_image(&self, media_type: &str, data: &str) -> Result<BlobRef, BlobStoreError> {
4472            let blob_id = BlobId::from(format!("sha256:test-{media_type}-{data}"));
4473            let payload = BlobPayload {
4474                blob_id: blob_id.clone(),
4475                media_type: media_type.to_string(),
4476                data: data.to_string(),
4477            };
4478            self.blobs.lock().insert(blob_id.clone(), payload);
4479            Ok(BlobRef {
4480                blob_id,
4481                media_type: media_type.to_string(),
4482            })
4483        }
4484
4485        async fn get(&self, blob_id: &BlobId) -> Result<BlobPayload, BlobStoreError> {
4486            self.blobs
4487                .lock()
4488                .get(blob_id)
4489                .cloned()
4490                .ok_or_else(|| BlobStoreError::NotFound(blob_id.clone()))
4491        }
4492
4493        async fn delete(&self, blob_id: &BlobId) -> Result<(), BlobStoreError> {
4494            self.blobs.lock().remove(blob_id);
4495            Ok(())
4496        }
4497
4498        fn is_persistent(&self) -> bool {
4499            false
4500        }
4501    }
4502
4503    #[derive(Default)]
4504    struct TestPeerInteractionHandle {
4505        outbound:
4506            Mutex<HashMap<meerkat_core::PeerCorrelationId, meerkat_core::OutboundPeerRequestState>>,
4507        inbound:
4508            Mutex<HashMap<meerkat_core::PeerCorrelationId, meerkat_core::InboundPeerRequestState>>,
4509        inbound_modes:
4510            Mutex<HashMap<meerkat_core::PeerCorrelationId, meerkat_core::types::HandlingMode>>,
4511        cleanup_observer:
4512            Mutex<Option<Arc<dyn meerkat_core::handles::PeerInteractionCleanupObserver>>>,
4513    }
4514
4515    impl TestPeerInteractionHandle {
4516        fn notify_cleanup(&self, corr_id: meerkat_core::PeerCorrelationId) {
4517            if let Some(observer) = self.cleanup_observer.lock().clone() {
4518                observer.on_peer_interaction_cleanup(corr_id);
4519            }
4520        }
4521
4522        fn guard_rejected(
4523            context: &'static str,
4524            corr_id: meerkat_core::PeerCorrelationId,
4525        ) -> meerkat_core::handles::DslTransitionError {
4526            meerkat_core::handles::DslTransitionError::guard_rejected(
4527                context,
4528                format!("test authority rejected corr_id {corr_id}"),
4529            )
4530        }
4531    }
4532
4533    impl meerkat_core::handles::PeerInteractionHandle for TestPeerInteractionHandle {
4534        fn request_sent(
4535            &self,
4536            corr_id: meerkat_core::PeerCorrelationId,
4537        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4538            let mut outbound = self.outbound.lock();
4539            if outbound.contains_key(&corr_id) {
4540                return Err(Self::guard_rejected(
4541                    "PeerInteractionHandle::request_sent",
4542                    corr_id,
4543                ));
4544            }
4545            outbound.insert(corr_id, meerkat_core::OutboundPeerRequestState::Sent);
4546            Ok(())
4547        }
4548
4549        fn response_progress(
4550            &self,
4551            corr_id: meerkat_core::PeerCorrelationId,
4552        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4553            let mut outbound = self.outbound.lock();
4554            if !outbound.contains_key(&corr_id) {
4555                return Err(Self::guard_rejected(
4556                    "PeerInteractionHandle::response_progress",
4557                    corr_id,
4558                ));
4559            }
4560            outbound.insert(
4561                corr_id,
4562                meerkat_core::OutboundPeerRequestState::AcceptedProgress,
4563            );
4564            Ok(())
4565        }
4566
4567        fn response_terminal(
4568            &self,
4569            corr_id: meerkat_core::PeerCorrelationId,
4570            _disposition: meerkat_core::handles::PeerTerminalDisposition,
4571        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4572            if self.outbound.lock().remove(&corr_id).is_none() {
4573                return Err(Self::guard_rejected(
4574                    "PeerInteractionHandle::response_terminal",
4575                    corr_id,
4576                ));
4577            }
4578            self.notify_cleanup(corr_id);
4579            Ok(())
4580        }
4581
4582        fn response_rejected(
4583            &self,
4584            corr_id: meerkat_core::PeerCorrelationId,
4585        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4586            if self.outbound.lock().remove(&corr_id).is_none() {
4587                return Err(Self::guard_rejected(
4588                    "PeerInteractionHandle::response_rejected",
4589                    corr_id,
4590                ));
4591            }
4592            self.notify_cleanup(corr_id);
4593            Ok(())
4594        }
4595
4596        fn request_timed_out(
4597            &self,
4598            corr_id: meerkat_core::PeerCorrelationId,
4599        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4600            if self.outbound.lock().remove(&corr_id).is_none() {
4601                return Err(Self::guard_rejected(
4602                    "PeerInteractionHandle::request_timed_out",
4603                    corr_id,
4604                ));
4605            }
4606            self.notify_cleanup(corr_id);
4607            Ok(())
4608        }
4609
4610        fn request_send_failed(
4611            &self,
4612            corr_id: meerkat_core::PeerCorrelationId,
4613        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4614            if self.outbound.lock().remove(&corr_id).is_none() {
4615                return Err(Self::guard_rejected(
4616                    "PeerInteractionHandle::request_send_failed",
4617                    corr_id,
4618                ));
4619            }
4620            self.notify_cleanup(corr_id);
4621            Ok(())
4622        }
4623
4624        fn request_received(
4625            &self,
4626            corr_id: meerkat_core::PeerCorrelationId,
4627            handling_mode: meerkat_core::types::HandlingMode,
4628        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4629            let mut inbound = self.inbound.lock();
4630            if inbound.contains_key(&corr_id) {
4631                return Err(Self::guard_rejected(
4632                    "PeerInteractionHandle::request_received",
4633                    corr_id,
4634                ));
4635            }
4636            inbound.insert(corr_id, meerkat_core::InboundPeerRequestState::Received);
4637            self.inbound_modes.lock().insert(corr_id, handling_mode);
4638            Ok(())
4639        }
4640
4641        fn classify_response_reply(
4642            &self,
4643            status: meerkat_core::ResponseStatus,
4644        ) -> Result<meerkat_core::TerminalityClass, meerkat_core::handles::DslTransitionError>
4645        {
4646            let generated = meerkat_runtime::handles::RuntimePeerInteractionHandle::ephemeral();
4647            meerkat_core::handles::PeerInteractionHandle::classify_response_reply(
4648                &generated, status,
4649            )
4650        }
4651
4652        fn response_replied(
4653            &self,
4654            corr_id: meerkat_core::PeerCorrelationId,
4655        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4656            if self.inbound.lock().remove(&corr_id).is_none() {
4657                return Err(Self::guard_rejected(
4658                    "PeerInteractionHandle::response_replied",
4659                    corr_id,
4660                ));
4661            }
4662            self.inbound_modes.lock().remove(&corr_id);
4663            Ok(())
4664        }
4665
4666        fn outbound_state(
4667            &self,
4668            corr_id: meerkat_core::PeerCorrelationId,
4669        ) -> Option<meerkat_core::OutboundPeerRequestState> {
4670            self.outbound.lock().get(&corr_id).copied()
4671        }
4672
4673        fn inbound_state(
4674            &self,
4675            corr_id: meerkat_core::PeerCorrelationId,
4676        ) -> Option<meerkat_core::InboundPeerRequestState> {
4677            self.inbound.lock().get(&corr_id).copied()
4678        }
4679
4680        fn inbound_handling_mode(
4681            &self,
4682            corr_id: meerkat_core::PeerCorrelationId,
4683        ) -> Option<meerkat_core::types::HandlingMode> {
4684            self.inbound_modes.lock().get(&corr_id).copied()
4685        }
4686
4687        fn install_cleanup_observer(
4688            &self,
4689            observer: Arc<dyn meerkat_core::handles::PeerInteractionCleanupObserver>,
4690        ) {
4691            *self.cleanup_observer.lock() = Some(observer);
4692        }
4693    }
4694
4695    #[derive(Default)]
4696    struct TestInteractionStreamHandle {
4697        states:
4698            Mutex<HashMap<meerkat_core::PeerCorrelationId, meerkat_core::InteractionStreamState>>,
4699        cleanup_observer:
4700            Mutex<Option<Arc<dyn meerkat_core::handles::InteractionStreamCleanupObserver>>>,
4701    }
4702
4703    impl TestInteractionStreamHandle {
4704        fn notify_cleanup(&self, corr_id: meerkat_core::PeerCorrelationId) {
4705            if let Some(observer) = self.cleanup_observer.lock().clone() {
4706                observer.on_interaction_stream_cleanup(corr_id);
4707            }
4708        }
4709
4710        fn guard_rejected(
4711            context: &'static str,
4712            corr_id: meerkat_core::PeerCorrelationId,
4713        ) -> meerkat_core::handles::DslTransitionError {
4714            meerkat_core::handles::DslTransitionError::guard_rejected(
4715                context,
4716                format!("test stream authority rejected corr_id {corr_id}"),
4717            )
4718        }
4719
4720        fn terminal(
4721            &self,
4722            corr_id: meerkat_core::PeerCorrelationId,
4723            context: &'static str,
4724        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4725            if self.states.lock().remove(&corr_id).is_none() {
4726                return Err(Self::guard_rejected(context, corr_id));
4727            }
4728            self.notify_cleanup(corr_id);
4729            Ok(())
4730        }
4731    }
4732
4733    impl meerkat_core::handles::InteractionStreamHandle for TestInteractionStreamHandle {
4734        fn reserved(
4735            &self,
4736            corr_id: meerkat_core::PeerCorrelationId,
4737        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4738            let mut states = self.states.lock();
4739            if states.contains_key(&corr_id) {
4740                return Err(Self::guard_rejected(
4741                    "InteractionStreamHandle::reserved",
4742                    corr_id,
4743                ));
4744            }
4745            states.insert(corr_id, meerkat_core::InteractionStreamState::Reserved);
4746            Ok(())
4747        }
4748
4749        fn attached(
4750            &self,
4751            corr_id: meerkat_core::PeerCorrelationId,
4752        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4753            let mut states = self.states.lock();
4754            match states.get_mut(&corr_id) {
4755                Some(state) if *state == meerkat_core::InteractionStreamState::Reserved => {
4756                    *state = meerkat_core::InteractionStreamState::Attached;
4757                    Ok(())
4758                }
4759                _ => Err(Self::guard_rejected(
4760                    "InteractionStreamHandle::attached",
4761                    corr_id,
4762                )),
4763            }
4764        }
4765
4766        fn completed(
4767            &self,
4768            corr_id: meerkat_core::PeerCorrelationId,
4769        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4770            self.terminal(corr_id, "InteractionStreamHandle::completed")
4771        }
4772
4773        fn expired(
4774            &self,
4775            corr_id: meerkat_core::PeerCorrelationId,
4776        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4777            self.terminal(corr_id, "InteractionStreamHandle::expired")
4778        }
4779
4780        fn closed_early(
4781            &self,
4782            corr_id: meerkat_core::PeerCorrelationId,
4783        ) -> Result<(), meerkat_core::handles::DslTransitionError> {
4784            self.terminal(corr_id, "InteractionStreamHandle::closed_early")
4785        }
4786
4787        fn state(
4788            &self,
4789            corr_id: meerkat_core::PeerCorrelationId,
4790        ) -> Option<meerkat_core::InteractionStreamState> {
4791            self.states.lock().get(&corr_id).copied()
4792        }
4793
4794        fn install_cleanup_observer(
4795            &self,
4796            observer: Arc<dyn meerkat_core::handles::InteractionStreamCleanupObserver>,
4797        ) {
4798            *self.cleanup_observer.lock() = Some(observer);
4799        }
4800    }
4801
4802    fn install_test_peer_request_response_authority(runtime: &Arc<CommsRuntime>) {
4803        runtime.install_peer_request_response_authority(PeerRequestResponseAuthority::new(
4804            Arc::new(TestPeerInteractionHandle::default()),
4805            Arc::new(TestInteractionStreamHandle::default()),
4806        ));
4807    }
4808
4809    fn test_runtime_config(name: &str, tmp: &tempfile::TempDir) -> ResolvedCommsConfig {
4810        ResolvedCommsConfig {
4811            enabled: true,
4812            name: name.to_string(),
4813            inproc_namespace: None,
4814            listen_uds: None,
4815            listen_tcp: None,
4816            advertise_address: None,
4817            event_listen_tcp: None,
4818            #[cfg(unix)]
4819            event_listen_uds: None,
4820            identity_dir: tmp.path().join("identity"),
4821            trusted_peers_path: tmp.path().join("trusted_peers.json"),
4822            comms_config: crate::CommsConfig::default(),
4823            auth: meerkat_core::CommsAuthMode::Open,
4824            require_peer_auth: true,
4825            allow_external_unauthenticated: false,
4826            pairing_password: None,
4827        }
4828    }
4829
4830    #[tokio::test]
4831    async fn pairing_password_installs_trusted_peer_and_returns_binding() {
4832        let tmp = tempfile::TempDir::new().unwrap();
4833        let mut config = test_runtime_config("pairing-target", &tmp);
4834        config.listen_tcp = Some("127.0.0.1:0".parse().unwrap());
4835        config.pairing_password = Some("0123456789abcdef0123456789abcdef".to_string());
4836        let mut runtime = CommsRuntime::new_machine_authority_required_with_silent_intents(
4837            config,
4838            Arc::new(HashSet::new()),
4839        )
4840        .await
4841        .unwrap();
4842        runtime.start_listeners().await.unwrap();
4843        let addr = runtime.config.listen_tcp.unwrap();
4844
4845        let caller_keypair = Keypair::generate();
4846        let caller_public_key = caller_keypair.public_key().to_pubkey_string();
4847        let caller_address = "tcp://127.0.0.1:65530";
4848        let stream = TcpStream::connect(addr).await.unwrap();
4849        let mut reader = BufReader::new(stream);
4850        reader
4851            .get_mut()
4852            .write_all(
4853                serde_json::json!({
4854                    "kind": "meerkat_pairing_hello",
4855                    "version": PAIRING_VERSION,
4856                })
4857                .to_string()
4858                .as_bytes(),
4859            )
4860            .await
4861            .unwrap();
4862        reader.get_mut().write_all(b"\n").await.unwrap();
4863
4864        let mut line = String::new();
4865        reader.read_line(&mut line).await.unwrap();
4866        let challenge: serde_json::Value = serde_json::from_str(&line).unwrap();
4867        assert_eq!(challenge["kind"], PAIRING_CHALLENGE_KIND);
4868        let challenge_token = challenge["challenge"].as_str().unwrap();
4869        let proof = comms_pairing_password_proof(
4870            "0123456789abcdef0123456789abcdef",
4871            challenge_token,
4872            &caller_public_key,
4873            caller_address,
4874        );
4875        line.clear();
4876        reader
4877            .get_mut()
4878            .write_all(
4879                serde_json::json!({
4880                    "kind": "meerkat_pairing_proof",
4881                    "version": PAIRING_VERSION,
4882                    "password_proof": proof,
4883                    "caller": {
4884                        "name": "hive",
4885                        "address": caller_address,
4886                        "identity": {
4887                            "kind": "ed25519_public_key",
4888                            "public_key": caller_public_key,
4889                        }
4890                    }
4891                })
4892                .to_string()
4893                .as_bytes(),
4894            )
4895            .await
4896            .unwrap();
4897        reader.get_mut().write_all(b"\n").await.unwrap();
4898
4899        reader.read_line(&mut line).await.unwrap();
4900        let complete: serde_json::Value = serde_json::from_str(&line).unwrap();
4901        assert_eq!(complete["kind"], PAIRING_COMPLETE_KIND);
4902        assert_eq!(complete["binding"]["kind"], "external");
4903        assert_eq!(
4904            complete["binding"]["identity"]["public_key"],
4905            runtime.public_key().to_pubkey_string()
4906        );
4907
4908        let persisted = TrustStore::load(&runtime.config.trusted_peers_path)
4909            .await
4910            .unwrap();
4911        assert!(persisted.entries().any(|entry| {
4912            entry.name.as_str() == "hive" && entry.pubkey == caller_keypair.public_key()
4913        }));
4914        let live_peers = CoreCommsRuntime::peers(&runtime).await;
4915        assert!(
4916            live_peers
4917                .iter()
4918                .any(|peer| peer.peer_id == caller_keypair.public_key().to_peer_id()),
4919            "pairing must update the live router peer-id index, not only trusted_peers.json"
4920        );
4921    }
4922
4923    #[tokio::test]
4924    async fn pairing_classification_tolerates_delayed_hello() {
4925        let tmp = tempfile::TempDir::new().unwrap();
4926        let mut config = test_runtime_config("pairing-delayed-target", &tmp);
4927        config.listen_tcp = Some("127.0.0.1:0".parse().unwrap());
4928        config.pairing_password = Some("0123456789abcdef0123456789abcdef".to_string());
4929        let mut runtime = CommsRuntime::new_machine_authority_required_with_silent_intents(
4930            config,
4931            Arc::new(HashSet::new()),
4932        )
4933        .await
4934        .unwrap();
4935        runtime.start_listeners().await.unwrap();
4936        let addr = runtime.config.listen_tcp.unwrap();
4937
4938        let stream = TcpStream::connect(addr).await.unwrap();
4939        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
4940        let mut reader = BufReader::new(stream);
4941        reader
4942            .get_mut()
4943            .write_all(
4944                serde_json::json!({
4945                    "kind": "meerkat_pairing_hello",
4946                    "version": PAIRING_VERSION,
4947                })
4948                .to_string()
4949                .as_bytes(),
4950            )
4951            .await
4952            .unwrap();
4953        reader.get_mut().write_all(b"\n").await.unwrap();
4954
4955        let mut line = String::new();
4956        reader.read_line(&mut line).await.unwrap();
4957        let challenge: serde_json::Value = serde_json::from_str(&line).unwrap();
4958        assert_eq!(challenge["kind"], PAIRING_CHALLENGE_KIND);
4959    }
4960
4961    #[tokio::test]
4962    async fn wildcard_signed_listener_requires_advertised_address() {
4963        let tmp = tempfile::TempDir::new().unwrap();
4964        let mut config = test_runtime_config("wildcard-target", &tmp);
4965        config.listen_tcp = Some("0.0.0.0:0".parse().unwrap());
4966        let mut runtime = CommsRuntime::new_machine_authority_required_with_silent_intents(
4967            config,
4968            Arc::new(HashSet::new()),
4969        )
4970        .await
4971        .unwrap();
4972
4973        let error = runtime
4974            .start_listeners()
4975            .await
4976            .expect_err("wildcard listener without advertise address should fail");
4977        assert!(
4978            error
4979                .to_string()
4980                .contains("requires an explicit advertised address"),
4981            "unexpected error: {error}"
4982        );
4983    }
4984
4985    #[tokio::test]
4986    async fn explicit_advertised_address_overrides_bound_listener_address() {
4987        let tmp = tempfile::TempDir::new().unwrap();
4988        let mut config = test_runtime_config("advertised-target", &tmp);
4989        config.listen_tcp = Some("127.0.0.1:0".parse().unwrap());
4990        config.advertise_address = Some("tcp://203.0.113.10:4200".to_string());
4991        let mut runtime = CommsRuntime::new_machine_authority_required_with_silent_intents(
4992            config,
4993            Arc::new(HashSet::new()),
4994        )
4995        .await
4996        .unwrap();
4997
4998        runtime.start_listeners().await.unwrap();
4999        assert_eq!(runtime.advertised_address(), "tcp://203.0.113.10:4200");
5000    }
5001
5002    #[tokio::test]
5003    async fn machine_required_tcp_runtime_rejects_ingress_before_handle_or_listener_start() {
5004        let tmp = tempfile::TempDir::new().unwrap();
5005        let suffix = Uuid::new_v4().simple().to_string();
5006        let sender_name = format!("pre-authority-sender-{suffix}");
5007        let receiver_name = format!("pre-authority-receiver-{suffix}");
5008        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
5009        let mut config = test_runtime_config(&receiver_name, &tmp);
5010        config.listen_tcp = Some("127.0.0.1:0".parse().unwrap());
5011        config.require_peer_auth = false;
5012        let receiver = CommsRuntime::new_machine_authority_required_with_silent_intents(
5013            config,
5014            Arc::new(HashSet::new()),
5015        )
5016        .await
5017        .unwrap();
5018
5019        assert!(receiver.peer_comms_machine_authority_required());
5020        assert!(receiver.peer_comms_handle().is_none());
5021
5022        add_trusted_peer_with_generated_authority(
5023            &sender,
5024            trusted_descriptor(
5025                &receiver_name,
5026                receiver.public_key(),
5027                &format!("inproc://{receiver_name}"),
5028            ),
5029        )
5030        .await;
5031
5032        let result = CoreCommsRuntime::send(
5033            &sender,
5034            CommsCommand::PeerMessage {
5035                content_taint: None,
5036                blocks: None,
5037                to: peer_route(&receiver_name, receiver.public_key()),
5038                body: "must not pass local classifier".to_string(),
5039                handling_mode: meerkat_core::types::HandlingMode::Queue,
5040            },
5041        )
5042        .await;
5043
5044        assert!(matches!(
5045            result,
5046            Err(SendError::AdmissionDropped {
5047                reason: meerkat_core::comms::AdmissionDropReason::ClassificationRejected
5048            })
5049        ));
5050        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
5051        assert!(
5052            interactions.is_empty(),
5053            "runtime-required ingress without a machine handle must not reach the inbox"
5054        );
5055    }
5056
5057    fn signed_envelope(from: &Keypair, to: PubKey, kind: MessageKind) -> Envelope {
5058        let mut envelope = Envelope {
5059            id: Uuid::new_v4(),
5060            from: from.public_key(),
5061            to,
5062            kind,
5063            sig: Signature::new([0u8; 64]),
5064        };
5065        envelope.sign(from);
5066        envelope
5067    }
5068
5069    #[test]
5070    fn bridge_bootstrap_token_is_stable_for_the_same_keypair() {
5071        let keypair = Keypair::generate();
5072        let first = CommsRuntime::derive_bridge_bootstrap_token(&keypair);
5073        let second = CommsRuntime::derive_bridge_bootstrap_token(&keypair);
5074        assert_eq!(
5075            first, second,
5076            "same keypair should derive same bootstrap token"
5077        );
5078    }
5079
5080    #[test]
5081    fn bridge_bootstrap_token_changes_with_a_different_keypair() {
5082        let first = CommsRuntime::derive_bridge_bootstrap_token(&Keypair::generate());
5083        let second = CommsRuntime::derive_bridge_bootstrap_token(&Keypair::generate());
5084        assert_ne!(
5085            first, second,
5086            "different keypairs should not derive the same bootstrap token"
5087        );
5088    }
5089
5090    /// Regression: auth=Open must always load keypair and trusted peers from disk
5091    /// so that outbound routing still works.
5092    #[tokio::test]
5093    async fn test_auth_open_loads_keypair_and_peers() {
5094        let tmp = tempfile::TempDir::new().unwrap();
5095
5096        let config = ResolvedCommsConfig {
5097            enabled: true,
5098            name: "test-agent".to_string(),
5099            inproc_namespace: None,
5100            listen_uds: None,
5101            listen_tcp: None,
5102            advertise_address: None,
5103            event_listen_tcp: None,
5104            #[cfg(unix)]
5105            event_listen_uds: None,
5106            identity_dir: tmp.path().join("identity"),
5107            trusted_peers_path: tmp.path().join("trusted_peers.json"),
5108            comms_config: crate::CommsConfig::default(),
5109            auth: meerkat_core::CommsAuthMode::Open,
5110            require_peer_auth: true,
5111            allow_external_unauthenticated: false,
5112            pairing_password: None,
5113        };
5114
5115        let runtime = CommsRuntime::new(config).await.unwrap();
5116        assert_ne!(runtime.public_key().as_bytes(), &[0u8; 32]);
5117    }
5118
5119    /// Regression: signed listener must ALWAYS start, regardless of auth mode.
5120    #[tokio::test]
5121    #[ignore] // integration-real: binds TCP port
5122    async fn test_signed_listener_starts_in_open_mode() {
5123        let tmp = tempfile::TempDir::new().unwrap();
5124
5125        let config = ResolvedCommsConfig {
5126            enabled: true,
5127            name: "test-signed".to_string(),
5128            inproc_namespace: None,
5129            listen_uds: None,
5130            listen_tcp: Some("127.0.0.1:0".parse().unwrap()),
5131            advertise_address: None,
5132            event_listen_tcp: None,
5133            #[cfg(unix)]
5134            event_listen_uds: None,
5135            identity_dir: tmp.path().join("identity"),
5136            trusted_peers_path: tmp.path().join("trusted_peers.json"),
5137            comms_config: crate::CommsConfig::default(),
5138            auth: meerkat_core::CommsAuthMode::Open,
5139            require_peer_auth: true,
5140            allow_external_unauthenticated: false,
5141            pairing_password: None,
5142        };
5143
5144        let mut runtime = CommsRuntime::new(config).await.unwrap();
5145        runtime.start_listeners().await.unwrap();
5146        assert!(runtime.listeners_started);
5147        runtime.shutdown();
5148    }
5149
5150    /// Regression: non-loopback plain event listener must be rejected.
5151    #[tokio::test]
5152    async fn test_plain_listener_rejects_non_loopback() {
5153        let tmp = tempfile::TempDir::new().unwrap();
5154
5155        let config = ResolvedCommsConfig {
5156            enabled: true,
5157            name: "test-reject".to_string(),
5158            inproc_namespace: None,
5159            listen_uds: None,
5160            listen_tcp: None,
5161            advertise_address: None,
5162            event_listen_tcp: Some("0.0.0.0:4201".parse().unwrap()),
5163            #[cfg(unix)]
5164            event_listen_uds: None,
5165            identity_dir: tmp.path().join("identity"),
5166            trusted_peers_path: tmp.path().join("trusted_peers.json"),
5167            comms_config: crate::CommsConfig::default(),
5168            auth: meerkat_core::CommsAuthMode::Open,
5169            require_peer_auth: true,
5170            allow_external_unauthenticated: false,
5171            pairing_password: None,
5172        };
5173
5174        let mut runtime = CommsRuntime::new(config).await.unwrap();
5175        let result = runtime.start_listeners().await;
5176        assert!(result.is_err());
5177        assert!(result.unwrap_err().to_string().contains("prompt injection"));
5178    }
5179
5180    #[tokio::test]
5181    async fn test_drain_inbox_interactions_converts_all_authenticated_content_variants() {
5182        let tmp = tempfile::TempDir::new().unwrap();
5183        let config = test_runtime_config("variants", &tmp);
5184        let runtime = CommsRuntime::new(config).await.unwrap();
5185        install_test_peer_comms_handle(&runtime);
5186
5187        let sender = Keypair::generate();
5188        add_trusted_peer_with_generated_authority(
5189            &runtime,
5190            trusted_descriptor("sender", sender.public_key(), "tcp://127.0.0.1:4200"),
5191        )
5192        .await;
5193
5194        let request_id = Uuid::new_v4();
5195        let reply_to = Uuid::new_v4();
5196
5197        let msg = signed_envelope(
5198            &sender,
5199            runtime.public_key(),
5200            MessageKind::Message {
5201                content_taint: None,
5202                blocks: None,
5203                body: "hello".to_string(),
5204                handling_mode: None,
5205            },
5206        );
5207        let req = signed_envelope(
5208            &sender,
5209            runtime.public_key(),
5210            MessageKind::Request {
5211                intent: "review".to_string(),
5212                params: serde_json::json!({"pr": 19}),
5213                blocks: None,
5214                content_taint: None,
5215                handling_mode: None,
5216            },
5217        );
5218        let mut req = req;
5219        req.id = request_id;
5220        req.sign(&sender);
5221        let resp = signed_envelope(
5222            &sender,
5223            runtime.public_key(),
5224            MessageKind::Response {
5225                in_reply_to: reply_to,
5226                status: Status::Completed,
5227                result: serde_json::json!({"ok": true}),
5228                blocks: None,
5229                content_taint: None,
5230                handling_mode: None,
5231            },
5232        );
5233
5234        runtime
5235            .router
5236            .inbox_sender()
5237            .send_classified(InboxItem::External { envelope: msg })
5238            .into_result()
5239            .unwrap();
5240        runtime
5241            .router
5242            .inbox_sender()
5243            .send_classified(InboxItem::External { envelope: req })
5244            .into_result()
5245            .unwrap();
5246        runtime
5247            .router
5248            .inbox_sender()
5249            .send_classified(InboxItem::External { envelope: resp })
5250            .into_result()
5251            .unwrap();
5252
5253        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
5254        assert_eq!(interactions.len(), 3);
5255
5256        assert!(interactions.iter().any(|i| {
5257            matches!(
5258                &i.content,
5259                meerkat_core::InteractionContent::Message { body, .. } if body == "hello"
5260            )
5261        }));
5262        assert!(interactions.iter().any(|i| {
5263            matches!(
5264                &i.content,
5265                meerkat_core::InteractionContent::Request { intent, params, .. }
5266                    if intent == "review" && params["pr"] == 19
5267            )
5268        }));
5269        assert!(interactions.iter().any(|i| {
5270            matches!(
5271                &i.content,
5272                meerkat_core::InteractionContent::Response {
5273                    in_reply_to,
5274                    status,
5275                    result,
5276                    ..
5277                }
5278                    if in_reply_to.0 == reply_to
5279                        && *status == meerkat_core::ResponseStatus::Completed
5280                        && result["ok"] == true
5281            )
5282        }));
5283    }
5284
5285    /// Ask 5 gate: the signed envelope's `content_taint` rides the classified
5286    /// drain into `InboxInteraction.sender_taint` as content-adjacent payload.
5287    /// `None` (no declaration) and `Some(Clean)` stay distinct typed states —
5288    /// the drain never coalesces an absent declaration into `Clean`.
5289    #[tokio::test]
5290    async fn drain_inbox_interactions_carries_sender_content_taint() {
5291        use meerkat_core::comms::SenderContentTaint;
5292
5293        let tmp = tempfile::TempDir::new().unwrap();
5294        let config = test_runtime_config("taint-drain", &tmp);
5295        let runtime = CommsRuntime::new(config).await.unwrap();
5296        install_test_peer_comms_handle(&runtime);
5297
5298        let sender = Keypair::generate();
5299        add_trusted_peer_with_generated_authority(
5300            &runtime,
5301            trusted_descriptor("sender", sender.public_key(), "tcp://127.0.0.1:4200"),
5302        )
5303        .await;
5304
5305        let tainted = signed_envelope(
5306            &sender,
5307            runtime.public_key(),
5308            MessageKind::Message {
5309                body: "tainted body".to_string(),
5310                blocks: None,
5311                content_taint: Some(SenderContentTaint::Tainted),
5312                handling_mode: None,
5313            },
5314        );
5315        let clean = signed_envelope(
5316            &sender,
5317            runtime.public_key(),
5318            MessageKind::Request {
5319                intent: "review".to_string(),
5320                params: serde_json::json!({"pr": 7}),
5321                blocks: None,
5322                content_taint: Some(SenderContentTaint::Clean),
5323                handling_mode: None,
5324            },
5325        );
5326        let undeclared = signed_envelope(
5327            &sender,
5328            runtime.public_key(),
5329            MessageKind::Message {
5330                body: "undeclared body".to_string(),
5331                blocks: None,
5332                content_taint: None,
5333                handling_mode: None,
5334            },
5335        );
5336
5337        for envelope in [tainted, clean, undeclared] {
5338            runtime
5339                .router
5340                .inbox_sender()
5341                .send_classified(InboxItem::External { envelope })
5342                .into_result()
5343                .unwrap();
5344        }
5345
5346        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
5347        assert_eq!(interactions.len(), 3);
5348
5349        let taint_for = |predicate: &dyn Fn(&meerkat_core::InboxInteraction) -> bool| {
5350            interactions
5351                .iter()
5352                .find(|interaction| predicate(interaction))
5353                .expect("expected drained interaction")
5354                .sender_taint
5355        };
5356        assert_eq!(
5357            taint_for(&|i| matches!(
5358                &i.content,
5359                meerkat_core::InteractionContent::Message { body, .. } if body == "tainted body"
5360            )),
5361            Some(SenderContentTaint::Tainted)
5362        );
5363        assert_eq!(
5364            taint_for(&|i| matches!(
5365                &i.content,
5366                meerkat_core::InteractionContent::Request { intent, .. } if intent == "review"
5367            )),
5368            Some(SenderContentTaint::Clean)
5369        );
5370        assert_eq!(
5371            taint_for(&|i| matches!(
5372                &i.content,
5373                meerkat_core::InteractionContent::Message { body, .. } if body == "undeclared body"
5374            )),
5375            None,
5376            "no declaration must stay None, never coalesced into Clean"
5377        );
5378    }
5379
5380    #[tokio::test]
5381    async fn lifecycle_envelopes_remain_non_responseable_request_projections() {
5382        let tmp = tempfile::TempDir::new().unwrap();
5383        let config = test_runtime_config("lifecycle-no-response-cache", &tmp);
5384        let runtime = CommsRuntime::new(config).await.unwrap();
5385        install_test_peer_comms_handle(&runtime);
5386
5387        let sender = Keypair::generate();
5388        add_trusted_peer_with_generated_authority(
5389            &runtime,
5390            trusted_descriptor("sender", sender.public_key(), "tcp://127.0.0.1:4200"),
5391        )
5392        .await;
5393
5394        let lifecycle_id = Uuid::new_v4();
5395        let mut envelope = signed_envelope(
5396            &sender,
5397            runtime.public_key(),
5398            MessageKind::Lifecycle {
5399                kind: meerkat_core::comms::PeerLifecycleKind::PeerAdded,
5400                params: serde_json::json!({"peer": "sender"}),
5401            },
5402        );
5403        envelope.id = lifecycle_id;
5404        envelope.sign(&sender);
5405
5406        runtime
5407            .router
5408            .inbox_sender()
5409            .send_classified(InboxItem::External { envelope })
5410            .into_result()
5411            .unwrap();
5412
5413        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
5414        assert_eq!(interactions.len(), 1);
5415        assert!(matches!(
5416            &interactions[0].content,
5417            meerkat_core::InteractionContent::Request { intent, .. }
5418                if intent == meerkat_core::comms::PeerLifecycleKind::PeerAdded.as_str()
5419        ));
5420        assert_eq!(
5421            interactions[0].handling_mode,
5422            meerkat_core::types::HandlingMode::Queue
5423        );
5424    }
5425
5426    #[tokio::test]
5427    async fn test_drain_inbox_interactions_multimodal_message_keeps_body_as_projection() {
5428        let tmp = tempfile::TempDir::new().unwrap();
5429        let config = test_runtime_config("multimodal-body-projection", &tmp);
5430        let runtime = CommsRuntime::new(config).await.unwrap();
5431        install_test_peer_comms_handle(&runtime);
5432
5433        let sender = Keypair::generate();
5434        add_trusted_peer_with_generated_authority(
5435            &runtime,
5436            trusted_descriptor("sender", sender.public_key(), "tcp://127.0.0.1:4200"),
5437        )
5438        .await;
5439
5440        let blocks = vec![
5441            meerkat_core::types::ContentBlock::Text {
5442                text: "caption text".to_string(),
5443            },
5444            meerkat_core::types::ContentBlock::Image {
5445                media_type: "image/png".to_string(),
5446                data: "abc123".into(),
5447            },
5448        ];
5449        let msg = signed_envelope(
5450            &sender,
5451            runtime.public_key(),
5452            MessageKind::Message {
5453                body: "please inspect this".to_string(),
5454                blocks: Some(blocks.clone()),
5455                content_taint: None,
5456                handling_mode: None,
5457            },
5458        );
5459
5460        runtime
5461            .router
5462            .inbox_sender()
5463            .send_classified(InboxItem::External { envelope: msg })
5464            .into_result()
5465            .unwrap();
5466
5467        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
5468        assert_eq!(interactions.len(), 1);
5469        let interaction = &interactions[0];
5470        assert_eq!(
5471            interaction.rendered_text,
5472            "Peer message from sender:\nplease inspect this"
5473        );
5474        match &interaction.content {
5475            meerkat_core::InteractionContent::Message {
5476                body,
5477                blocks: got_blocks,
5478            } => {
5479                assert_eq!(body, "please inspect this");
5480                assert_eq!(got_blocks.as_ref(), Some(&blocks));
5481            }
5482            other => panic!("expected message content, got {other:?}"),
5483        }
5484    }
5485
5486    /// ROW #267 gate: a peer message whose body is "DISMISS" must NOT control
5487    /// runtime lifecycle. It is drained as an ordinary peer message, never
5488    /// swallowed into a dismiss signal. Lifecycle dismissal is a typed signal
5489    /// owned by the runtime authority, not a peer-controlled string body.
5490    #[tokio::test]
5491    async fn peer_message_body_dismiss_does_not_control_lifecycle() {
5492        let tmp = tempfile::TempDir::new().unwrap();
5493        let config = test_runtime_config("dismiss-body-is-not-lifecycle", &tmp);
5494        let runtime = CommsRuntime::new(config).await.unwrap();
5495        install_test_peer_comms_handle(&runtime);
5496
5497        let sender = Keypair::generate();
5498        add_trusted_peer_with_generated_authority(
5499            &runtime,
5500            trusted_descriptor("sender", sender.public_key(), "tcp://127.0.0.1:4242"),
5501        )
5502        .await;
5503
5504        let msg = signed_envelope(
5505            &sender,
5506            runtime.public_key(),
5507            MessageKind::Message {
5508                body: "DISMISS".to_string(),
5509                blocks: None,
5510                content_taint: None,
5511                handling_mode: None,
5512            },
5513        );
5514        runtime
5515            .router
5516            .inbox_sender()
5517            .send_classified(InboxItem::External { envelope: msg })
5518            .into_result()
5519            .unwrap();
5520
5521        // The body-string trigger is gone: the message survives the drain as a
5522        // normal peer message rather than being consumed as a dismiss signal.
5523        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
5524        assert_eq!(
5525            interactions.len(),
5526            1,
5527            "a 'DISMISS' body must not be swallowed by a lifecycle trigger"
5528        );
5529        assert!(
5530            matches!(
5531                &interactions[0].content,
5532                meerkat_core::InteractionContent::Message { body, .. } if body == "DISMISS"
5533            ),
5534            "the 'DISMISS' body must drain as an ordinary peer message"
5535        );
5536        // And the runtime exposes no dismiss signal from a peer body: with the
5537        // override removed, the real runtime uses the trait default (`false`).
5538        assert!(
5539            !CoreCommsRuntime::dismiss_received(&runtime),
5540            "a peer message body must never raise a runtime dismiss signal"
5541        );
5542    }
5543
5544    /// ROW #291 gate: a peer-request transport send failure must surface as a
5545    /// typed `SendError` to the caller, never be laundered into a timeout
5546    /// success/receipt. The connectivity failure (unreachable TCP peer) is a
5547    /// `Transport`/`PeerOffline` send error, not a `PeerRequestSent` receipt.
5548    #[cfg(not(target_arch = "wasm32"))]
5549    #[tokio::test]
5550    async fn peer_request_send_failure_is_typed_send_error_not_timeout() {
5551        let suffix = Uuid::new_v4().simple().to_string();
5552        let peer_name = format!("unreachable-peer-{suffix}");
5553        let runtime_name = format!("send-fail-runtime-{suffix}");
5554
5555        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
5556        let runtime = Arc::new(CommsRuntime::inproc_only(&runtime_name).unwrap());
5557        install_test_peer_request_response_authority(&runtime);
5558
5559        // Trust the peer at an unreachable TCP address so the transport send
5560        // genuinely fails (connection refused) rather than completing.
5561        add_trusted_peer_with_generated_authority(
5562            &runtime,
5563            trusted_descriptor(&peer_name, peer.public_key(), "tcp://127.0.0.1:1"),
5564        )
5565        .await;
5566
5567        let result = CoreCommsRuntime::send(
5568            runtime.as_ref(),
5569            CommsCommand::PeerRequest {
5570                content_taint: None,
5571                to: peer_route(&peer_name, peer.public_key()),
5572                intent: "checksum".to_string(),
5573                params: serde_json::json!({"path": "README.md"}),
5574                blocks: None,
5575                handling_mode: meerkat_core::types::HandlingMode::Queue,
5576                stream: InputStreamMode::ReserveInteraction,
5577            },
5578        )
5579        .await;
5580
5581        // The failure must be a typed send error, never a successful receipt
5582        // (which would have masqueraded as a delivered-then-timed-out request).
5583        match result {
5584            Err(SendError::Transport(_) | SendError::PeerOffline | SendError::PeerNotFound(_)) => {}
5585            Ok(receipt) => {
5586                panic!("transport send failure must not produce a success receipt, got {receipt:?}")
5587            }
5588            Err(other) => panic!(
5589                "transport send failure must be a typed connectivity send error, got {other:?}"
5590            ),
5591        }
5592    }
5593
5594    #[tokio::test]
5595    async fn test_subscription_correlation_e2e_one_shot() {
5596        let tmp = tempfile::TempDir::new().unwrap();
5597        let config = test_runtime_config("subscription", &tmp);
5598        let runtime = CommsRuntime::new(config).await.unwrap();
5599        install_test_peer_comms_handle(&runtime);
5600
5601        let injector = CommsEventInjector::new(
5602            runtime.router.inbox_sender().clone(),
5603            runtime.subscriber_registry.clone(),
5604        );
5605        let sub = injector
5606            .inject_with_subscription(
5607                "tracked".to_string().into(),
5608                meerkat_core::PlainEventSource::Rpc,
5609                meerkat_core::types::HandlingMode::Queue,
5610                None,
5611            )
5612            .unwrap();
5613        let tracked_id = sub.id;
5614
5615        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
5616        assert_eq!(interactions.len(), 1);
5617        assert_eq!(interactions[0].id, tracked_id);
5618
5619        let first = CoreCommsRuntime::interaction_subscriber(&runtime, &tracked_id);
5620        assert!(first.is_some(), "subscriber should be found");
5621        let second = CoreCommsRuntime::interaction_subscriber(&runtime, &tracked_id);
5622        assert!(second.is_none(), "subscriber should be one-shot");
5623    }
5624
5625    #[tokio::test]
5626    async fn test_peer_request_reserved_stream_correlates_via_request_envelope_id() {
5627        let suffix = Uuid::new_v4().simple().to_string();
5628        let peer_name = format!("corr-peer-{suffix}");
5629        let runtime_name = format!("corr-runtime-{suffix}");
5630
5631        let peer = inproc_only_with_test_peer_authority(&peer_name);
5632        let runtime = Arc::new(CommsRuntime::inproc_only(&runtime_name).unwrap());
5633        install_test_peer_request_response_authority(&runtime);
5634        {
5635            let mut trusted = runtime.trusted_peers.write();
5636            trusted
5637                .upsert(test_trust_entry(
5638                    &peer_name,
5639                    peer.public_key(),
5640                    &format!("inproc://{peer_name}"),
5641                ))
5642                .expect("valid test peer should upsert");
5643        }
5644        // Peer must also trust the runtime: the receive-side admission gate
5645        // now drops untrusted envelopes with `UntrustedSender` instead of
5646        // silently reporting success.
5647        // Use the generated trust mutation path so the classified inbox's
5648        // canonical `trusted_peers` BTreeSet is synced too, not just the RwLock.
5649        add_trusted_peer_with_generated_authority(
5650            &peer,
5651            trusted_descriptor(
5652                &runtime_name,
5653                runtime.public_key(),
5654                &format!("inproc://{runtime_name}"),
5655            ),
5656        )
5657        .await;
5658
5659        let receipt = CoreCommsRuntime::send(
5660            runtime.as_ref(),
5661            CommsCommand::PeerRequest {
5662                content_taint: None,
5663                to: peer_route(&peer_name, peer.public_key()),
5664                intent: "checksum".to_string(),
5665                params: serde_json::json!({"path": "README.md"}),
5666                blocks: None,
5667                handling_mode: meerkat_core::types::HandlingMode::Queue,
5668                stream: InputStreamMode::ReserveInteraction,
5669            },
5670        )
5671        .await
5672        .expect("peer request send should succeed");
5673
5674        let envelope_id = match receipt {
5675            SendReceipt::PeerRequestSent { envelope_id, .. } => envelope_id,
5676            other => panic!("expected PeerRequestSent, got {other:?}"),
5677        };
5678
5679        let subscriber =
5680            CoreCommsRuntime::interaction_subscriber(runtime.as_ref(), &InteractionId(envelope_id));
5681        assert!(
5682            subscriber.is_some(),
5683            "reserved peer-request stream should correlate via the request envelope id carried in in_reply_to"
5684        );
5685    }
5686
5687    #[tokio::test]
5688    async fn transport_only_runtime_rejects_peer_request_receipts_without_authority() {
5689        let suffix = Uuid::new_v4().simple().to_string();
5690        let peer_name = format!("transport-only-peer-{suffix}");
5691        let runtime_name = format!("transport-only-runtime-{suffix}");
5692
5693        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
5694        let runtime = CommsRuntime::inproc_only(&runtime_name).unwrap();
5695        add_trusted_peer_with_generated_authority(
5696            &runtime,
5697            trusted_descriptor(
5698                &peer_name,
5699                peer.public_key(),
5700                &format!("inproc://{peer_name}"),
5701            ),
5702        )
5703        .await;
5704        add_trusted_peer_with_generated_authority(
5705            &peer,
5706            trusted_descriptor(
5707                &runtime_name,
5708                runtime.public_key(),
5709                &format!("inproc://{runtime_name}"),
5710            ),
5711        )
5712        .await;
5713
5714        let result = CoreCommsRuntime::send(
5715            &runtime,
5716            CommsCommand::PeerRequest {
5717                content_taint: None,
5718                to: peer_route(&peer_name, peer.public_key()),
5719                intent: "must-have-authority".to_string(),
5720                params: serde_json::json!({}),
5721                blocks: None,
5722                handling_mode: meerkat_core::types::HandlingMode::Queue,
5723                stream: InputStreamMode::None,
5724            },
5725        )
5726        .await;
5727
5728        assert!(
5729            matches!(result, Err(SendError::Validation(ref message)) if message.contains("machine peer interaction authority")),
5730            "transport-only runtime must fail before emitting PeerRequestSent, got {result:?}"
5731        );
5732    }
5733
5734    #[tokio::test]
5735    async fn transport_only_runtime_rejects_peer_request_send_and_stream_without_authority() {
5736        let suffix = Uuid::new_v4().simple().to_string();
5737        let peer_name = format!("transport-only-stream-peer-{suffix}");
5738        let runtime_name = format!("transport-only-stream-runtime-{suffix}");
5739
5740        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
5741        let runtime = CommsRuntime::inproc_only(&runtime_name).unwrap();
5742        add_trusted_peer_with_generated_authority(
5743            &runtime,
5744            trusted_descriptor(
5745                &peer_name,
5746                peer.public_key(),
5747                &format!("inproc://{peer_name}"),
5748            ),
5749        )
5750        .await;
5751        add_trusted_peer_with_generated_authority(
5752            &peer,
5753            trusted_descriptor(
5754                &runtime_name,
5755                runtime.public_key(),
5756                &format!("inproc://{runtime_name}"),
5757            ),
5758        )
5759        .await;
5760
5761        let result = CoreCommsRuntime::send_and_stream(
5762            &runtime,
5763            CommsCommand::PeerRequest {
5764                content_taint: None,
5765                to: peer_route(&peer_name, peer.public_key()),
5766                intent: "must-have-stream-authority".to_string(),
5767                params: serde_json::json!({}),
5768                blocks: None,
5769                handling_mode: meerkat_core::types::HandlingMode::Queue,
5770                stream: InputStreamMode::ReserveInteraction,
5771            },
5772        )
5773        .await;
5774
5775        assert!(
5776            matches!(
5777                result,
5778                Err(SendAndStreamError::Send(SendError::Validation(ref message)))
5779                    if message.contains("machine peer interaction authority")
5780            ),
5781            "transport-only runtime must fail before emitting streamed PeerRequestSent"
5782        );
5783    }
5784
5785    #[tokio::test]
5786    async fn transport_only_runtime_rejects_peer_response_receipts_without_authority() {
5787        let suffix = Uuid::new_v4().simple().to_string();
5788        let peer_name = format!("transport-only-response-peer-{suffix}");
5789        let runtime_name = format!("transport-only-response-runtime-{suffix}");
5790
5791        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
5792        let runtime = CommsRuntime::inproc_only(&runtime_name).unwrap();
5793        add_trusted_peer_with_generated_authority(
5794            &runtime,
5795            trusted_descriptor(
5796                &peer_name,
5797                peer.public_key(),
5798                &format!("inproc://{peer_name}"),
5799            ),
5800        )
5801        .await;
5802        add_trusted_peer_with_generated_authority(
5803            &peer,
5804            trusted_descriptor(
5805                &runtime_name,
5806                runtime.public_key(),
5807                &format!("inproc://{runtime_name}"),
5808            ),
5809        )
5810        .await;
5811
5812        let result = CoreCommsRuntime::send(
5813            &runtime,
5814            CommsCommand::PeerResponse {
5815                content_taint: None,
5816                to: peer_route(&peer_name, peer.public_key()),
5817                in_reply_to: meerkat_core::InteractionId(Uuid::new_v4()),
5818                status: meerkat_core::ResponseStatus::Completed,
5819                result: serde_json::json!({"ok": true}),
5820                blocks: None,
5821                handling_mode: Some(meerkat_core::types::HandlingMode::Queue),
5822            },
5823        )
5824        .await;
5825
5826        assert!(
5827            matches!(result, Err(SendError::Validation(ref message)) if message.contains("machine peer interaction authority")),
5828            "transport-only runtime must fail before emitting PeerResponseSent, got {result:?}"
5829        );
5830    }
5831
5832    #[tokio::test]
5833    async fn terminal_peer_response_route_failure_keeps_inbound_request_retryable() {
5834        let suffix = Uuid::new_v4().simple().to_string();
5835        let runtime = Arc::new(
5836            CommsRuntime::inproc_only(&format!("response-route-failure-{suffix}")).unwrap(),
5837        );
5838        let peer_handle = Arc::new(TestPeerInteractionHandle::default());
5839        runtime.install_peer_request_response_authority(PeerRequestResponseAuthority::new(
5840            peer_handle.clone(),
5841            Arc::new(TestInteractionStreamHandle::default()),
5842        ));
5843
5844        let interaction_id = Uuid::new_v4();
5845        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
5846        meerkat_core::handles::PeerInteractionHandle::request_received(
5847            peer_handle.as_ref(),
5848            corr_id,
5849            meerkat_core::types::HandlingMode::Queue,
5850        )
5851        .expect("test authority should seed inbound request state");
5852
5853        let result = CoreCommsRuntime::send(
5854            runtime.as_ref(),
5855            CommsCommand::PeerResponse {
5856                content_taint: None,
5857                to: missing_peer_route(&format!("missing-response-peer-{suffix}")),
5858                in_reply_to: InteractionId(interaction_id),
5859                status: meerkat_core::ResponseStatus::Completed,
5860                result: serde_json::json!({"ok": true}),
5861                blocks: None,
5862                handling_mode: Some(meerkat_core::types::HandlingMode::Queue),
5863            },
5864        )
5865        .await;
5866
5867        assert!(
5868            matches!(result, Err(SendError::PeerNotFound(_))),
5869            "test must fail at transport routing, got {result:?}"
5870        );
5871        assert_eq!(
5872            meerkat_core::handles::PeerInteractionHandle::inbound_state(
5873                peer_handle.as_ref(),
5874                corr_id
5875            ),
5876            Some(meerkat_core::InboundPeerRequestState::Received),
5877            "failed terminal PeerResponse send must leave inbound request state retryable"
5878        );
5879    }
5880
5881    #[tokio::test]
5882    async fn terminal_peer_response_default_handling_mode_projects_from_machine_authority() {
5883        let suffix = Uuid::new_v4().simple().to_string();
5884        let sender_name = format!("response-default-sender-{suffix}");
5885        let receiver_name = format!("response-default-receiver-{suffix}");
5886        let sender = Arc::new(CommsRuntime::inproc_only(&sender_name).unwrap());
5887        let receiver = Arc::new(CommsRuntime::inproc_only(&receiver_name).unwrap());
5888        let peer_handle = Arc::new(TestPeerInteractionHandle::default());
5889        sender.install_peer_request_response_authority(PeerRequestResponseAuthority::new(
5890            peer_handle.clone(),
5891            Arc::new(TestInteractionStreamHandle::default()),
5892        ));
5893        install_test_peer_comms_handle(receiver.as_ref());
5894        let receiver_peer_handle = Arc::new(TestPeerInteractionHandle::default());
5895        receiver.install_peer_request_response_authority(PeerRequestResponseAuthority::new(
5896            receiver_peer_handle.clone(),
5897            Arc::new(TestInteractionStreamHandle::default()),
5898        ));
5899
5900        add_trusted_peer_with_generated_authority(
5901            sender.as_ref(),
5902            trusted_descriptor(
5903                &receiver_name,
5904                receiver.public_key(),
5905                &format!("inproc://{receiver_name}"),
5906            ),
5907        )
5908        .await;
5909        add_trusted_peer_with_generated_authority(
5910            receiver.as_ref(),
5911            trusted_descriptor(
5912                &sender_name,
5913                sender.public_key(),
5914                &format!("inproc://{sender_name}"),
5915            ),
5916        )
5917        .await;
5918
5919        let interaction_id = Uuid::new_v4();
5920        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
5921        meerkat_core::handles::PeerInteractionHandle::request_received(
5922            peer_handle.as_ref(),
5923            corr_id,
5924            meerkat_core::types::HandlingMode::Queue,
5925        )
5926        .expect("machine authority should record inbound request lane");
5927        assert_eq!(
5928            meerkat_core::handles::PeerInteractionHandle::inbound_handling_mode(
5929                peer_handle.as_ref(),
5930                corr_id
5931            ),
5932            Some(meerkat_core::types::HandlingMode::Queue)
5933        );
5934        meerkat_core::handles::PeerInteractionHandle::request_sent(
5935            receiver_peer_handle.as_ref(),
5936            corr_id,
5937        )
5938        .expect("receiver machine authority should expect the response");
5939
5940        let receipt = CoreCommsRuntime::send(
5941            sender.as_ref(),
5942            CommsCommand::PeerResponse {
5943                content_taint: None,
5944                to: peer_route(&receiver_name, receiver.public_key()),
5945                in_reply_to: InteractionId(interaction_id),
5946                status: meerkat_core::ResponseStatus::Completed,
5947                result: serde_json::json!({"ok": true}),
5948                blocks: None,
5949                handling_mode: None,
5950            },
5951        )
5952        .await
5953        .expect("terminal response send should succeed");
5954        assert!(matches!(receipt, SendReceipt::PeerResponseSent { .. }));
5955
5956        let interactions = CoreCommsRuntime::drain_inbox_interactions(receiver.as_ref()).await;
5957        assert_eq!(interactions.len(), 1);
5958        assert_eq!(
5959            interactions[0].handling_mode,
5960            meerkat_core::types::HandlingMode::Queue
5961        );
5962        assert!(
5963            meerkat_core::handles::PeerInteractionHandle::inbound_handling_mode(
5964                peer_handle.as_ref(),
5965                corr_id
5966            )
5967            .is_none()
5968        );
5969        assert!(matches!(
5970            &interactions[0].content,
5971            meerkat_core::InteractionContent::Response { in_reply_to, .. }
5972                if in_reply_to.0 == interaction_id
5973        ));
5974    }
5975
5976    #[tokio::test]
5977    async fn transport_only_peer_directory_does_not_advertise_semantic_request_response() {
5978        let suffix = Uuid::new_v4().simple().to_string();
5979        let peer_name = format!("directory-transport-peer-{suffix}");
5980        let runtime_name = format!("directory-transport-runtime-{suffix}");
5981
5982        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
5983        let runtime = CommsRuntime::inproc_only(&runtime_name).unwrap();
5984        add_trusted_peer_with_generated_authority(
5985            &runtime,
5986            trusted_descriptor(
5987                &peer_name,
5988                peer.public_key(),
5989                &format!("inproc://{peer_name}"),
5990            ),
5991        )
5992        .await;
5993
5994        let peers = CoreCommsRuntime::peers(&runtime).await;
5995        let entry = peers
5996            .iter()
5997            .find(|entry| entry.name.as_str() == peer_name)
5998            .expect("trusted peer should be listed");
5999
6000        assert_eq!(entry.sendable_kinds, vec![PeerSendability::PeerMessage]);
6001    }
6002
6003    #[tokio::test]
6004    async fn partial_peer_authority_does_not_advertise_semantic_request_response() {
6005        let suffix = Uuid::new_v4().simple().to_string();
6006        let peer_name = format!("directory-partial-peer-{suffix}");
6007        let runtime_name = format!("directory-partial-runtime-{suffix}");
6008
6009        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
6010        let runtime = Arc::new(CommsRuntime::inproc_only(&runtime_name).unwrap());
6011        runtime.install_peer_interaction_handle(Arc::new(TestPeerInteractionHandle::default()));
6012        add_trusted_peer_with_generated_authority(
6013            runtime.as_ref(),
6014            trusted_descriptor(
6015                &peer_name,
6016                peer.public_key(),
6017                &format!("inproc://{peer_name}"),
6018            ),
6019        )
6020        .await;
6021
6022        let peers = CoreCommsRuntime::peers(runtime.as_ref()).await;
6023        let entry = peers
6024            .iter()
6025            .find(|entry| entry.name.as_str() == peer_name)
6026            .expect("trusted peer should be listed");
6027
6028        assert_eq!(entry.sendable_kinds, vec![PeerSendability::PeerMessage]);
6029    }
6030
6031    #[tokio::test]
6032    async fn partial_peer_authority_rejects_peer_request_receipts_without_stream_authority() {
6033        let suffix = Uuid::new_v4().simple().to_string();
6034        let peer_name = format!("partial-authority-peer-{suffix}");
6035        let runtime_name = format!("partial-authority-runtime-{suffix}");
6036
6037        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
6038        let runtime = Arc::new(CommsRuntime::inproc_only(&runtime_name).unwrap());
6039        runtime.install_peer_interaction_handle(Arc::new(TestPeerInteractionHandle::default()));
6040        add_trusted_peer_with_generated_authority(
6041            runtime.as_ref(),
6042            trusted_descriptor(
6043                &peer_name,
6044                peer.public_key(),
6045                &format!("inproc://{peer_name}"),
6046            ),
6047        )
6048        .await;
6049        add_trusted_peer_with_generated_authority(
6050            &peer,
6051            trusted_descriptor(
6052                &runtime_name,
6053                runtime.public_key(),
6054                &format!("inproc://{runtime_name}"),
6055            ),
6056        )
6057        .await;
6058
6059        let result = CoreCommsRuntime::send(
6060            runtime.as_ref(),
6061            CommsCommand::PeerRequest {
6062                content_taint: None,
6063                to: peer_route(&peer_name, peer.public_key()),
6064                intent: "must-have-complete-authority".to_string(),
6065                params: serde_json::json!({}),
6066                blocks: None,
6067                handling_mode: meerkat_core::types::HandlingMode::Queue,
6068                stream: InputStreamMode::None,
6069            },
6070        )
6071        .await;
6072
6073        assert!(
6074            matches!(result, Err(SendError::Validation(ref message)) if message.contains("machine interaction stream authority")),
6075            "partial peer authority must fail before emitting PeerRequestSent, got {result:?}"
6076        );
6077    }
6078
6079    #[tokio::test]
6080    async fn partial_peer_authority_rejects_peer_response_receipts_without_stream_authority() {
6081        let suffix = Uuid::new_v4().simple().to_string();
6082        let peer_name = format!("partial-response-peer-{suffix}");
6083        let runtime_name = format!("partial-response-runtime-{suffix}");
6084
6085        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
6086        let runtime = Arc::new(CommsRuntime::inproc_only(&runtime_name).unwrap());
6087        let peer_handle = Arc::new(TestPeerInteractionHandle::default());
6088        runtime.install_peer_interaction_handle(peer_handle.clone());
6089        add_trusted_peer_with_generated_authority(
6090            runtime.as_ref(),
6091            trusted_descriptor(
6092                &peer_name,
6093                peer.public_key(),
6094                &format!("inproc://{peer_name}"),
6095            ),
6096        )
6097        .await;
6098        add_trusted_peer_with_generated_authority(
6099            &peer,
6100            trusted_descriptor(
6101                &runtime_name,
6102                runtime.public_key(),
6103                &format!("inproc://{runtime_name}"),
6104            ),
6105        )
6106        .await;
6107
6108        let interaction_id = Uuid::new_v4();
6109        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
6110        meerkat_core::handles::PeerInteractionHandle::request_received(
6111            peer_handle.as_ref(),
6112            corr_id,
6113            meerkat_core::types::HandlingMode::Queue,
6114        )
6115        .expect("seed inbound request state");
6116
6117        let result = CoreCommsRuntime::send(
6118            runtime.as_ref(),
6119            CommsCommand::PeerResponse {
6120                content_taint: None,
6121                to: peer_route(&peer_name, peer.public_key()),
6122                in_reply_to: InteractionId(interaction_id),
6123                status: meerkat_core::ResponseStatus::Completed,
6124                result: serde_json::json!({"ok": true}),
6125                blocks: None,
6126                handling_mode: Some(meerkat_core::types::HandlingMode::Queue),
6127            },
6128        )
6129        .await;
6130
6131        assert!(
6132            matches!(result, Err(SendError::Validation(ref message)) if message.contains("machine interaction stream authority")),
6133            "partial peer authority must fail before emitting PeerResponseSent, got {result:?}"
6134        );
6135        assert_eq!(
6136            meerkat_core::handles::PeerInteractionHandle::inbound_state(
6137                peer_handle.as_ref(),
6138                corr_id
6139            ),
6140            Some(meerkat_core::InboundPeerRequestState::Received),
6141            "failed PeerResponse must leave inbound request state retryable"
6142        );
6143    }
6144
6145    #[tokio::test]
6146    async fn peer_directory_advertises_semantic_request_response_with_authority() {
6147        let suffix = Uuid::new_v4().simple().to_string();
6148        let peer_name = format!("directory-semantic-peer-{suffix}");
6149        let runtime_name = format!("directory-semantic-runtime-{suffix}");
6150
6151        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
6152        let runtime = Arc::new(CommsRuntime::inproc_only(&runtime_name).unwrap());
6153        install_test_peer_request_response_authority(&runtime);
6154        add_trusted_peer_with_generated_authority(
6155            runtime.as_ref(),
6156            trusted_descriptor(
6157                &peer_name,
6158                peer.public_key(),
6159                &format!("inproc://{peer_name}"),
6160            ),
6161        )
6162        .await;
6163
6164        let peers = CoreCommsRuntime::peers(runtime.as_ref()).await;
6165        let entry = peers
6166            .iter()
6167            .find(|entry| entry.name.as_str() == peer_name)
6168            .expect("trusted peer should be listed");
6169
6170        assert_eq!(
6171            entry.sendable_kinds,
6172            vec![
6173                PeerSendability::PeerMessage,
6174                PeerSendability::PeerRequest,
6175                PeerSendability::PeerResponse,
6176            ]
6177        );
6178    }
6179
6180    #[tokio::test]
6181    async fn test_peer_lifecycle_send_stays_typed_and_non_rendered() {
6182        let suffix = Uuid::new_v4().simple().to_string();
6183        let sender_name = format!("life-sender-{suffix}");
6184        let receiver_name = format!("life-receiver-{suffix}");
6185
6186        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
6187        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
6188
6189        add_trusted_peer_with_generated_authority(
6190            &sender,
6191            trusted_descriptor(
6192                &receiver_name,
6193                receiver.public_key(),
6194                &format!("inproc://{receiver_name}"),
6195            ),
6196        )
6197        .await;
6198        add_trusted_peer_with_generated_authority(
6199            &receiver,
6200            trusted_descriptor(
6201                &sender_name,
6202                sender.public_key(),
6203                &format!("inproc://{sender_name}"),
6204            ),
6205        )
6206        .await;
6207
6208        let receipt = CoreCommsRuntime::send(
6209            &sender,
6210            CommsCommand::PeerLifecycle {
6211                to: peer_route(&receiver_name, receiver.public_key()),
6212                kind: meerkat_core::comms::PeerLifecycleKind::PeerAdded,
6213                params: serde_json::json!({
6214                    "peer": "worker-1",
6215                    "role": "worker",
6216                }),
6217            },
6218        )
6219        .await
6220        .expect("peer lifecycle send should succeed");
6221
6222        match receipt {
6223            SendReceipt::PeerLifecycleSent { .. } => {}
6224            other => panic!("expected PeerLifecycleSent, got {other:?}"),
6225        }
6226
6227        let interactions = receiver
6228            .drain_classified_inbox_interactions()
6229            .await
6230            .expect("classified drain should succeed");
6231        assert_eq!(interactions.len(), 1);
6232        assert_eq!(
6233            interactions[0].class(),
6234            meerkat_core::PeerInputClass::PeerLifecycleAdded
6235        );
6236        assert_eq!(interactions[0].lifecycle_peer.as_deref(), Some("worker-1"));
6237        assert!(
6238            interactions[0].interaction.rendered_text.is_empty(),
6239            "silent lifecycle notices must not synthesize prompt text"
6240        );
6241    }
6242
6243    #[tokio::test]
6244    async fn test_subscription_correlation_preserves_inline_blocks() {
6245        let tmp = tempfile::TempDir::new().unwrap();
6246        let config = test_runtime_config("subscription-blocks", &tmp);
6247        let runtime = CommsRuntime::new(config).await.unwrap();
6248        install_test_peer_comms_handle(&runtime);
6249
6250        let injector = CommsEventInjector::new(
6251            runtime.router.inbox_sender().clone(),
6252            runtime.subscriber_registry.clone(),
6253        );
6254        let sub = injector
6255            .inject_with_subscription(
6256                meerkat_core::types::ContentInput::Blocks(vec![
6257                    meerkat_core::types::ContentBlock::Text {
6258                        text: "tracked".to_string(),
6259                    },
6260                    meerkat_core::types::ContentBlock::Image {
6261                        media_type: "image/png".to_string(),
6262                        data: "aGVsbG8=".into(),
6263                    },
6264                ]),
6265                meerkat_core::PlainEventSource::Rpc,
6266                meerkat_core::types::HandlingMode::Queue,
6267                None,
6268            )
6269            .unwrap();
6270        let tracked_id = sub.id;
6271
6272        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
6273        assert_eq!(interactions.len(), 1);
6274        assert_eq!(interactions[0].id, tracked_id);
6275        match &interactions[0].content {
6276            meerkat_core::InteractionContent::Message { blocks, .. } => {
6277                let blocks = blocks.as_ref().expect("blocks preserved");
6278                assert!(
6279                    blocks.iter().any(|block| matches!(
6280                        block,
6281                        meerkat_core::types::ContentBlock::Image { .. }
6282                    )),
6283                    "expected inline image block to survive into drained interaction"
6284                );
6285            }
6286            other => panic!("expected message interaction, got {other:?}"),
6287        }
6288    }
6289
6290    #[tokio::test]
6291    async fn runtime_bridge_red_ok_send_and_stream_reserves_one_interaction_channel() {
6292        let suffix = Uuid::new_v4().simple().to_string();
6293        let runtime = inproc_only_with_test_peer_authority(&format!("phase1-bridge-{suffix}"));
6294
6295        let cmd = CommsCommand::Input {
6296            blocks: None,
6297            session_id: SessionId::new(),
6298            body: "phase 1 bridge input".to_string(),
6299            handling_mode: meerkat_core::types::HandlingMode::Queue,
6300            source: InputSource::Rpc,
6301            stream: InputStreamMode::ReserveInteraction,
6302            allow_self_session: true,
6303        };
6304
6305        let (receipt, _stream) = CoreCommsRuntime::send_and_stream(&runtime, cmd)
6306            .await
6307            .expect("send_and_stream should reserve interaction scope");
6308
6309        let interaction_id = match receipt {
6310            SendReceipt::InputAccepted {
6311                interaction_id,
6312                stream_reserved,
6313            } => {
6314                assert!(
6315                    stream_reserved,
6316                    "bridge receipt should advertise reservation"
6317                );
6318                interaction_id
6319            }
6320            other => panic!("expected InputAccepted, got {other:?}"),
6321        };
6322
6323        let duplicate =
6324            CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id));
6325        assert!(
6326            matches!(duplicate, Err(StreamError::AlreadyAttached(_))),
6327            "reserved interaction streams should reject a second attachment"
6328        );
6329    }
6330
6331    #[tokio::test]
6332    async fn local_input_stream_remains_transport_only_with_machine_stream_authority() {
6333        let suffix = Uuid::new_v4().simple().to_string();
6334        let runtime = Arc::new(inproc_only_with_test_peer_authority(&format!(
6335            "local-transport-authority-{suffix}"
6336        )));
6337        let stream_handle = Arc::new(TestInteractionStreamHandle::default());
6338        runtime.install_interaction_stream_handle(stream_handle.clone());
6339
6340        let cmd = CommsCommand::Input {
6341            blocks: None,
6342            session_id: SessionId::new(),
6343            body: "local input stream".to_string(),
6344            handling_mode: meerkat_core::types::HandlingMode::Queue,
6345            source: InputSource::Rpc,
6346            stream: InputStreamMode::ReserveInteraction,
6347            allow_self_session: true,
6348        };
6349
6350        let (receipt, _stream) = CoreCommsRuntime::send_and_stream(runtime.as_ref(), cmd)
6351            .await
6352            .expect("local input send_and_stream should attach without semantic stream authority");
6353
6354        let interaction_id = match receipt {
6355            SendReceipt::InputAccepted {
6356                interaction_id,
6357                stream_reserved,
6358            } => {
6359                assert!(stream_reserved);
6360                interaction_id
6361            }
6362            other => panic!("expected InputAccepted, got {other:?}"),
6363        };
6364        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id.0);
6365        assert_eq!(
6366            meerkat_core::handles::InteractionStreamHandle::state(stream_handle.as_ref(), corr_id),
6367            None,
6368            "local input streams must not create semantic stream DSL state"
6369        );
6370
6371        let duplicate =
6372            CoreCommsRuntime::stream(runtime.as_ref(), StreamScope::Interaction(interaction_id));
6373        assert!(
6374            matches!(duplicate, Err(StreamError::AlreadyAttached(_))),
6375            "local input streams should keep transport-only duplicate attach semantics"
6376        );
6377    }
6378
6379    #[tokio::test]
6380    async fn runtime_bridge_red_ok_completed_interaction_terminates_reserved_stream() {
6381        let suffix = Uuid::new_v4().simple().to_string();
6382        let runtime = inproc_only_with_test_peer_authority(&format!("phase1-complete-{suffix}"));
6383
6384        let receipt = CoreCommsRuntime::send(
6385            &runtime,
6386            CommsCommand::Input {
6387                blocks: None,
6388                session_id: SessionId::new(),
6389                body: "complete reserved interaction".to_string(),
6390                handling_mode: meerkat_core::types::HandlingMode::Queue,
6391                source: InputSource::Rpc,
6392                stream: InputStreamMode::ReserveInteraction,
6393                allow_self_session: true,
6394            },
6395        )
6396        .await
6397        .expect("send should succeed");
6398
6399        let interaction_id = match receipt {
6400            SendReceipt::InputAccepted { interaction_id, .. } => interaction_id,
6401            other => panic!("expected InputAccepted, got {other:?}"),
6402        };
6403
6404        let mut stream =
6405            CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id))
6406                .expect("reserved stream should attach");
6407        runtime.mark_interaction_complete(interaction_id.0);
6408
6409        let terminal = timeout(Duration::from_millis(100), stream.next())
6410            .await
6411            .expect("stream should terminate promptly after completion");
6412        assert!(
6413            terminal.is_none(),
6414            "interaction completion should close the reserved stream for parent waiters"
6415        );
6416    }
6417
6418    #[tokio::test]
6419    async fn test_plain_event_interaction_id_is_preserved_in_drain_inbox_interactions() {
6420        let tmp = tempfile::TempDir::new().unwrap();
6421        let config = test_runtime_config("plain-id", &tmp);
6422        let runtime = CommsRuntime::new(config).await.unwrap();
6423        install_test_peer_comms_handle(&runtime);
6424
6425        let interaction_id = Uuid::new_v4();
6426        runtime
6427            .router
6428            .inbox_sender()
6429            .send_classified(InboxItem::PlainEvent {
6430                blocks: None,
6431                body: "evt".to_string(),
6432                source: meerkat_core::PlainEventSource::Tcp,
6433                handling_mode: meerkat_core::types::HandlingMode::Queue,
6434                interaction_id: Some(interaction_id),
6435                render_metadata: None,
6436            })
6437            .into_result()
6438            .unwrap();
6439
6440        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
6441        assert_eq!(interactions.len(), 1);
6442        assert_eq!(interactions[0].id.0, interaction_id);
6443    }
6444
6445    #[tokio::test]
6446    async fn test_plain_event_preserves_handling_mode_and_render_metadata_in_classified_drain() {
6447        let tmp = tempfile::TempDir::new().unwrap();
6448        let config = test_runtime_config("plain-hints", &tmp);
6449        let runtime = CommsRuntime::new(config).await.unwrap();
6450        install_test_peer_comms_handle(&runtime);
6451        let render_metadata = meerkat_core::types::RenderMetadata {
6452            class: meerkat_core::types::RenderClass::ExternalEvent,
6453            salience: meerkat_core::types::RenderSalience::Urgent,
6454        };
6455
6456        runtime
6457            .router
6458            .inbox_sender()
6459            .send_classified(InboxItem::PlainEvent {
6460                blocks: None,
6461                body: "evt".to_string(),
6462                source: meerkat_core::PlainEventSource::Tcp,
6463                handling_mode: meerkat_core::types::HandlingMode::Steer,
6464                interaction_id: None,
6465                render_metadata: Some(render_metadata.clone()),
6466            })
6467            .into_result()
6468            .unwrap();
6469
6470        let interactions = runtime.drain_classified_inbox_interactions().await.unwrap();
6471        assert_eq!(interactions.len(), 1);
6472        let interaction = &interactions[0].interaction;
6473        assert_eq!(
6474            interaction.handling_mode,
6475            meerkat_core::types::HandlingMode::Steer
6476        );
6477        assert_eq!(interaction.render_metadata, Some(render_metadata));
6478    }
6479
6480    #[tokio::test]
6481    async fn test_plain_event_without_interaction_id_gets_stable_id_between_snapshot_and_drain() {
6482        let tmp = tempfile::TempDir::new().unwrap();
6483        let config = test_runtime_config("plain-generated-id", &tmp);
6484        let runtime = CommsRuntime::new(config).await.unwrap();
6485        install_test_peer_comms_handle(&runtime);
6486
6487        runtime
6488            .router
6489            .inbox_sender()
6490            .send_classified(InboxItem::PlainEvent {
6491                blocks: None,
6492                body: "evt".to_string(),
6493                source: meerkat_core::PlainEventSource::Tcp,
6494                handling_mode: meerkat_core::types::HandlingMode::Queue,
6495                interaction_id: None,
6496                render_metadata: None,
6497            })
6498            .into_result()
6499            .unwrap();
6500
6501        let snapshot = CoreCommsRuntime::peer_ingress_queue_snapshot(&runtime)
6502            .await
6503            .expect("classified queue snapshot should be available");
6504        let ingress_id = snapshot
6505            .queued_entries
6506            .first()
6507            .and_then(|entry| entry.interaction_id)
6508            .expect("plain event should have a stable generated interaction id at ingress")
6509            .0;
6510        assert_eq!(
6511            snapshot.queued_entries[0].raw_item_id,
6512            meerkat_core::InteractionId(ingress_id),
6513            "the queue snapshot should expose the same stable ingress id it uses as the raw item key"
6514        );
6515
6516        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
6517        assert_eq!(interactions.len(), 1);
6518        assert_eq!(interactions[0].id.0, ingress_id);
6519    }
6520
6521    #[tokio::test]
6522    async fn test_peer_ingress_queue_snapshot_reflects_classified_queue_without_draining() {
6523        let tmp = tempfile::TempDir::new().unwrap();
6524        let mut config = test_runtime_config("peer-snapshot", &tmp);
6525        config.require_peer_auth = false;
6526        let runtime = CommsRuntime::new(config).await.unwrap();
6527        install_test_peer_comms_handle(&runtime);
6528
6529        let sender = Keypair::generate();
6530        let envelope = signed_envelope(
6531            &sender,
6532            runtime.public_key,
6533            crate::types::MessageKind::Request {
6534                intent: "review".to_string(),
6535                params: serde_json::json!({"pr": 42}),
6536                blocks: None,
6537                content_taint: None,
6538                handling_mode: None,
6539            },
6540        );
6541
6542        runtime
6543            .router
6544            .inbox_sender()
6545            .send_classified(InboxItem::External { envelope })
6546            .into_result()
6547            .unwrap();
6548        runtime
6549            .router
6550            .inbox_sender()
6551            .send_classified(InboxItem::PlainEvent {
6552                blocks: None,
6553                body: "evt".to_string(),
6554                source: meerkat_core::PlainEventSource::Tcp,
6555                handling_mode: meerkat_core::types::HandlingMode::Queue,
6556                interaction_id: None,
6557                render_metadata: None,
6558            })
6559            .into_result()
6560            .unwrap();
6561
6562        let snapshot = CoreCommsRuntime::peer_ingress_queue_snapshot(&runtime)
6563            .await
6564            .expect("classified queue snapshot should be available");
6565        assert_eq!(snapshot.total_count, 2);
6566        assert_eq!(snapshot.actionable_count, 2);
6567        assert_eq!(snapshot.plain_event_count, 1);
6568        assert_eq!(snapshot.response_count, 0);
6569        assert_eq!(snapshot.lifecycle_count, 0);
6570        assert_eq!(snapshot.queued_entries.len(), 2);
6571        assert_eq!(
6572            snapshot.queued_entries[0].kind,
6573            meerkat_core::PeerIngressKind::Request
6574        );
6575        assert_eq!(
6576            snapshot.queued_entries[1].kind,
6577            meerkat_core::PeerIngressKind::PlainEvent
6578        );
6579
6580        let interactions = runtime.drain_classified_inbox_interactions().await.unwrap();
6581        assert_eq!(
6582            interactions.len(),
6583            2,
6584            "snapshot must not consume classified peer ingress"
6585        );
6586    }
6587
6588    #[tokio::test]
6589    async fn test_peer_ingress_runtime_snapshot_reflects_trusted_peers_and_queue() {
6590        let tmp = tempfile::TempDir::new().unwrap();
6591        let mut config = test_runtime_config("peer-runtime-snapshot", &tmp);
6592        config.require_peer_auth = false;
6593        let runtime = CommsRuntime::new(config).await.unwrap();
6594        install_test_peer_comms_handle(&runtime);
6595
6596        let peer_key = Keypair::generate();
6597        let trusted_peer = trusted_descriptor("ally", peer_key.public_key(), "inproc://ally");
6598
6599        add_trusted_peer_with_generated_authority(&runtime, trusted_peer.clone()).await;
6600
6601        runtime
6602            .event_injector()
6603            .inject(
6604                "evt".to_string().into(),
6605                meerkat_core::PlainEventSource::Tcp,
6606                meerkat_core::types::HandlingMode::Queue,
6607                None,
6608            )
6609            .expect("plain event injection should succeed");
6610
6611        let snapshot = CoreCommsRuntime::peer_ingress_runtime_snapshot(&runtime)
6612            .await
6613            .expect("peer runtime snapshot should be available");
6614        assert_eq!(snapshot.self_peer_id, runtime.public_key().to_peer_id());
6615        assert!(!snapshot.auth_required);
6616        assert_eq!(
6617            snapshot.authority_phase,
6618            meerkat_core::PeerIngressAuthorityPhase::Absent
6619        );
6620        assert_eq!(snapshot.trusted_peers, vec![trusted_peer]);
6621        assert_eq!(snapshot.submission_queue_len, 1);
6622        assert_eq!(snapshot.queue.total_count, 1);
6623        assert_eq!(snapshot.queue.plain_event_count, 1);
6624        assert_eq!(snapshot.queue.queued_entries.len(), 1);
6625        assert_eq!(
6626            snapshot.queue.queued_entries[0].kind,
6627            meerkat_core::PeerIngressKind::PlainEvent
6628        );
6629        assert_eq!(snapshot.queue.queued_entries[0].admission_diagnostic, None);
6630        assert_eq!(
6631            snapshot.queue.queued_entries[0].raw_item_id,
6632            snapshot.queue.queued_entries[0]
6633                .interaction_id
6634                .expect("plain event should retain an ingress interaction id")
6635        );
6636    }
6637
6638    #[tokio::test]
6639    async fn test_peer_ingress_runtime_snapshot_reflects_dropped_peer_authority_state() {
6640        let tmp = tempfile::TempDir::new().unwrap();
6641        let config = test_runtime_config("peer-runtime-dropped", &tmp);
6642        let runtime = CommsRuntime::new(config).await.unwrap();
6643        install_test_peer_comms_handle(&runtime);
6644
6645        let sender = Keypair::generate();
6646        let envelope = signed_envelope(
6647            &sender,
6648            runtime.public_key(),
6649            MessageKind::Request {
6650                intent: "review".to_string(),
6651                params: serde_json::json!({"scope": "peer"}),
6652                blocks: None,
6653                content_taint: None,
6654                handling_mode: None,
6655            },
6656        );
6657
6658        // Untrusted sender, require_peer_auth on: the admission gate drops
6659        // the envelope with `UntrustedSender`. We assert the typed outcome
6660        // directly so the silent-Ok path can't creep back.
6661        let outcome = runtime
6662            .router
6663            .inbox_sender()
6664            .send_classified(InboxItem::External { envelope });
6665        assert_eq!(
6666            outcome,
6667            crate::inbox::AdmissionOutcome::Dropped {
6668                reason: crate::inbox::DropReason::UntrustedSender
6669            }
6670        );
6671
6672        let snapshot = CoreCommsRuntime::peer_ingress_runtime_snapshot(&runtime)
6673            .await
6674            .expect("peer runtime snapshot should be available");
6675        assert_eq!(
6676            snapshot.authority_phase,
6677            meerkat_core::PeerIngressAuthorityPhase::Dropped
6678        );
6679        assert_eq!(snapshot.submission_queue_len, 0);
6680        assert_eq!(snapshot.queue.total_count, 0);
6681        assert!(snapshot.trusted_peers.is_empty());
6682    }
6683
6684    #[tokio::test]
6685    async fn test_live_peer_authority_syncs_trust_receive_and_drain() {
6686        use crate::peer_types::PeerIngressState;
6687
6688        let tmp = tempfile::TempDir::new().unwrap();
6689        let config = test_runtime_config("peer-authority-sync", &tmp);
6690        let runtime = CommsRuntime::new(config).await.unwrap();
6691        install_test_peer_comms_handle(&runtime);
6692
6693        let sender = Keypair::generate();
6694        let pubkey_for_authority_check = sender.public_key();
6695        let envelope = signed_envelope(
6696            &sender,
6697            runtime.public_key(),
6698            MessageKind::Request {
6699                intent: "review".to_string(),
6700                params: serde_json::json!({"scope": "peer"}),
6701                blocks: None,
6702                content_taint: None,
6703                handling_mode: None,
6704            },
6705        );
6706
6707        // Untrusted sender under require_peer_auth: must be explicitly
6708        // dropped at admission with a typed reason — no more silent Ok(()).
6709        let outcome = runtime
6710            .router
6711            .inbox_sender()
6712            .send_classified(InboxItem::External {
6713                envelope: envelope.clone(),
6714            });
6715        assert_eq!(
6716            outcome,
6717            crate::inbox::AdmissionOutcome::Dropped {
6718                reason: crate::inbox::DropReason::UntrustedSender
6719            }
6720        );
6721
6722        {
6723            let inbox = runtime.inbox.lock().await;
6724            assert_eq!(
6725                inbox
6726                    .classified_snapshot()
6727                    .expect("classified snapshot should exist")
6728                    .total_count,
6729                0
6730            );
6731            assert_eq!(
6732                inbox.peer_authority_test_snapshot(),
6733                Some((PeerIngressState::Dropped, 0))
6734            );
6735            assert_eq!(
6736                inbox.peer_authority_trusts_peer_for_test(&pubkey_for_authority_check),
6737                Some(false)
6738            );
6739            // Drop counter ticked exactly once — the bug repair is visible.
6740            assert_eq!(inbox.dropped_count(), Some(1));
6741        }
6742
6743        let trusted_peer = trusted_descriptor("sender", sender.public_key(), "inproc://sender");
6744        let trust_dsl =
6745            add_trusted_peer_with_generated_authority(&runtime, trusted_peer.clone()).await;
6746
6747        {
6748            let inbox = runtime.inbox.lock().await;
6749            assert_eq!(
6750                inbox.peer_authority_test_snapshot(),
6751                Some((PeerIngressState::Dropped, 0))
6752            );
6753            assert_eq!(
6754                inbox.peer_authority_trusts_peer_for_test(&pubkey_for_authority_check),
6755                Some(true)
6756            );
6757        }
6758
6759        runtime
6760            .router
6761            .inbox_sender()
6762            .send_classified(InboxItem::External { envelope })
6763            .into_result()
6764            .unwrap();
6765
6766        let snapshot = CoreCommsRuntime::peer_ingress_runtime_snapshot(&runtime)
6767            .await
6768            .expect("peer runtime snapshot should be available");
6769        assert_eq!(
6770            snapshot.authority_phase,
6771            meerkat_core::PeerIngressAuthorityPhase::Received
6772        );
6773        assert_eq!(snapshot.submission_queue_len, 1);
6774        assert_eq!(snapshot.queue.total_count, 1);
6775        assert_eq!(
6776            snapshot.queue.queued_entries[0].admission_diagnostic,
6777            Some(meerkat_core::PeerIngressAdmissionDiagnostic::TrustedAtAdmission)
6778        );
6779
6780        {
6781            let inbox = runtime.inbox.lock().await;
6782            assert_eq!(
6783                inbox
6784                    .classified_snapshot()
6785                    .expect("classified snapshot should exist")
6786                    .total_count,
6787                1
6788            );
6789            assert_eq!(
6790                inbox.peer_authority_test_snapshot(),
6791                Some((PeerIngressState::Received, 1))
6792            );
6793        }
6794
6795        let interactions = runtime.drain_classified_inbox_interactions().await.unwrap();
6796        assert_eq!(interactions.len(), 1);
6797
6798        {
6799            let inbox = runtime.inbox.lock().await;
6800            assert_eq!(
6801                inbox.peer_authority_test_snapshot(),
6802                Some((PeerIngressState::Delivered, 0))
6803            );
6804        }
6805
6806        let remove_authority =
6807            test_projection_remove_authority_with_dsl(Arc::clone(&trust_dsl), &trusted_peer);
6808        let removed = CoreCommsRuntime::apply_trust_mutation(
6809            &runtime,
6810            CommsTrustMutation::RemoveTrustedPeer {
6811                peer_id: trusted_peer.peer_id.to_string(),
6812                authority: remove_authority.authority,
6813            },
6814        )
6815        .await
6816        .expect("remove_trusted_peer should succeed");
6817        let CommsTrustMutationResult::Removed { removed } = removed else {
6818            panic!("unexpected generated trust mutation result: {removed:?}");
6819        };
6820        assert!(removed);
6821
6822        {
6823            let inbox = runtime.inbox.lock().await;
6824            assert_eq!(
6825                inbox.peer_authority_test_snapshot(),
6826                Some((PeerIngressState::Delivered, 0))
6827            );
6828            assert_eq!(
6829                inbox.peer_authority_trusts_peer_for_test(&pubkey_for_authority_check),
6830                Some(false)
6831            );
6832        }
6833    }
6834
6835    #[tokio::test]
6836    async fn test_live_peer_authority_accepts_unknown_peer_when_auth_open() {
6837        use crate::peer_types::PeerIngressState;
6838
6839        let tmp = tempfile::TempDir::new().unwrap();
6840        let mut config = test_runtime_config("peer-authority-auth-open", &tmp);
6841        config.require_peer_auth = false;
6842        let runtime = CommsRuntime::new(config).await.unwrap();
6843        install_test_peer_comms_handle(&runtime);
6844
6845        let sender = Keypair::generate();
6846        let envelope = signed_envelope(
6847            &sender,
6848            runtime.public_key(),
6849            MessageKind::Request {
6850                intent: "status".to_string(),
6851                params: serde_json::json!({}),
6852                blocks: None,
6853                content_taint: None,
6854                handling_mode: None,
6855            },
6856        );
6857
6858        runtime
6859            .router
6860            .inbox_sender()
6861            .send_classified(InboxItem::External { envelope })
6862            .into_result()
6863            .unwrap();
6864
6865        {
6866            let inbox = runtime.inbox.lock().await;
6867            assert_eq!(
6868                inbox
6869                    .classified_snapshot()
6870                    .expect("classified snapshot should exist")
6871                    .total_count,
6872                1
6873            );
6874            assert_eq!(
6875                inbox.peer_authority_test_snapshot(),
6876                Some((PeerIngressState::Received, 1))
6877            );
6878        }
6879
6880        let interactions = runtime.drain_classified_inbox_interactions().await.unwrap();
6881        assert_eq!(interactions.len(), 1);
6882
6883        {
6884            let inbox = runtime.inbox.lock().await;
6885            assert_eq!(
6886                inbox.peer_authority_test_snapshot(),
6887                Some((PeerIngressState::Delivered, 0))
6888            );
6889        }
6890    }
6891
6892    #[test]
6893    fn test_interaction_subscriber_correlation_miss_returns_none() {
6894        let runtime = CommsRuntime::inproc_only("corr-miss").unwrap();
6895        let random = meerkat_core::InteractionId(Uuid::new_v4());
6896        let sender = CoreCommsRuntime::interaction_subscriber(&runtime, &random);
6897        assert!(sender.is_none());
6898    }
6899
6900    #[tokio::test]
6901    async fn test_core_send_input_no_reservation() {
6902        let suffix = Uuid::new_v4().simple().to_string();
6903        let runtime = inproc_only_with_test_peer_authority(&format!("input-nores-{suffix}"));
6904
6905        let cmd = CommsCommand::Input {
6906            blocks: None,
6907            session_id: SessionId::new(),
6908            body: "standalone test input".to_string(),
6909            handling_mode: meerkat_core::types::HandlingMode::Queue,
6910            source: InputSource::Rpc,
6911            stream: InputStreamMode::None,
6912            allow_self_session: true,
6913        };
6914
6915        let receipt = CoreCommsRuntime::send(&runtime, cmd).await;
6916        assert!(receipt.is_ok(), "send should succeed: {receipt:?}");
6917
6918        match receipt.unwrap() {
6919            SendReceipt::InputAccepted {
6920                interaction_id: _,
6921                stream_reserved,
6922            } => assert!(!stream_reserved),
6923            other => panic!("Expected InputAccepted, got: {other:?}"),
6924        }
6925
6926        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
6927        assert_eq!(interactions.len(), 1);
6928        assert!(matches!(
6929            &interactions[0].content,
6930            meerkat_core::InteractionContent::Message { body, .. } if body == "standalone test input"
6931        ));
6932    }
6933
6934    /// ROW #269 gate: a non-stream comms `Input` returns an `InteractionId`
6935    /// that matches the `interaction_id` stamped on the injected event
6936    /// (correlatable), not a fresh unrelated UUID.
6937    #[tokio::test]
6938    async fn test_core_send_input_no_reservation_returns_correlated_id() {
6939        let suffix = Uuid::new_v4().simple().to_string();
6940        let runtime = inproc_only_with_test_peer_authority(&format!("input-corr-{suffix}"));
6941
6942        let cmd = CommsCommand::Input {
6943            blocks: None,
6944            session_id: SessionId::new(),
6945            body: "correlated input".to_string(),
6946            handling_mode: meerkat_core::types::HandlingMode::Queue,
6947            source: InputSource::Rpc,
6948            stream: InputStreamMode::None,
6949            allow_self_session: true,
6950        };
6951
6952        let returned_id = match CoreCommsRuntime::send(&runtime, cmd).await.unwrap() {
6953            SendReceipt::InputAccepted {
6954                interaction_id,
6955                stream_reserved,
6956            } => {
6957                assert!(!stream_reserved);
6958                interaction_id
6959            }
6960            other => panic!("Expected InputAccepted, got: {other:?}"),
6961        };
6962
6963        let interactions = CoreCommsRuntime::drain_inbox_interactions(&runtime).await;
6964        assert_eq!(interactions.len(), 1);
6965        // The injected event's interaction id is the SAME as the returned
6966        // handle — the handle correlates with the event, not a fresh UUID.
6967        assert_eq!(
6968            interactions[0].id, returned_id,
6969            "returned InteractionId must match the injected event's interaction id"
6970        );
6971    }
6972
6973    #[tokio::test]
6974    async fn test_core_send_input_reserves_stream() {
6975        let suffix = Uuid::new_v4().simple().to_string();
6976        let runtime = inproc_only_with_test_peer_authority(&format!("input-res-{suffix}"));
6977
6978        let cmd = CommsCommand::Input {
6979            blocks: None,
6980            session_id: SessionId::new(),
6981            body: "streaming input".to_string(),
6982            handling_mode: meerkat_core::types::HandlingMode::Queue,
6983            source: InputSource::Rpc,
6984            stream: InputStreamMode::ReserveInteraction,
6985            allow_self_session: true,
6986        };
6987
6988        let receipt = CoreCommsRuntime::send(&runtime, cmd).await.unwrap();
6989        let (interaction_id, reserved) = match receipt {
6990            SendReceipt::InputAccepted {
6991                interaction_id,
6992                stream_reserved,
6993            } => (interaction_id, stream_reserved),
6994            other => panic!("Expected InputAccepted, got: {other:?}"),
6995        };
6996
6997        assert!(reserved);
6998
6999        let sender = runtime
7000            .take_interaction_stream_sender(&interaction_id)
7001            .expect("reserved interaction should be registered");
7002        drop(sender);
7003
7004        assert!(
7005            runtime
7006                .take_interaction_stream_sender(&interaction_id)
7007                .is_none(),
7008            "interaction registration must be one-shot"
7009        );
7010    }
7011
7012    #[tokio::test]
7013    async fn test_core_stream_attachment_duplicate_attach_fails() {
7014        let suffix = Uuid::new_v4().simple().to_string();
7015        let runtime = inproc_only_with_test_peer_authority(&format!("dup-attach-{suffix}"));
7016
7017        let cmd = CommsCommand::Input {
7018            blocks: None,
7019            session_id: SessionId::new(),
7020            body: "duplicate stream test".to_string(),
7021            handling_mode: meerkat_core::types::HandlingMode::Queue,
7022            source: InputSource::Rpc,
7023            stream: InputStreamMode::ReserveInteraction,
7024            allow_self_session: true,
7025        };
7026
7027        let interaction_id = match CoreCommsRuntime::send(&runtime, cmd).await.unwrap() {
7028            SendReceipt::InputAccepted {
7029                interaction_id,
7030                stream_reserved,
7031            } => {
7032                assert!(stream_reserved);
7033                interaction_id
7034            }
7035            other => panic!("Expected InputAccepted, got: {other:?}"),
7036        };
7037
7038        let stream = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id))
7039            .expect("first attach should succeed");
7040
7041        let dup = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id));
7042        assert!(matches!(dup, Err(StreamError::AlreadyAttached(_))));
7043
7044        drop(stream);
7045
7046        let after_drop =
7047            CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id));
7048        assert!(matches!(after_drop, Err(StreamError::NotReserved(_))));
7049    }
7050
7051    #[tokio::test]
7052    async fn test_core_stream_not_reserved_before_send() {
7053        let suffix = Uuid::new_v4().simple().to_string();
7054        let runtime = inproc_only_with_test_peer_authority(&format!("pre-send-{suffix}"));
7055        let random = InteractionId(Uuid::new_v4());
7056
7057        let missing = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(random));
7058        assert!(matches!(missing, Err(StreamError::NotReserved(_))));
7059
7060        let cmd = CommsCommand::Input {
7061            blocks: None,
7062            session_id: SessionId::new(),
7063            body: "send before stream attach".to_string(),
7064            handling_mode: meerkat_core::types::HandlingMode::Queue,
7065            source: InputSource::Rpc,
7066            stream: InputStreamMode::ReserveInteraction,
7067            allow_self_session: true,
7068        };
7069        let interaction_id = match CoreCommsRuntime::send(&runtime, cmd).await.unwrap() {
7070            SendReceipt::InputAccepted {
7071                interaction_id,
7072                stream_reserved,
7073            } => {
7074                assert!(stream_reserved);
7075                interaction_id
7076            }
7077            other => panic!("Expected InputAccepted, got: {other:?}"),
7078        };
7079
7080        let _stream = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id))
7081            .expect("should attach after send");
7082    }
7083
7084    #[tokio::test]
7085    async fn test_core_send_and_stream_input_returns_stream_and_receipt() {
7086        let suffix = Uuid::new_v4().simple().to_string();
7087        let runtime = inproc_only_with_test_peer_authority(&format!("sas-{suffix}"));
7088
7089        let cmd = CommsCommand::Input {
7090            blocks: None,
7091            session_id: SessionId::new(),
7092            body: "stream-first".to_string(),
7093            handling_mode: meerkat_core::types::HandlingMode::Queue,
7094            source: InputSource::Rpc,
7095            stream: InputStreamMode::ReserveInteraction,
7096            allow_self_session: true,
7097        };
7098
7099        let (receipt, _stream) = CoreCommsRuntime::send_and_stream(&runtime, cmd)
7100            .await
7101            .unwrap();
7102
7103        let interaction_id = match receipt {
7104            SendReceipt::InputAccepted {
7105                interaction_id,
7106                stream_reserved,
7107            } => {
7108                assert!(stream_reserved);
7109                interaction_id
7110            }
7111            other => panic!("Expected InputAccepted, got: {other:?}"),
7112        };
7113
7114        let dup = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id));
7115        assert!(matches!(dup, Err(StreamError::AlreadyAttached(_))));
7116
7117        assert!(
7118            CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(interaction_id)).is_err()
7119        );
7120    }
7121
7122    #[tokio::test]
7123    async fn test_core_send_unknown_peer_fails() {
7124        let suffix = Uuid::new_v4().simple().to_string();
7125        let runtime = CommsRuntime::inproc_only(&format!("sender-{suffix}")).unwrap();
7126
7127        let cmd = CommsCommand::PeerMessage {
7128            content_taint: None,
7129            blocks: None,
7130            to: missing_peer_route("missing-peer"),
7131            body: "hello".to_string(),
7132            handling_mode: meerkat_core::types::HandlingMode::Queue,
7133        };
7134
7135        let result = CoreCommsRuntime::send(&runtime, cmd).await;
7136        assert!(matches!(result, Err(SendError::PeerNotFound(_))));
7137    }
7138
7139    #[tokio::test]
7140    async fn test_core_send_success_and_drain() {
7141        let suffix = Uuid::new_v4().simple().to_string();
7142        let sender_name = format!("sender-{suffix}");
7143        let receiver_name = format!("receiver-{suffix}");
7144        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
7145        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
7146
7147        add_trusted_peer_with_generated_authority(
7148            &sender,
7149            trusted_descriptor(
7150                &receiver_name,
7151                receiver.public_key(),
7152                &format!("inproc://{receiver_name}"),
7153            ),
7154        )
7155        .await;
7156
7157        add_trusted_peer_with_generated_authority(
7158            &receiver,
7159            trusted_descriptor(
7160                &sender_name,
7161                sender.public_key(),
7162                &format!("inproc://{sender_name}"),
7163            ),
7164        )
7165        .await;
7166
7167        let cmd = CommsCommand::PeerMessage {
7168            content_taint: None,
7169            blocks: None,
7170            to: peer_route(&receiver_name, receiver.public_key()),
7171            body: "greeting".to_string(),
7172            handling_mode: meerkat_core::types::HandlingMode::Queue,
7173        };
7174
7175        let receipt = CoreCommsRuntime::send(&sender, cmd).await;
7176        match receipt {
7177            Ok(SendReceipt::PeerMessageSent {
7178                envelope_id: _,
7179                acked,
7180            }) => assert!(!acked),
7181            other => panic!("Expected peer message receipt, got: {other:?}"),
7182        }
7183
7184        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
7185        assert_eq!(interactions.len(), 1);
7186        assert!(matches!(
7187            &interactions[0].content,
7188            meerkat_core::InteractionContent::Message { body, .. } if body == "greeting"
7189        ));
7190    }
7191
7192    #[tokio::test]
7193    async fn test_core_send_hydrates_blob_refs_before_transport() {
7194        let suffix = Uuid::new_v4().simple().to_string();
7195        let sender_name = format!("sender-blob-{suffix}");
7196        let receiver_name = format!("receiver-blob-{suffix}");
7197        let mut sender = CommsRuntime::inproc_only(&sender_name).unwrap();
7198        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
7199        let blob_store: Arc<dyn BlobStore> = Arc::new(TestBlobStore::default());
7200        let blob_ref = blob_store
7201            .put_image("image/png", "aGVsbG8=")
7202            .await
7203            .expect("blob stored");
7204        sender.set_blob_store(blob_store);
7205
7206        add_trusted_peer_with_generated_authority(
7207            &sender,
7208            trusted_descriptor(
7209                &receiver_name,
7210                receiver.public_key(),
7211                &format!("inproc://{receiver_name}"),
7212            ),
7213        )
7214        .await;
7215
7216        add_trusted_peer_with_generated_authority(
7217            &receiver,
7218            trusted_descriptor(
7219                &sender_name,
7220                sender.public_key(),
7221                &format!("inproc://{sender_name}"),
7222            ),
7223        )
7224        .await;
7225
7226        let cmd = CommsCommand::PeerMessage {
7227            content_taint: None,
7228            blocks: Some(vec![ContentBlock::Image {
7229                media_type: "image/png".to_string(),
7230                data: ImageData::Blob {
7231                    blob_id: blob_ref.blob_id,
7232                },
7233            }]),
7234            to: peer_route(&receiver_name, receiver.public_key()),
7235            body: "blob-backed image".to_string(),
7236            handling_mode: meerkat_core::types::HandlingMode::Queue,
7237        };
7238
7239        let receipt = CoreCommsRuntime::send(&sender, cmd).await;
7240        assert!(matches!(receipt, Ok(SendReceipt::PeerMessageSent { .. })));
7241
7242        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
7243        assert_eq!(interactions.len(), 1);
7244        match &interactions[0].content {
7245            meerkat_core::InteractionContent::Message { blocks, .. } => {
7246                let blocks = blocks.as_ref().expect("received blocks");
7247                assert!(matches!(
7248                    &blocks[0],
7249                    ContentBlock::Image {
7250                        data: ImageData::Inline { data },
7251                        ..
7252                    } if data == "aGVsbG8="
7253                ));
7254            }
7255            other => panic!("expected message interaction, got {other:?}"),
7256        }
7257    }
7258
7259    #[tokio::test]
7260    async fn test_core_send_hydrates_blob_refs_on_peer_request_before_transport() {
7261        let suffix = Uuid::new_v4().simple().to_string();
7262        let sender_name = format!("sender-request-blob-{suffix}");
7263        let receiver_name = format!("receiver-request-blob-{suffix}");
7264        let mut sender_runtime = CommsRuntime::inproc_only(&sender_name).unwrap();
7265        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
7266        let blob_store: Arc<dyn BlobStore> = Arc::new(TestBlobStore::default());
7267        let blob_ref = blob_store
7268            .put_image("image/png", "aGVsbG8=")
7269            .await
7270            .expect("blob stored");
7271        sender_runtime.set_blob_store(blob_store);
7272        let sender = Arc::new(sender_runtime);
7273        install_test_peer_request_response_authority(&sender);
7274
7275        add_trusted_peer_with_generated_authority(
7276            sender.as_ref(),
7277            trusted_descriptor(
7278                &receiver_name,
7279                receiver.public_key(),
7280                &format!("inproc://{receiver_name}"),
7281            ),
7282        )
7283        .await;
7284
7285        add_trusted_peer_with_generated_authority(
7286            &receiver,
7287            trusted_descriptor(
7288                &sender_name,
7289                sender.public_key(),
7290                &format!("inproc://{sender_name}"),
7291            ),
7292        )
7293        .await;
7294
7295        let cmd = CommsCommand::PeerRequest {
7296            content_taint: None,
7297            blocks: Some(vec![ContentBlock::Image {
7298                media_type: "image/png".to_string(),
7299                data: ImageData::Blob {
7300                    blob_id: blob_ref.blob_id,
7301                },
7302            }]),
7303            to: peer_route(&receiver_name, receiver.public_key()),
7304            intent: "checksum".to_string(),
7305            params: serde_json::json!({"subject": "image"}),
7306            handling_mode: meerkat_core::types::HandlingMode::Queue,
7307            stream: InputStreamMode::ReserveInteraction,
7308        };
7309
7310        let receipt = CoreCommsRuntime::send(sender.as_ref(), cmd).await;
7311        assert!(matches!(receipt, Ok(SendReceipt::PeerRequestSent { .. })));
7312
7313        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
7314        assert_eq!(interactions.len(), 1);
7315        match &interactions[0].content {
7316            meerkat_core::InteractionContent::Request { blocks, .. } => {
7317                let blocks = blocks.as_ref().expect("received blocks");
7318                assert!(matches!(
7319                    &blocks[0],
7320                    ContentBlock::Image {
7321                        data: ImageData::Inline { data },
7322                        ..
7323                    } if data == "aGVsbG8="
7324                ));
7325            }
7326            other => panic!("expected request interaction, got {other:?}"),
7327        }
7328    }
7329
7330    #[tokio::test]
7331    async fn test_core_send_hydrates_blob_refs_on_peer_response_before_transport() {
7332        let suffix = Uuid::new_v4().simple().to_string();
7333        let sender_name = format!("sender-response-blob-{suffix}");
7334        let receiver_name = format!("receiver-response-blob-{suffix}");
7335        let mut sender_runtime = CommsRuntime::inproc_only(&sender_name).unwrap();
7336        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
7337        let blob_store: Arc<dyn BlobStore> = Arc::new(TestBlobStore::default());
7338        let blob_ref = blob_store
7339            .put_image("image/png", "aGVsbG8=")
7340            .await
7341            .expect("blob stored");
7342        sender_runtime.set_blob_store(blob_store);
7343        let sender = Arc::new(sender_runtime);
7344        let peer_handle = Arc::new(TestPeerInteractionHandle::default());
7345        sender.install_peer_request_response_authority(PeerRequestResponseAuthority::new(
7346            peer_handle.clone(),
7347            Arc::new(TestInteractionStreamHandle::default()),
7348        ));
7349
7350        add_trusted_peer_with_generated_authority(
7351            sender.as_ref(),
7352            trusted_descriptor(
7353                &receiver_name,
7354                receiver.public_key(),
7355                &format!("inproc://{receiver_name}"),
7356            ),
7357        )
7358        .await;
7359
7360        add_trusted_peer_with_generated_authority(
7361            &receiver,
7362            trusted_descriptor(
7363                &sender_name,
7364                sender.public_key(),
7365                &format!("inproc://{sender_name}"),
7366            ),
7367        )
7368        .await;
7369
7370        let interaction_id = Uuid::new_v4();
7371        let corr_id = meerkat_core::PeerCorrelationId::from_uuid(interaction_id);
7372        meerkat_core::handles::PeerInteractionHandle::request_received(
7373            peer_handle.as_ref(),
7374            corr_id,
7375            meerkat_core::types::HandlingMode::Queue,
7376        )
7377        .expect("seed inbound request state");
7378
7379        let cmd = CommsCommand::PeerResponse {
7380            content_taint: None,
7381            blocks: Some(vec![ContentBlock::Image {
7382                media_type: "image/png".to_string(),
7383                data: ImageData::Blob {
7384                    blob_id: blob_ref.blob_id,
7385                },
7386            }]),
7387            to: peer_route(&receiver_name, receiver.public_key()),
7388            in_reply_to: InteractionId(interaction_id),
7389            status: meerkat_core::ResponseStatus::Completed,
7390            result: serde_json::json!({"ok": true}),
7391            handling_mode: Some(meerkat_core::types::HandlingMode::Queue),
7392        };
7393
7394        let receipt = CoreCommsRuntime::send(sender.as_ref(), cmd).await;
7395        assert!(matches!(receipt, Ok(SendReceipt::PeerResponseSent { .. })));
7396
7397        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
7398        assert_eq!(interactions.len(), 1);
7399        match &interactions[0].content {
7400            meerkat_core::InteractionContent::Response { blocks, .. } => {
7401                let blocks = blocks.as_ref().expect("received blocks");
7402                assert!(matches!(
7403                    &blocks[0],
7404                    ContentBlock::Image {
7405                        data: ImageData::Inline { data },
7406                        ..
7407                    } if data == "aGVsbG8="
7408                ));
7409            }
7410            other => panic!("expected response interaction, got {other:?}"),
7411        }
7412    }
7413
7414    #[tokio::test]
7415    async fn test_core_send_fails_without_trusted_entry() {
7416        let suffix = Uuid::new_v4().simple().to_string();
7417        let sender_name = format!("sender-ipc-{suffix}");
7418        let receiver_name = format!("receiver-ipc-{suffix}");
7419        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
7420        let receiver = CommsRuntime::inproc_only(&receiver_name).unwrap();
7421
7422        let cmd = CommsCommand::PeerMessage {
7423            content_taint: None,
7424            blocks: None,
7425            to: peer_route(&receiver_name, receiver.public_key()),
7426            body: "inproc-only hello".to_string(),
7427            handling_mode: meerkat_core::types::HandlingMode::Queue,
7428        };
7429
7430        let receipt = CoreCommsRuntime::send(&sender, cmd).await;
7431        match receipt {
7432            Err(SendError::PeerNotFound(_)) => {}
7433            other => panic!("Expected peer-not-found for missing trust, got: {other:?}"),
7434        }
7435
7436        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
7437        assert_eq!(interactions.len(), 0);
7438    }
7439
7440    #[tokio::test]
7441    async fn test_core_send_requires_generated_trust_when_auth_disabled() {
7442        let suffix = Uuid::new_v4().simple().to_string();
7443        let sender_name = format!("sender-ipc-no-auth-{suffix}");
7444        let receiver_name = format!("receiver-ipc-no-auth-{suffix}");
7445        let sender_tmp = tempfile::tempdir().unwrap();
7446        let receiver_tmp = tempfile::tempdir().unwrap();
7447
7448        let sender_config = ResolvedCommsConfig {
7449            enabled: true,
7450            name: sender_name.clone(),
7451            inproc_namespace: None,
7452            identity_dir: sender_tmp.path().join("identity"),
7453            trusted_peers_path: sender_tmp.path().join("trusted_peers.json"),
7454            comms_config: crate::CommsConfig::default(),
7455            auth: meerkat_core::CommsAuthMode::Open,
7456            require_peer_auth: false,
7457            listen_uds: None,
7458            listen_tcp: None,
7459            advertise_address: None,
7460            event_listen_tcp: None,
7461            #[cfg(unix)]
7462            event_listen_uds: None,
7463            allow_external_unauthenticated: false,
7464            pairing_password: None,
7465        };
7466
7467        let receiver_config = ResolvedCommsConfig {
7468            enabled: true,
7469            name: receiver_name.clone(),
7470            inproc_namespace: None,
7471            identity_dir: receiver_tmp.path().join("identity"),
7472            trusted_peers_path: receiver_tmp.path().join("trusted_peers.json"),
7473            comms_config: crate::CommsConfig::default(),
7474            auth: meerkat_core::CommsAuthMode::Open,
7475            require_peer_auth: false,
7476            listen_uds: None,
7477            listen_tcp: None,
7478            advertise_address: None,
7479            event_listen_tcp: None,
7480            #[cfg(unix)]
7481            event_listen_uds: None,
7482            allow_external_unauthenticated: false,
7483            pairing_password: None,
7484        };
7485
7486        let sender = CommsRuntime::new(sender_config).await.unwrap();
7487        let receiver = new_with_test_peer_authority(receiver_config).await;
7488        let sender_pubkey = sender.public_key();
7489        let receiver_pubkey = receiver.public_key();
7490
7491        let cmd = CommsCommand::PeerMessage {
7492            content_taint: None,
7493            blocks: None,
7494            to: peer_route(&receiver_name, receiver.public_key()),
7495            body: "hello without trusted".to_string(),
7496            handling_mode: meerkat_core::types::HandlingMode::Queue,
7497        };
7498
7499        let expected_peer_id = receiver.public_key().to_peer_id().to_string();
7500        let receipt = CoreCommsRuntime::send(&sender, cmd).await;
7501        match receipt {
7502            Err(SendError::PeerNotFound(peer_id)) if peer_id == expected_peer_id => {}
7503            other => panic!("Expected peer-not-found without generated trust, got: {other:?}"),
7504        }
7505
7506        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
7507        assert_eq!(interactions.len(), 0);
7508
7509        assert!(crate::InprocRegistry::global().unregister(&sender_pubkey));
7510        assert!(crate::InprocRegistry::global().unregister(&receiver_pubkey));
7511    }
7512
7513    #[tokio::test]
7514    async fn test_core_peers_excludes_inproc_without_generated_trust_when_auth_disabled() {
7515        let suffix = Uuid::new_v4().simple().to_string();
7516        let runtime_name = format!("directory-no-auth-runtime-{suffix}");
7517        let peer_name = format!("directory-no-auth-peer-{suffix}");
7518        let runtime_tmp = tempfile::tempdir().unwrap();
7519        let peer_tmp = tempfile::tempdir().unwrap();
7520
7521        let runtime_config = ResolvedCommsConfig {
7522            enabled: true,
7523            name: runtime_name.clone(),
7524            inproc_namespace: None,
7525            identity_dir: runtime_tmp.path().join("identity"),
7526            trusted_peers_path: runtime_tmp.path().join("trusted_peers.json"),
7527            comms_config: crate::CommsConfig::default(),
7528            auth: meerkat_core::CommsAuthMode::Open,
7529            require_peer_auth: false,
7530            listen_uds: None,
7531            listen_tcp: None,
7532            event_listen_tcp: None,
7533            #[cfg(unix)]
7534            event_listen_uds: None,
7535            allow_external_unauthenticated: false,
7536            advertise_address: None,
7537            pairing_password: None,
7538        };
7539        let peer_config = ResolvedCommsConfig {
7540            enabled: true,
7541            name: peer_name.clone(),
7542            inproc_namespace: None,
7543            identity_dir: peer_tmp.path().join("identity"),
7544            trusted_peers_path: peer_tmp.path().join("trusted_peers.json"),
7545            comms_config: crate::CommsConfig::default(),
7546            auth: meerkat_core::CommsAuthMode::Open,
7547            require_peer_auth: false,
7548            listen_uds: None,
7549            listen_tcp: None,
7550            event_listen_tcp: None,
7551            #[cfg(unix)]
7552            event_listen_uds: None,
7553            allow_external_unauthenticated: false,
7554            advertise_address: None,
7555            pairing_password: None,
7556        };
7557
7558        let runtime = CommsRuntime::new(runtime_config).await.unwrap();
7559        let peer = CommsRuntime::new(peer_config).await.unwrap();
7560        let runtime_pubkey = runtime.public_key();
7561        let peer_pubkey = peer.public_key();
7562
7563        let peers = CoreCommsRuntime::peers(&runtime).await;
7564        assert!(
7565            peers.iter().all(|entry| entry.name.as_str() != peer_name),
7566            "inproc registry membership must not enter the peer directory without generated trust"
7567        );
7568
7569        assert!(crate::InprocRegistry::global().unregister(&runtime_pubkey));
7570        assert!(crate::InprocRegistry::global().unregister(&peer_pubkey));
7571    }
7572
7573    #[tokio::test]
7574    async fn test_add_trusted_peer_updates_peers_and_enables_send() {
7575        let suffix = Uuid::new_v4().simple().to_string();
7576        let sender_name = format!("trust-sender-{suffix}");
7577        let receiver_name = format!("trust-receiver-{suffix}");
7578        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
7579        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
7580
7581        let peer_spec = trusted_descriptor(
7582            &receiver_name,
7583            receiver.public_key(),
7584            &format!("inproc://{receiver_name}"),
7585        );
7586
7587        add_trusted_peer_with_generated_authority(&sender, peer_spec).await;
7588
7589        let reverse_spec = trusted_descriptor(
7590            &sender_name,
7591            sender.public_key(),
7592            &format!("inproc://{sender_name}"),
7593        );
7594
7595        add_trusted_peer_with_generated_authority(&receiver, reverse_spec).await;
7596
7597        let peers = CoreCommsRuntime::peers(&sender).await;
7598        let receiver_entries: Vec<_> = peers
7599            .iter()
7600            .filter(|entry| entry.name.as_str() == receiver_name)
7601            .collect();
7602        assert_eq!(receiver_entries.len(), 1, "peer should appear in peers()");
7603
7604        let send_cmd = CommsCommand::PeerMessage {
7605            content_taint: None,
7606            blocks: None,
7607            to: peer_route(&receiver_name, receiver.public_key()),
7608            body: "hello trusted peer".to_string(),
7609            handling_mode: meerkat_core::types::HandlingMode::Queue,
7610        };
7611        let receipt = CoreCommsRuntime::send(&sender, send_cmd).await;
7612        assert!(matches!(receipt, Ok(SendReceipt::PeerMessageSent { .. })));
7613
7614        let interactions = CoreCommsRuntime::drain_inbox_interactions(&receiver).await;
7615        assert_eq!(interactions.len(), 1);
7616        assert!(matches!(
7617            &interactions[0].content,
7618            meerkat_core::InteractionContent::Message { body, .. } if body == "hello trusted peer"
7619        ));
7620    }
7621
7622    #[tokio::test]
7623    async fn test_add_trusted_peer_invalid_peer_id_is_rejected() {
7624        let sender = CommsRuntime::inproc_only("trust-invalid-sender").unwrap();
7625        // Construct a descriptor whose `peer_id` does not match the supplied
7626        // `pubkey`; generated peer-projection authority refuses to package a
7627        // trust mutation for a drifting descriptor.
7628        let bogus_peer_key = Keypair::generate();
7629        let real_peer_key = Keypair::generate();
7630        let invalid_peer_spec = meerkat_core::comms::TrustedPeerDescriptor {
7631            name: meerkat_core::comms::PeerName::new("invalid").expect("valid peer name"),
7632            peer_id: bogus_peer_key.public_key().to_peer_id(),
7633            address: meerkat_core::comms::PeerAddress::new(
7634                meerkat_core::comms::PeerTransport::Inproc,
7635                "invalid",
7636            ),
7637            pubkey: *real_peer_key.public_key().as_bytes(),
7638        };
7639
7640        let result =
7641            try_add_trusted_peer_with_generated_authority(&sender, invalid_peer_spec).await;
7642        assert!(matches!(result, Err(SendError::Validation(_))));
7643    }
7644
7645    #[tokio::test]
7646    async fn test_runtime_startup_rejects_persisted_zero_pubkey_trust() {
7647        let tmp = tempfile::TempDir::new().unwrap();
7648        let config = test_runtime_config("persisted-zero-pubkey", &tmp);
7649        tokio::fs::write(
7650            &config.trusted_peers_path,
7651            serde_json::json!({
7652                "peers": [{
7653                    "name": "persisted-zero-peer",
7654                    "pubkey": PubKey::new([0u8; 32]).to_pubkey_string(),
7655                    "addr": "inproc://persisted-zero-peer"
7656                }]
7657            })
7658            .to_string(),
7659        )
7660        .await
7661        .unwrap();
7662
7663        let error = match CommsRuntime::new(config).await {
7664            Ok(_) => panic!("runtime startup must reject persisted zero-pubkey trust"),
7665            Err(error) => error,
7666        };
7667
7668        match error {
7669            CommsRuntimeError::TrustLoadError(message) => {
7670                assert!(
7671                    message.contains("pubkey") && message.contains("non-zero"),
7672                    "unexpected trust-load error: {message}"
7673                );
7674            }
7675            other => panic!("unexpected runtime startup error: {other:?}"),
7676        }
7677    }
7678
7679    #[tokio::test]
7680    async fn test_runtime_startup_rejects_persisted_trusted_peer_seed() {
7681        let tmp = tempfile::TempDir::new().unwrap();
7682        let config = test_runtime_config("persisted-trust-seed", &tmp);
7683        let peer_key = Keypair::generate().public_key();
7684        tokio::fs::write(
7685            &config.trusted_peers_path,
7686            serde_json::json!({
7687                "peers": [{
7688                    "name": "persisted-peer",
7689                    "pubkey": peer_key.to_pubkey_string(),
7690                    "addr": "inproc://persisted-peer"
7691                }]
7692            })
7693            .to_string(),
7694        )
7695        .await
7696        .unwrap();
7697
7698        let error = match CommsRuntime::new(config).await {
7699            Ok(_) => panic!("runtime startup must reject persisted trust seeds"),
7700            Err(error) => error,
7701        };
7702
7703        match error {
7704            CommsRuntimeError::TrustLoadError(message) => {
7705                assert!(
7706                    message.contains("generated comms trust mutation authority"),
7707                    "unexpected trust-load error: {message}"
7708                );
7709            }
7710            other => panic!("unexpected runtime startup error: {other:?}"),
7711        }
7712    }
7713
7714    #[test]
7715    fn test_core_runtime_public_key_is_exposed_via_trait() {
7716        let runtime = CommsRuntime::inproc_only("pub-key-trait").unwrap();
7717        let public_key = <CommsRuntime as CoreCommsRuntime>::public_key(&runtime);
7718        assert!(
7719            public_key
7720                .as_ref()
7721                .is_some_and(|id| id.starts_with("ed25519:")),
7722            "public_key should be available and formatted as an ed25519 public key"
7723        );
7724        let public_key_bytes = <CommsRuntime as CoreCommsRuntime>::public_key_bytes(&runtime)
7725            .expect("typed public key bytes should be available");
7726        assert_ne!(public_key_bytes, [0u8; 32]);
7727        assert_eq!(
7728            <CommsRuntime as CoreCommsRuntime>::peer_id(&runtime),
7729            Some(PeerId::from_ed25519_pubkey(&public_key_bytes))
7730        );
7731    }
7732
7733    /// Build a fresh per-test session-claim handle. Each test gets its own
7734    /// registry instance so suite-scoped identity claims do not leak across
7735    /// tests run in parallel.
7736    fn test_claim_handle() -> Arc<dyn SessionClaimHandle> {
7737        Arc::new(meerkat_core::handles::DefaultSessionClaimRegistry::new())
7738    }
7739
7740    #[tokio::test]
7741    async fn test_session_scoped_inproc_identity_preserves_peer_id_for_same_session() {
7742        let tmp = tempfile::tempdir().expect("tempdir");
7743        let session_id = SessionId::new();
7744        let claim_handle = test_claim_handle();
7745        let runtime = CommsRuntime::inproc_only_session_scoped_with_silent_intents(
7746            "session-scoped-peer",
7747            None,
7748            tmp.path().join("session-identities"),
7749            &session_id,
7750            Arc::new(HashSet::new()),
7751            Arc::clone(&claim_handle),
7752        )
7753        .await
7754        .expect("create first session-scoped runtime");
7755        let first_peer_id = runtime.public_key().to_peer_id();
7756        drop(runtime);
7757
7758        let rebuilt = CommsRuntime::inproc_only_session_scoped_with_silent_intents(
7759            "session-scoped-peer",
7760            None,
7761            tmp.path().join("session-identities"),
7762            &session_id,
7763            Arc::new(HashSet::new()),
7764            claim_handle,
7765        )
7766        .await
7767        .expect("recreate session-scoped runtime");
7768        let rebuilt_peer_id = rebuilt.public_key().to_peer_id();
7769
7770        assert_eq!(
7771            rebuilt_peer_id, first_peer_id,
7772            "same session id should preserve the inproc peer_id"
7773        );
7774    }
7775
7776    #[tokio::test]
7777    async fn test_session_scoped_inproc_identity_rejects_second_live_activation() {
7778        let tmp = tempfile::tempdir().expect("tempdir");
7779        let session_id = SessionId::new();
7780        let claim_handle = test_claim_handle();
7781        let _runtime = CommsRuntime::inproc_only_session_scoped_with_silent_intents(
7782            "session-scoped-lock",
7783            None,
7784            tmp.path().join("session-identities"),
7785            &session_id,
7786            Arc::new(HashSet::new()),
7787            Arc::clone(&claim_handle),
7788        )
7789        .await
7790        .expect("create first session-scoped runtime");
7791
7792        let error = match CommsRuntime::inproc_only_session_scoped_with_silent_intents(
7793            "session-scoped-lock",
7794            None,
7795            tmp.path().join("session-identities"),
7796            &session_id,
7797            Arc::new(HashSet::new()),
7798            claim_handle,
7799        )
7800        .await
7801        {
7802            Ok(_) => panic!("second live activation should fail"),
7803            Err(error) => error,
7804        };
7805
7806        assert!(
7807            matches!(error, CommsRuntimeError::SessionIdentityInUse(_)),
7808            "unexpected error: {error:?}"
7809        );
7810    }
7811
7812    #[tokio::test]
7813    async fn test_session_scoped_inproc_identity_does_not_restore_trust_from_disk() {
7814        let tmp = tempfile::tempdir().expect("tempdir");
7815        let session_id = SessionId::new();
7816        let claim_handle = test_claim_handle();
7817        let sender = CommsRuntime::inproc_only_session_scoped_with_silent_intents(
7818            "session-scoped-trust",
7819            None,
7820            tmp.path().join("session-identities"),
7821            &session_id,
7822            Arc::new(HashSet::new()),
7823            Arc::clone(&claim_handle),
7824        )
7825        .await
7826        .expect("create session-scoped runtime");
7827        let peer = CommsRuntime::inproc_only("session-scoped-trust-peer").unwrap();
7828        add_trusted_peer_with_generated_authority(
7829            &sender,
7830            trusted_descriptor(
7831                "session-scoped-trust-peer",
7832                peer.public_key(),
7833                &format!("inproc://{}", peer.participant_name()),
7834            ),
7835        )
7836        .await;
7837        assert_eq!(
7838            sender.peers().await.len(),
7839            1,
7840            "trust should be visible while live"
7841        );
7842        drop(sender);
7843
7844        let rebuilt = CommsRuntime::inproc_only_session_scoped_with_silent_intents(
7845            "session-scoped-trust",
7846            None,
7847            tmp.path().join("session-identities"),
7848            &session_id,
7849            Arc::new(HashSet::new()),
7850            claim_handle,
7851        )
7852        .await
7853        .expect("recreate session-scoped runtime");
7854
7855        assert!(
7856            rebuilt.peers().await.is_empty(),
7857            "session-scoped identity should preserve the keypair without replaying trusted peers"
7858        );
7859    }
7860
7861    #[tokio::test]
7862    async fn test_core_peers_includes_trusted_without_self() {
7863        let suffix = Uuid::new_v4().simple().to_string();
7864        let peer_name = format!("trusted-only-{suffix}");
7865        let runtime_name = format!("runtime-mixed-{suffix}");
7866
7867        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
7868        let runtime = CommsRuntime::inproc_only(&runtime_name).unwrap();
7869
7870        {
7871            let mut trusted = runtime.trusted_peers.write();
7872            trusted
7873                .upsert(test_trust_entry(
7874                    &peer_name,
7875                    peer.public_key(),
7876                    &format!("inproc://{peer_name}"),
7877                ))
7878                .expect("valid test peer should upsert");
7879        }
7880
7881        let peers = CoreCommsRuntime::peers(&runtime).await;
7882        let names: Vec<_> = peers.iter().map(|entry| entry.name.as_string()).collect();
7883        assert!(names.iter().any(|name| name == &peer_name));
7884        assert!(!names.iter().any(|name| name == &runtime_name));
7885
7886        let matched: Vec<_> = peers
7887            .iter()
7888            .filter(|entry| entry.name.as_str() == peer_name)
7889            .collect();
7890        assert_eq!(matched.len(), 1);
7891
7892        let peer = matched[0];
7893        assert_eq!(peer.source, PeerDirectorySource::Trusted);
7894        assert_eq!(peer.address.to_string(), format!("inproc://{peer_name}"));
7895        assert_eq!(peer.sendable_kinds, vec![PeerSendability::PeerMessage]);
7896    }
7897
7898    #[tokio::test]
7899    async fn test_core_peers_send_success_keeps_peer_directory_projection() {
7900        let suffix = Uuid::new_v4().simple().to_string();
7901        let sender_name = format!("reach-sender-{suffix}");
7902        let receiver_name = format!("reach-receiver-{suffix}");
7903        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
7904        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
7905
7906        add_trusted_peer_with_generated_authority(
7907            &sender,
7908            trusted_descriptor(
7909                &receiver_name,
7910                receiver.public_key(),
7911                &format!("inproc://{receiver_name}"),
7912            ),
7913        )
7914        .await;
7915        // Receiver must trust the sender too: `require_peer_auth` is on by
7916        // default, and with typed drop-reasons the receiver no longer silently
7917        // accepts untrusted envelopes as `Ok(())` — the admission gate now
7918        // reports `Dropped { UntrustedSender }`, which surfaces to this caller
7919        // as `PeerOffline`. Mutual trust mirrors real deployments.
7920        add_trusted_peer_with_generated_authority(
7921            &receiver,
7922            trusted_descriptor(
7923                &sender_name,
7924                sender.public_key(),
7925                &format!("inproc://{sender_name}"),
7926            ),
7927        )
7928        .await;
7929
7930        CoreCommsRuntime::send(
7931            &sender,
7932            CommsCommand::PeerMessage {
7933                content_taint: None,
7934                blocks: None,
7935                to: peer_route(&receiver_name, receiver.public_key()),
7936                body: "hello".to_string(),
7937                handling_mode: meerkat_core::types::HandlingMode::Queue,
7938            },
7939        )
7940        .await
7941        .expect("send should succeed");
7942
7943        let peers_after = CoreCommsRuntime::peers(&sender).await;
7944        let after = peers_after
7945            .iter()
7946            .find(|entry| entry.name.as_str() == receiver_name)
7947            .expect("receiver should still be listed");
7948        assert_eq!(after.peer_id, receiver.public_key().to_peer_id());
7949    }
7950
7951    #[tokio::test]
7952    async fn test_core_peers_transport_failure_keeps_directory_projection() {
7953        let suffix = Uuid::new_v4().simple().to_string();
7954        let sender = CommsRuntime::inproc_only(&format!("transport-sender-{suffix}")).unwrap();
7955        let peer_name = format!("transport-peer-{suffix}");
7956        let peer_pubkey = Keypair::generate().public_key();
7957
7958        add_trusted_peer_with_generated_authority(
7959            &sender,
7960            trusted_descriptor(&peer_name, peer_pubkey, "tcp://127.0.0.1:9"),
7961        )
7962        .await;
7963
7964        let result = CoreCommsRuntime::send(
7965            &sender,
7966            CommsCommand::PeerMessage {
7967                content_taint: None,
7968                blocks: None,
7969                to: peer_route(&peer_name, peer_pubkey),
7970                body: "hello".to_string(),
7971                handling_mode: meerkat_core::types::HandlingMode::Queue,
7972            },
7973        )
7974        .await;
7975        assert!(
7976            matches!(result, Err(SendError::Transport(_))),
7977            "transport failure should surface as a typed transport send error, got: {result:?}"
7978        );
7979
7980        let peers = CoreCommsRuntime::peers(&sender).await;
7981        let entry = peers
7982            .iter()
7983            .find(|listed| listed.name.as_str() == peer_name)
7984            .expect("trusted peer should remain listed after transport failure");
7985        assert_eq!(entry.peer_id, peer_pubkey.to_peer_id());
7986    }
7987
7988    #[tokio::test]
7989    async fn test_send_to_untrusted_peer_surfaces_admission_dropped_not_offline() {
7990        // Regression: before typed `AdmissionOutcome`, the router collapsed a
7991        // receiver-side ingress rejection into `PeerOffline`. That made a
7992        // policy failure indistinguishable from a transport failure in
7993        // REST/RPC/MCP payloads. Now it must come through as
7994        // `SendError::AdmissionDropped { UntrustedSender }`; peer directory
7995        // liveness is intentionally not updated from this send result.
7996        let suffix = Uuid::new_v4().simple().to_string();
7997        let sender_name = format!("admit-sender-{suffix}");
7998        let receiver_name = format!("admit-receiver-{suffix}");
7999        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
8000        let receiver = inproc_only_with_test_peer_authority(&receiver_name);
8001
8002        // Sender trusts receiver; receiver does NOT trust sender. Receiver
8003        // has `require_peer_auth: true` by default, so our envelope lands
8004        // at its ingress gate and gets rejected with `UntrustedSender`.
8005        add_trusted_peer_with_generated_authority(
8006            &sender,
8007            trusted_descriptor(
8008                &receiver_name,
8009                receiver.public_key(),
8010                &format!("inproc://{receiver_name}"),
8011            ),
8012        )
8013        .await;
8014
8015        let result = CoreCommsRuntime::send(
8016            &sender,
8017            CommsCommand::PeerMessage {
8018                content_taint: None,
8019                blocks: None,
8020                to: peer_route(&receiver_name, receiver.public_key()),
8021                body: "policy-rejected".to_string(),
8022                handling_mode: meerkat_core::types::HandlingMode::Queue,
8023            },
8024        )
8025        .await;
8026        assert!(
8027            matches!(
8028                result,
8029                Err(SendError::AdmissionDropped {
8030                    reason: meerkat_core::comms::AdmissionDropReason::UntrustedSender
8031                })
8032            ),
8033            "untrusted-sender ingress drop must surface as typed AdmissionDropped, \
8034             got: {result:?}"
8035        );
8036
8037        let peers = CoreCommsRuntime::peers(&sender).await;
8038        peers
8039            .iter()
8040            .find(|listed| listed.name.as_str() == receiver_name)
8041            .expect("trusted peer should remain listed after admission drop");
8042    }
8043
8044    #[tokio::test]
8045    async fn test_unknown_send_target_does_not_create_directory_entry() {
8046        let suffix = Uuid::new_v4().simple().to_string();
8047        let sender = CommsRuntime::inproc_only(&format!("unknown-target-{suffix}")).unwrap();
8048        let missing_name = format!("missing-{suffix}");
8049
8050        let result = CoreCommsRuntime::send(
8051            &sender,
8052            CommsCommand::PeerMessage {
8053                content_taint: None,
8054                blocks: None,
8055                to: missing_peer_route(&missing_name),
8056                body: "hello".to_string(),
8057                handling_mode: meerkat_core::types::HandlingMode::Queue,
8058            },
8059        )
8060        .await;
8061        assert!(
8062            matches!(result, Err(SendError::PeerNotFound(_))),
8063            "missing peer should fail with PeerNotFound, got: {result:?}"
8064        );
8065
8066        let peers = CoreCommsRuntime::peers(&sender).await;
8067        assert!(
8068            peers
8069                .iter()
8070                .all(|entry| entry.name.as_str() != missing_name),
8071            "unknown attempted target must not become a directory entry"
8072        );
8073    }
8074
8075    #[tokio::test]
8076    async fn test_core_peers_resolved_peer_not_found_keeps_directory_projection() {
8077        let suffix = Uuid::new_v4().simple().to_string();
8078        let sender =
8079            CommsRuntime::inproc_only(&format!("resolved-missing-sender-{suffix}")).unwrap();
8080        let missing_name = format!("resolved-missing-peer-{suffix}");
8081        let missing_pubkey = Keypair::generate().public_key();
8082        let missing_peer_id = missing_pubkey.to_peer_id().as_str();
8083
8084        add_trusted_peer_with_generated_authority(
8085            &sender,
8086            trusted_descriptor(
8087                &missing_name,
8088                missing_pubkey,
8089                &format!("inproc://{missing_name}"),
8090            ),
8091        )
8092        .await;
8093
8094        let result = CoreCommsRuntime::send(
8095            &sender,
8096            CommsCommand::PeerMessage {
8097                content_taint: None,
8098                blocks: None,
8099                to: peer_route(&missing_name, missing_pubkey),
8100                body: "hello".to_string(),
8101                handling_mode: meerkat_core::types::HandlingMode::Queue,
8102            },
8103        )
8104        .await;
8105        assert!(
8106            matches!(result, Err(SendError::PeerNotFound(ref peer)) if peer == &missing_peer_id),
8107            "resolved missing peer should preserve PeerNotFound, got: {result:?}"
8108        );
8109
8110        let peers = CoreCommsRuntime::peers(&sender).await;
8111        let entry = peers
8112            .iter()
8113            .find(|listed| listed.name.as_str() == missing_name)
8114            .expect("trusted peer should remain listed after failed send");
8115        assert_eq!(entry.peer_id, missing_pubkey.to_peer_id());
8116    }
8117
8118    /// Truthfulness invariant: every peer/kind in peers() must be sendable.
8119    #[tokio::test]
8120    async fn test_m3_truthfulness_invariant_all_advertised_peers_are_sendable() {
8121        let suffix = Uuid::new_v4().simple().to_string();
8122        let peer_name = format!("truth-peer-{suffix}");
8123        let runtime_name = format!("truth-runtime-{suffix}");
8124
8125        let peer = CommsRuntime::inproc_only(&peer_name).unwrap();
8126        let runtime = CommsRuntime::inproc_only(&runtime_name).unwrap();
8127        {
8128            let mut trusted = runtime.trusted_peers.write();
8129            trusted
8130                .upsert(test_trust_entry(
8131                    &peer_name,
8132                    peer.public_key(),
8133                    &format!("inproc://{peer_name}"),
8134                ))
8135                .expect("valid test peer should upsert");
8136        }
8137
8138        let all_peers = CoreCommsRuntime::peers(&runtime).await;
8139        let peers: Vec<_> = all_peers
8140            .iter()
8141            .filter(|e| e.name.as_str() == peer_name)
8142            .collect();
8143        assert_eq!(
8144            peers.len(),
8145            1,
8146            "peer should be visible when explicitly trusted"
8147        );
8148
8149        for entry in peers {
8150            for kind in &entry.sendable_kinds {
8151                let cmd = match kind {
8152                    PeerSendability::PeerMessage => CommsCommand::PeerMessage {
8153                        content_taint: None,
8154                        blocks: None,
8155                        to: PeerRoute::with_display_name(entry.peer_id, entry.name.clone()),
8156                        body: "truthfulness test".to_string(),
8157                        handling_mode: meerkat_core::types::HandlingMode::Queue,
8158                    },
8159                    PeerSendability::PeerRequest => CommsCommand::PeerRequest {
8160                        content_taint: None,
8161                        to: PeerRoute::with_display_name(entry.peer_id, entry.name.clone()),
8162                        intent: "test".to_string(),
8163                        params: serde_json::json!({}),
8164                        blocks: None,
8165                        handling_mode: meerkat_core::types::HandlingMode::Queue,
8166                        stream: InputStreamMode::None,
8167                    },
8168                    PeerSendability::PeerResponse => CommsCommand::PeerResponse {
8169                        content_taint: None,
8170                        to: PeerRoute::with_display_name(entry.peer_id, entry.name.clone()),
8171                        in_reply_to: meerkat_core::InteractionId(Uuid::new_v4()),
8172                        status: meerkat_core::ResponseStatus::Completed,
8173                        result: serde_json::json!({}),
8174                        blocks: None,
8175                        handling_mode: None,
8176                    },
8177                };
8178                let result = CoreCommsRuntime::send(&runtime, cmd).await;
8179                assert!(
8180                    !matches!(result, Err(SendError::PeerNotFound(_))),
8181                    "peer '{}' advertised kind '{}' but send failed with PeerNotFound",
8182                    entry.name.as_str(),
8183                    kind
8184                );
8185            }
8186        }
8187    }
8188
8189    // --- M4: Reservation FSM + concurrency tests ---
8190
8191    #[tokio::test]
8192    async fn test_m4_duplicate_close_is_safe() {
8193        let suffix = Uuid::new_v4().simple().to_string();
8194        let runtime = inproc_only_with_test_peer_authority(&format!("dup-close-{suffix}"));
8195        let session_id = meerkat_core::SessionId::new();
8196
8197        let cmd = CommsCommand::Input {
8198            blocks: None,
8199            session_id: session_id.clone(),
8200            body: "hello".to_string(),
8201            handling_mode: meerkat_core::types::HandlingMode::Queue,
8202            source: meerkat_core::InputSource::Rpc,
8203            stream: InputStreamMode::ReserveInteraction,
8204            allow_self_session: true,
8205        };
8206        let receipt = CoreCommsRuntime::send(&runtime, cmd).await.unwrap();
8207        let iid = match receipt {
8208            SendReceipt::InputAccepted { interaction_id, .. } => interaction_id,
8209            _ => panic!("expected InputAccepted"),
8210        };
8211
8212        let stream = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(iid)).unwrap();
8213        // Drop stream → ClosedEarly
8214        drop(stream);
8215
8216        // Second attach after close → NotReserved (entry cleaned up)
8217        let result = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(iid));
8218        assert!(matches!(result, Err(StreamError::NotReserved(_))));
8219    }
8220
8221    #[tokio::test]
8222    async fn test_m4_mark_interaction_complete_cleans_up() {
8223        let suffix = Uuid::new_v4().simple().to_string();
8224        let runtime = inproc_only_with_test_peer_authority(&format!("complete-{suffix}"));
8225        let session_id = meerkat_core::SessionId::new();
8226
8227        let cmd = CommsCommand::Input {
8228            blocks: None,
8229            session_id: session_id.clone(),
8230            body: "hello".to_string(),
8231            handling_mode: meerkat_core::types::HandlingMode::Queue,
8232            source: meerkat_core::InputSource::Rpc,
8233            stream: InputStreamMode::ReserveInteraction,
8234            allow_self_session: true,
8235        };
8236        let receipt = CoreCommsRuntime::send(&runtime, cmd).await.unwrap();
8237        let iid = match receipt {
8238            SendReceipt::InputAccepted { interaction_id, .. } => interaction_id,
8239            _ => panic!("expected InputAccepted"),
8240        };
8241
8242        let mut stream = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(iid)).unwrap();
8243
8244        // Simulate terminal event
8245        runtime.mark_interaction_complete(iid.0);
8246
8247        let terminal = timeout(Duration::from_millis(100), stream.next())
8248            .await
8249            .expect("stream should terminate promptly after completion");
8250        assert!(terminal.is_none());
8251
8252        // Registry entry should be cleaned up
8253        let registry = runtime.interaction_stream_registry.lock();
8254        assert!(
8255            !registry.contains_key(&iid.0),
8256            "completed entry should be cleaned from registry"
8257        );
8258    }
8259
8260    #[tokio::test]
8261    async fn test_m4_reap_expired_reservations() {
8262        let suffix = Uuid::new_v4().simple().to_string();
8263        let runtime = CommsRuntime::inproc_only(&format!("reap-{suffix}")).unwrap();
8264
8265        // Manually insert an entry with an old timestamp
8266        let id = Uuid::new_v4();
8267        let (sender, receiver) = mpsc::channel::<meerkat_core::AgentEvent>(16);
8268        {
8269            let mut entry = StreamRegistryEntry::new(
8270                sender,
8271                receiver,
8272                InteractionStreamLifecycleAuthority::LocalTransportOnly,
8273            );
8274            entry.created_at = Instant::now()
8275                .checked_sub(meerkat_core::time_compat::Duration::from_secs(60))
8276                .unwrap();
8277            runtime.interaction_stream_registry.lock().insert(id, entry);
8278        }
8279
8280        runtime.reap_expired_reservations();
8281
8282        let registry = runtime.interaction_stream_registry.lock();
8283        assert!(
8284            !registry.contains_key(&id),
8285            "expired reservation should have been reaped"
8286        );
8287    }
8288
8289    // --- M5: send_and_stream partial-failure tests ---
8290
8291    #[tokio::test]
8292    async fn test_m5_send_and_stream_non_streamable_returns_stream_attach() {
8293        let suffix = Uuid::new_v4().simple().to_string();
8294        let peer_name = format!("m5-peer-{suffix}");
8295        let runtime_name = format!("m5-runtime-{suffix}");
8296
8297        let peer = inproc_only_with_test_peer_authority(&peer_name);
8298        let runtime = CommsRuntime::inproc_only(&runtime_name).unwrap();
8299        {
8300            let mut trusted = runtime.trusted_peers.write();
8301            trusted
8302                .upsert(test_trust_entry(
8303                    &peer_name,
8304                    peer.public_key(),
8305                    &format!("inproc://{peer_name}"),
8306                ))
8307                .expect("valid test peer should upsert");
8308        }
8309        // Receive side must also trust the sender after the silent-drop fix.
8310        // Use generated trust mutation so the classified queue's trust set syncs.
8311        add_trusted_peer_with_generated_authority(
8312            &peer,
8313            trusted_descriptor(
8314                &runtime_name,
8315                runtime.public_key(),
8316                &format!("inproc://{runtime_name}"),
8317            ),
8318        )
8319        .await;
8320
8321        let cmd = CommsCommand::PeerMessage {
8322            content_taint: None,
8323            blocks: None,
8324            to: peer_route(&peer_name, peer.public_key()),
8325            body: "not streamable".to_string(),
8326            handling_mode: meerkat_core::types::HandlingMode::Queue,
8327        };
8328        let result = CoreCommsRuntime::send_and_stream(&runtime, cmd).await;
8329        match result {
8330            Err(SendAndStreamError::StreamAttach { receipt, error }) => {
8331                assert!(
8332                    matches!(receipt, SendReceipt::PeerMessageSent { .. }),
8333                    "receipt should be PeerMessageSent"
8334                );
8335                assert!(
8336                    matches!(error, StreamError::NotFound(_)),
8337                    "error should be NotFound for non-streamable command"
8338                );
8339            }
8340            Err(e) => panic!("expected StreamAttach error, got send error: {e}"),
8341            Ok(_) => panic!("expected StreamAttach error, got Ok"),
8342        }
8343    }
8344
8345    #[tokio::test]
8346    async fn test_m5_send_and_stream_input_none_is_not_streamable() {
8347        let suffix = Uuid::new_v4().simple().to_string();
8348        let runtime = inproc_only_with_test_peer_authority(&format!("m5-none-{suffix}"));
8349        let session_id = meerkat_core::SessionId::new();
8350
8351        let cmd = CommsCommand::Input {
8352            blocks: None,
8353            session_id: session_id.clone(),
8354            body: "no stream".to_string(),
8355            handling_mode: meerkat_core::types::HandlingMode::Queue,
8356            source: meerkat_core::InputSource::Rpc,
8357            stream: InputStreamMode::None,
8358            allow_self_session: true,
8359        };
8360        let result = CoreCommsRuntime::send_and_stream(&runtime, cmd).await;
8361        match result {
8362            Err(SendAndStreamError::StreamAttach { receipt, error }) => {
8363                assert!(
8364                    matches!(
8365                        receipt,
8366                        SendReceipt::InputAccepted {
8367                            stream_reserved: false,
8368                            ..
8369                        }
8370                    ),
8371                    "receipt should indicate no stream reserved"
8372                );
8373                assert!(matches!(error, StreamError::NotFound(_)));
8374            }
8375            Err(e) => panic!("expected StreamAttach error, got send error: {e}"),
8376            Ok(_) => panic!("expected StreamAttach error, got Ok"),
8377        }
8378    }
8379
8380    #[tokio::test]
8381    async fn test_m4_attach_after_completed_returns_not_reserved() {
8382        let suffix = Uuid::new_v4().simple().to_string();
8383        let runtime = inproc_only_with_test_peer_authority(&format!("post-comp-{suffix}"));
8384        let session_id = meerkat_core::SessionId::new();
8385
8386        let cmd = CommsCommand::Input {
8387            blocks: None,
8388            session_id: session_id.clone(),
8389            body: "hello".to_string(),
8390            handling_mode: meerkat_core::types::HandlingMode::Queue,
8391            source: meerkat_core::InputSource::Rpc,
8392            stream: InputStreamMode::ReserveInteraction,
8393            allow_self_session: true,
8394        };
8395        let receipt = CoreCommsRuntime::send(&runtime, cmd).await.unwrap();
8396        let iid = match receipt {
8397            SendReceipt::InputAccepted { interaction_id, .. } => interaction_id,
8398            _ => panic!("expected InputAccepted"),
8399        };
8400
8401        // Attach
8402        let _stream = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(iid)).unwrap();
8403
8404        // Complete
8405        runtime.mark_interaction_complete(iid.0);
8406
8407        // Try to attach again → NotReserved
8408        let result = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(iid));
8409        assert!(
8410            matches!(result, Err(StreamError::NotReserved(_))),
8411            "attach after completed should return NotReserved"
8412        );
8413    }
8414
8415    #[tokio::test]
8416    async fn test_allow_self_session_guard_rejects_default() {
8417        let suffix = Uuid::new_v4().simple().to_string();
8418        let runtime = CommsRuntime::inproc_only(&format!("self-guard-{suffix}")).unwrap();
8419        let cmd = CommsCommand::Input {
8420            blocks: None,
8421            session_id: meerkat_core::SessionId::new(),
8422            body: "blocked".to_string(),
8423            handling_mode: meerkat_core::types::HandlingMode::Queue,
8424            source: meerkat_core::InputSource::Rpc,
8425            stream: InputStreamMode::None,
8426            allow_self_session: false,
8427        };
8428        let result = CoreCommsRuntime::send(&runtime, cmd).await;
8429        assert!(
8430            matches!(result, Err(SendError::Validation(_))),
8431            "allow_self_session=false should reject self-input"
8432        );
8433    }
8434
8435    #[tokio::test]
8436    async fn test_remove_trusted_peer_removes_from_peers_and_revokes_send() {
8437        let suffix = Uuid::new_v4().simple().to_string();
8438        let sender_name = format!("rm-sender-{suffix}");
8439        let receiver_name = format!("rm-receiver-{suffix}");
8440        let sender = CommsRuntime::inproc_only(&sender_name).unwrap();
8441        let receiver = CommsRuntime::inproc_only(&receiver_name).unwrap();
8442
8443        // Add receiver as trusted peer of sender
8444        let peer_spec = trusted_descriptor(
8445            &receiver_name,
8446            receiver.public_key(),
8447            &format!("inproc://{receiver_name}"),
8448        );
8449
8450        let trust_dsl = add_trusted_peer_with_generated_authority(&sender, peer_spec.clone()).await;
8451
8452        // Verify receiver appears in sender's peers
8453        let peers = CoreCommsRuntime::peers(&sender).await;
8454        let receiver_entries: Vec<_> = peers
8455            .iter()
8456            .filter(|entry| entry.name.as_str() == receiver_name)
8457            .collect();
8458        assert_eq!(
8459            receiver_entries.len(),
8460            1,
8461            "receiver should appear in sender's peers after add"
8462        );
8463
8464        // Remove the trusted peer by canonical PeerId.
8465        let remove_authority =
8466            test_projection_remove_authority_with_dsl(Arc::clone(&trust_dsl), &peer_spec);
8467        let removed = CoreCommsRuntime::apply_trust_mutation(
8468            &sender,
8469            CommsTrustMutation::RemoveTrustedPeer {
8470                peer_id: peer_spec.peer_id.to_string(),
8471                authority: remove_authority.authority,
8472            },
8473        )
8474        .await
8475        .expect("remove_trusted_peer should succeed");
8476        let CommsTrustMutationResult::Removed { removed } = removed else {
8477            panic!("unexpected generated trust mutation result: {removed:?}");
8478        };
8479        assert!(removed, "remove should return true for existing peer");
8480
8481        // Verify receiver no longer appears in sender's peers
8482        let peers_after = CoreCommsRuntime::peers(&sender).await;
8483        let receiver_entries_after: Vec<_> = peers_after
8484            .iter()
8485            .filter(|entry| entry.name.as_str() == receiver_name)
8486            .collect();
8487        assert!(
8488            receiver_entries_after.is_empty(),
8489            "receiver should not appear in sender's peers after removal"
8490        );
8491
8492        // Verify sending to the removed peer fails (PeerNotFound)
8493        let cmd = CommsCommand::PeerMessage {
8494            content_taint: None,
8495            blocks: None,
8496            to: peer_route(&receiver_name, receiver.public_key()),
8497            body: "should fail".to_string(),
8498            handling_mode: meerkat_core::types::HandlingMode::Queue,
8499        };
8500        let result = CoreCommsRuntime::send(&sender, cmd).await;
8501        assert!(
8502            matches!(result, Err(SendError::PeerNotFound(_))),
8503            "send to removed peer should fail with PeerNotFound, got: {result:?}"
8504        );
8505    }
8506
8507    #[tokio::test]
8508    async fn test_add_private_trusted_peer_zero_pubkey_fails_closed_without_generated_authority() {
8509        let suffix = Uuid::new_v4().simple().to_string();
8510        let runtime = CommsRuntime::inproc_only(&format!("rm-private-zero-{suffix}")).unwrap();
8511        let peer_id = PeerId::new();
8512        let peer_name = format!("private-legacy-peer-{suffix}");
8513        let spec = TrustedPeerDescriptor::test_only_unsigned_typed(
8514            peer_name.clone(),
8515            peer_id,
8516            format!("inproc://{peer_name}"),
8517        )
8518        .expect("valid zero-pubkey descriptor");
8519
8520        let result = CoreCommsRuntime::add_private_trusted_peer(&runtime, spec).await;
8521
8522        assert!(
8523            matches!(result, Err(SendError::Unsupported(ref message)) if message.contains("generated comms private trust mutation authority")),
8524            "raw private zero-pubkey add must fail closed before mutation: {result:?}"
8525        );
8526        let removed = CoreCommsRuntime::remove_private_trusted_peer(&runtime, &peer_id.to_string())
8527            .await
8528            .expect_err("raw private removal must fail closed");
8529        assert!(
8530            matches!(removed, SendError::Unsupported(ref message) if message.contains("generated comms private trust mutation authority")),
8531            "raw private removal must fail closed: {removed:?}"
8532        );
8533    }
8534
8535    #[tokio::test]
8536    async fn test_remove_trusted_peer_returns_false_for_absent_peer() {
8537        let suffix = Uuid::new_v4().simple().to_string();
8538        let sender = CommsRuntime::inproc_only(&format!("rm-absent-{suffix}")).unwrap();
8539        let other = CommsRuntime::inproc_only(&format!("rm-absent-other-{suffix}")).unwrap();
8540
8541        let peer_id = other.public_key().to_peer_id().to_string();
8542        let peer = trusted_descriptor(
8543            &format!("rm-absent-other-{suffix}"),
8544            other.public_key(),
8545            &format!("inproc://rm-absent-other-{suffix}"),
8546        );
8547        let removed = remove_trusted_peer_with_generated_authority(&sender, &peer)
8548            .await
8549            .expect("remove_trusted_peer should succeed even for absent peer");
8550        assert_eq!(peer.peer_id.to_string(), peer_id);
8551        assert!(
8552            !removed,
8553            "remove should return false for peer that was never trusted"
8554        );
8555    }
8556
8557    #[tokio::test]
8558    async fn test_remove_trusted_peer_rejects_pubkey_string_argument() {
8559        let suffix = Uuid::new_v4().simple().to_string();
8560        let sender = CommsRuntime::inproc_only(&format!("rm-pubkey-sender-{suffix}")).unwrap();
8561        let receiver = CommsRuntime::inproc_only(&format!("rm-pubkey-receiver-{suffix}")).unwrap();
8562
8563        let peer_pubkey_string = receiver.public_key().to_pubkey_string();
8564        let peer = trusted_descriptor(
8565            &format!("rm-pubkey-receiver-{suffix}"),
8566            receiver.public_key(),
8567            &format!("inproc://rm-pubkey-receiver-{suffix}"),
8568        );
8569        let remove_authority =
8570            test_projection_remove_authority(&local_descriptor_for_runtime(&sender), &peer, 0);
8571        remove_authority.install_owner(&sender);
8572        let result = CoreCommsRuntime::apply_trust_mutation(
8573            &sender,
8574            CommsTrustMutation::RemoveTrustedPeer {
8575                authority: remove_authority.authority,
8576                peer_id: peer_pubkey_string,
8577            },
8578        )
8579        .await;
8580
8581        assert!(
8582            matches!(result, Err(SendError::Validation(_))),
8583            "remove_trusted_peer should validate its argument as PeerId, got: {result:?}"
8584        );
8585    }
8586}