1#[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
55const RESERVATION_TTL: meerkat_core::time_compat::Duration =
57 meerkat_core::time_compat::Duration::from_secs(30);
58
59#[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 stream_handle: Option<Arc<dyn meerkat_core::handles::InteractionStreamHandle>>,
131 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#[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#[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#[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#[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
278fn peer_id_from_pubkey(pubkey: &crate::identity::PubKey) -> meerkat_core::comms::PeerId {
291 crate::router::peer_id_from_pubkey(pubkey)
292}
293
294#[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
301fn 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 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 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 CommsRuntime::set_outbound_content_taint(self, taint);
425 Ok(())
426 }
427
428 async fn drain_messages(&self) -> Vec<String> {
429 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 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 let _ = handle.expired(corr_id);
621 }
622 }
623 InteractionStreamLifecycleAuthority::LocalTransportOnly => {
624 self.interaction_stream_registry.lock().remove(&id);
627 }
628 }
629 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 if let Some(handle) = stream_handle.as_ref() {
660 handle.attached(corr_id).map_err(|err| {
661 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1454fn 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
1517pub 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 pub fn router(&self) -> &Arc<Router> {
1532 &self.router
1533 }
1534 pub fn trusted_peers(&self) -> &TrustedPeersView {
1536 &self.trusted_peers
1537 }
1538 pub fn trusted_peers_shared(&self) -> TrustedPeersView {
1540 self.trusted_peers.clone()
1541 }
1542 pub fn self_pubkey(&self) -> crate::identity::PubKey {
1544 self.self_pubkey
1545 }
1546 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 #[cfg(target_arch = "wasm32")]
1564 name: String,
1565 #[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 actionable_notify: Option<Arc<tokio::sync::Notify>>,
1587 blob_store: Option<Arc<dyn BlobStore>>,
1588 peer_interaction_handle:
1593 parking_lot::RwLock<Option<Arc<dyn meerkat_core::handles::PeerInteractionHandle>>>,
1594 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 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 let trusted_peers = Arc::new(parking_lot::RwLock::new(trusted_peers));
1683
1684 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 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 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 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 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 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 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 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 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 #[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 if self.config.auth == meerkat_core::CommsAuthMode::Open {
2300 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 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 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 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 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 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 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 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 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 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 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 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 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 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 self.subscriber_registry.lock().remove(&interaction_id);
2689 self.interaction_stream_registry
2690 .lock()
2691 .remove(&interaction_id);
2692 }
2693 }
2694 }
2695
2696 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 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 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 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 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 pub fn event_injector(&self) -> Arc<dyn meerkat_core::EventInjector> {
2783 Arc::new(self.event_injector_concrete())
2784 }
2785
2786 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 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 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 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 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 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 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 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 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 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 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 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#[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 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, };
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 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 #[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 #[test]
3555 fn pairing_caller_identity_is_typed_and_fails_closed() {
3556 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 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 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 #[test]
3610 fn pairing_client_messages_parse_typed_and_fail_closed() {
3611 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 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 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 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 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 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 #[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 #[tokio::test]
5121 #[ignore] 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 #[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 #[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 #[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 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 assert!(
5539 !CoreCommsRuntime::dismiss_received(&runtime),
5540 "a peer message body must never raise a runtime dismiss signal"
5541 );
5542 }
5543
5544 #[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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 #[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 #[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);
8215
8216 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 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 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 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 #[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 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 let _stream = CoreCommsRuntime::stream(&runtime, StreamScope::Interaction(iid)).unwrap();
8403
8404 runtime.mark_interaction_complete(iid.0);
8406
8407 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 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 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 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 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 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}