1use crate::event::{AgentEvent, EventEnvelope};
8use crate::interaction::{InteractionId, ResponseStatus};
9use crate::types::{ContentBlock, HandlingMode};
10use futures::Stream;
11use serde::{Deserialize, Serialize};
12use std::any::Any;
13use std::collections::BTreeMap;
14use std::pin::Pin;
15use std::sync::{
16 Arc,
17 atomic::{AtomicBool, Ordering},
18};
19use uuid::Uuid;
20
21pub const SUPERVISOR_BRIDGE_INTENT: &str = "supervisor.bridge";
28
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum CommsPeerRequestIntent {
44 #[serde(rename = "supervisor.bridge")]
45 SupervisorBridge,
46 #[serde(rename = "checksum_token")]
47 ChecksumToken,
48}
49
50impl CommsPeerRequestIntent {
51 pub const fn as_str(&self) -> &'static str {
53 match self {
54 Self::SupervisorBridge => SUPERVISOR_BRIDGE_INTENT,
55 Self::ChecksumToken => "checksum_token",
56 }
57 }
58}
59
60impl std::fmt::Display for CommsPeerRequestIntent {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(self.as_str())
63 }
64}
65
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
79pub struct PeerId(#[cfg_attr(feature = "schema", schemars(with = "String"))] pub Uuid);
80
81const PEER_ID_ED25519_PUBKEY_NAMESPACE: Uuid =
88 Uuid::from_u128(0x6d65_6572_6b61_7450_6565_7249_6430_0001);
89
90impl PeerId {
91 pub fn new() -> Self {
93 Self(crate::time_compat::new_uuid_v7())
94 }
95
96 pub const fn from_uuid(uuid: Uuid) -> Self {
98 Self(uuid)
99 }
100
101 pub fn parse(s: &str) -> Result<Self, PeerIdError> {
103 Uuid::parse_str(s)
104 .map(Self)
105 .map_err(|source| PeerIdError::Invalid {
106 input: s.to_string(),
107 source,
108 })
109 }
110
111 pub fn from_ed25519_pubkey(pubkey: &[u8; 32]) -> Self {
113 Self(uuid_v5_from_bytes(
114 &PEER_ID_ED25519_PUBKEY_NAMESPACE,
115 pubkey,
116 ))
117 }
118
119 pub fn as_str(&self) -> String {
121 self.0.to_string()
122 }
123
124 pub const fn as_uuid(&self) -> &Uuid {
126 &self.0
127 }
128}
129
130fn uuid_v5_from_bytes(namespace: &Uuid, name: &[u8]) -> Uuid {
131 let digest = sha1_digest_bytes(&[namespace.as_bytes(), name]);
132 let mut bytes = [0u8; 16];
133 bytes.copy_from_slice(&digest[..16]);
134 bytes[6] = (bytes[6] & 0x0f) | 0x50;
135 bytes[8] = (bytes[8] & 0x3f) | 0x80;
136 Uuid::from_bytes(bytes)
137}
138
139fn sha1_digest_bytes(parts: &[&[u8]]) -> [u8; 20] {
140 let total_len = parts.iter().map(|part| part.len()).sum::<usize>();
141 let mut message = Vec::with_capacity(((total_len + 9).div_ceil(64)) * 64);
142 for part in parts {
143 message.extend_from_slice(part);
144 }
145 let bit_len = (total_len as u64) * 8;
146 message.push(0x80);
147 while message.len() % 64 != 56 {
148 message.push(0);
149 }
150 message.extend_from_slice(&bit_len.to_be_bytes());
151
152 let mut h0 = 0x6745_2301u32;
153 let mut h1 = 0xefcd_ab89u32;
154 let mut h2 = 0x98ba_dcfeu32;
155 let mut h3 = 0x1032_5476u32;
156 let mut h4 = 0xc3d2_e1f0u32;
157
158 for chunk in message.chunks_exact(64) {
159 let mut schedule = [0u32; 80];
160 for (word_index, word) in schedule.iter_mut().take(16).enumerate() {
161 let offset = word_index * 4;
162 *word = u32::from_be_bytes([
163 chunk[offset],
164 chunk[offset + 1],
165 chunk[offset + 2],
166 chunk[offset + 3],
167 ]);
168 }
169 for word_index in 16..80 {
170 schedule[word_index] = (schedule[word_index - 3]
171 ^ schedule[word_index - 8]
172 ^ schedule[word_index - 14]
173 ^ schedule[word_index - 16])
174 .rotate_left(1);
175 }
176
177 let mut work_a = h0;
178 let mut work_b = h1;
179 let mut work_c = h2;
180 let mut work_d = h3;
181 let mut work_e = h4;
182
183 for (round_index, word) in schedule.iter().enumerate() {
184 let (round_function, round_constant) = match round_index {
185 0..=19 => ((work_b & work_c) | ((!work_b) & work_d), 0x5a82_7999),
186 20..=39 => (work_b ^ work_c ^ work_d, 0x6ed9_eba1),
187 40..=59 => (
188 (work_b & work_c) | (work_b & work_d) | (work_c & work_d),
189 0x8f1b_bcdc,
190 ),
191 _ => (work_b ^ work_c ^ work_d, 0xca62_c1d6),
192 };
193 let temp = work_a
194 .rotate_left(5)
195 .wrapping_add(round_function)
196 .wrapping_add(work_e)
197 .wrapping_add(round_constant)
198 .wrapping_add(*word);
199 work_e = work_d;
200 work_d = work_c;
201 work_c = work_b.rotate_left(30);
202 work_b = work_a;
203 work_a = temp;
204 }
205
206 h0 = h0.wrapping_add(work_a);
207 h1 = h1.wrapping_add(work_b);
208 h2 = h2.wrapping_add(work_c);
209 h3 = h3.wrapping_add(work_d);
210 h4 = h4.wrapping_add(work_e);
211 }
212
213 let mut digest = [0u8; 20];
214 for (offset, value) in [h0, h1, h2, h3, h4].into_iter().enumerate() {
215 digest[offset * 4..offset * 4 + 4].copy_from_slice(&value.to_be_bytes());
216 }
217 digest
218}
219
220impl Default for PeerId {
221 fn default() -> Self {
222 Self::new()
223 }
224}
225
226impl std::fmt::Display for PeerId {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 self.0.fmt(f)
229 }
230}
231
232#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
234pub enum PeerIdError {
235 #[error("invalid peer id {input:?}: {source}")]
236 Invalid {
237 input: String,
238 #[source]
239 source: uuid::Error,
240 },
241}
242
243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251#[non_exhaustive]
252pub enum PeerTransport {
253 Inproc,
255 Uds,
257 Tcp,
259}
260
261impl PeerTransport {
262 pub const fn as_scheme(&self) -> &'static str {
264 match self {
265 Self::Inproc => "inproc",
266 Self::Uds => "uds",
267 Self::Tcp => "tcp",
268 }
269 }
270}
271
272impl std::fmt::Display for PeerTransport {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.write_str(self.as_scheme())
275 }
276}
277
278#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
284#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
285pub struct PeerAddress {
286 pub transport: PeerTransport,
287 pub endpoint: String,
288}
289
290#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
292pub enum PeerAddressParseError {
293 #[error("peer address missing transport scheme: {input}")]
294 MissingTransportScheme { input: String },
295 #[error("unknown peer address transport {scheme:?} in address {input:?}")]
296 UnknownTransport { input: String, scheme: String },
297}
298
299impl PeerAddress {
300 pub fn new(transport: PeerTransport, endpoint: impl Into<String>) -> Self {
301 Self {
302 transport,
303 endpoint: endpoint.into(),
304 }
305 }
306
307 pub const fn transport(&self) -> PeerTransport {
308 self.transport
309 }
310
311 pub fn endpoint(&self) -> &str {
312 &self.endpoint
313 }
314
315 pub fn parse(raw: impl AsRef<str>) -> Result<Self, PeerAddressParseError> {
321 let raw = raw.as_ref();
322 let (scheme, endpoint) =
323 raw.split_once("://")
324 .ok_or_else(|| PeerAddressParseError::MissingTransportScheme {
325 input: raw.to_string(),
326 })?;
327 let transport = match scheme {
328 "inproc" => PeerTransport::Inproc,
329 "uds" => PeerTransport::Uds,
330 "tcp" => PeerTransport::Tcp,
331 other => {
332 return Err(PeerAddressParseError::UnknownTransport {
333 input: raw.to_string(),
334 scheme: other.to_string(),
335 });
336 }
337 };
338 Ok(Self::new(transport, endpoint))
339 }
340}
341
342impl std::fmt::Display for PeerAddress {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 write!(f, "{}://{}", self.transport.as_scheme(), self.endpoint)
345 }
346}
347
348impl std::str::FromStr for PeerAddress {
349 type Err = PeerAddressParseError;
350
351 fn from_str(s: &str) -> Result<Self, Self::Err> {
352 Self::parse(s)
353 }
354}
355
356impl TryFrom<&str> for PeerAddress {
357 type Error = PeerAddressParseError;
358
359 fn try_from(value: &str) -> Result<Self, Self::Error> {
360 Self::parse(value)
361 }
362}
363
364impl TryFrom<String> for PeerAddress {
365 type Error = PeerAddressParseError;
366
367 fn try_from(value: String) -> Result<Self, Self::Error> {
368 Self::parse(value)
369 }
370}
371
372#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
379#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
380pub struct PeerName(String);
381
382impl PeerName {
383 pub fn new(name: impl Into<String>) -> Result<Self, String> {
385 let name = name.into();
386 if name.trim().is_empty() {
387 return Err("peer name cannot be empty".to_string());
388 }
389 if name.chars().any(char::is_control) {
390 return Err("peer name cannot contain control characters".to_string());
391 }
392 Ok(Self(name))
393 }
394
395 pub fn as_str(&self) -> &str {
396 &self.0
397 }
398
399 pub fn as_string(&self) -> String {
400 self.0.clone()
401 }
402}
403
404impl AsRef<str> for PeerName {
405 fn as_ref(&self) -> &str {
406 self.as_str()
407 }
408}
409
410impl std::fmt::Display for PeerName {
411 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412 self.0.fmt(f)
413 }
414}
415
416impl From<PeerName> for String {
417 fn from(peer_name: PeerName) -> Self {
418 peer_name.0
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
428pub struct PeerRoute {
429 pub peer_id: PeerId,
430 pub display_name: Option<PeerName>,
431}
432
433impl PeerRoute {
434 pub fn new(peer_id: PeerId) -> Self {
435 Self {
436 peer_id,
437 display_name: None,
438 }
439 }
440
441 pub fn with_display_name(peer_id: PeerId, display_name: PeerName) -> Self {
442 Self {
443 peer_id,
444 display_name: Some(display_name),
445 }
446 }
447
448 pub fn label(&self) -> String {
449 self.display_name
450 .as_ref()
451 .map(PeerName::as_string)
452 .unwrap_or_else(|| self.peer_id.to_string())
453 }
454}
455
456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
460#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
461#[serde(deny_unknown_fields)]
462pub struct PeerRecipientIncarnation {
463 pub mob_id: String,
464 pub agent_identity: String,
465 pub host_id: String,
466 pub binding_generation: u64,
467 pub member_session_id: String,
468 pub generation: u64,
469 pub fence_token: u64,
470}
471
472#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484pub struct TrustedPeerDescriptor {
485 pub peer_id: PeerId,
487 pub name: PeerName,
490 pub address: PeerAddress,
493 pub pubkey: [u8; 32],
497}
498
499#[derive(Debug, Clone)]
506pub struct CommsTrustMutationAuthority {
507 source_kind: GeneratedCommsTrustAuthoritySourceKind,
508 source_epoch: u64,
509 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
510 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
511 operation: GeneratedCommsTrustAuthorityOperation,
512 peer_id: String,
513 trust_store_peer_id: Option<String>,
514 peer_descriptor: Option<TrustedPeerDescriptor>,
515 consumed: Arc<AtomicBool>,
516}
517
518#[derive(Clone)]
519pub struct GeneratedPeerCommsOwnerToken {
520 inner: Arc<dyn Any + Send + Sync>,
521}
522
523impl std::fmt::Debug for GeneratedPeerCommsOwnerToken {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 f.debug_struct("GeneratedPeerCommsOwnerToken").finish()
526 }
527}
528
529impl GeneratedPeerCommsOwnerToken {
530 #[cfg_attr(
531 any(test, not(meerkat_internal_generated_authority_bridge)),
532 allow(dead_code)
533 )]
534 pub(crate) fn from_generated_owner_token(inner: Arc<dyn Any + Send + Sync>) -> Self {
535 Self { inner }
536 }
537
538 pub fn same_owner(&self, other: &Self) -> bool {
539 Arc::ptr_eq(&self.inner, &other.inner)
540 }
541
542 fn matches_raw_owner(&self, other: &Arc<dyn Any + Send + Sync>) -> bool {
543 Arc::ptr_eq(&self.inner, other)
544 }
545}
546
547#[doc(hidden)]
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
549pub enum GeneratedCommsTrustAuthoritySourceKind {
550 MeerkatMachinePeerProjection,
551 MeerkatMachineSupervisorPublish,
552 MeerkatMachineSupervisorRevoke,
553 MobMachineMemberTrustWiring,
554 MobMachineMemberTrustUnwiring,
555 MobMachineExternalPeerTrustWiring,
556 MobMachineExternalPeerTrustUnwiring,
557 MobMachineExternalPeerTrustRepair,
558 MobMachineExternalPeerReciprocalTrust,
559}
560
561#[doc(hidden)]
562#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
563pub enum GeneratedCommsTrustAuthorityOperation {
564 PublicAdd,
565 PublicRemove,
566 PrivateAdd,
567 PrivateRemove,
568}
569
570impl CommsTrustMutationAuthority {
571 #[cfg_attr(not(meerkat_internal_generated_authority_bridge), allow(dead_code))]
572 #[allow(clippy::too_many_arguments)]
573 fn from_generated_parts(
574 source_kind: GeneratedCommsTrustAuthoritySourceKind,
575 source_epoch: u64,
576 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
577 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
578 operation: GeneratedCommsTrustAuthorityOperation,
579 peer_id: impl Into<String>,
580 trust_store_peer_id: Option<String>,
581 peer_descriptor: Option<TrustedPeerDescriptor>,
582 ) -> Result<Self, String> {
583 let peer_id = peer_id.into();
584 if matches!(
585 operation,
586 GeneratedCommsTrustAuthorityOperation::PublicAdd
587 | GeneratedCommsTrustAuthorityOperation::PrivateAdd
588 ) && peer_descriptor.is_none()
589 {
590 return Err(format!(
591 "generated comms trust add for peer {peer_id:?} requires a trusted peer descriptor"
592 ));
593 }
594 if let Some(peer) = peer_descriptor.as_ref()
595 && peer.peer_id.to_string() != peer_id
596 {
597 return Err(format!(
598 "generated comms trust descriptor peer_id {} does not match requested {:?}",
599 peer.peer_id, peer_id,
600 ));
601 }
602 if matches!(
603 operation,
604 GeneratedCommsTrustAuthorityOperation::PublicRemove
605 | GeneratedCommsTrustAuthorityOperation::PrivateRemove
606 ) && peer_descriptor.is_some()
607 {
608 return Err(format!(
609 "generated comms trust remove for peer {peer_id:?} must not carry a trusted peer descriptor"
610 ));
611 }
612 Ok(Self {
613 source_kind,
614 source_epoch,
615 source_owner_token,
616 trust_row_owner_kind,
617 operation,
618 peer_id,
619 trust_store_peer_id,
620 peer_descriptor,
621 consumed: Arc::new(AtomicBool::new(false)),
622 })
623 }
624
625 pub fn validate_public_add(
626 &self,
627 trust_store_peer_id: Option<PeerId>,
628 peer: &TrustedPeerDescriptor,
629 ) -> Result<(), String> {
630 self.validate_add_operation(
631 GeneratedCommsTrustAuthorityOperation::PublicAdd,
632 trust_store_peer_id,
633 peer,
634 "add a public trusted peer",
635 )
636 }
637
638 pub fn validate_public_remove(
639 &self,
640 trust_store_peer_id: Option<PeerId>,
641 peer_id: PeerId,
642 ) -> Result<(), String> {
643 self.validate_operation(
644 GeneratedCommsTrustAuthorityOperation::PublicRemove,
645 trust_store_peer_id,
646 peer_id,
647 "remove a public trusted peer",
648 )
649 }
650
651 pub fn validate_private_add(
652 &self,
653 trust_store_peer_id: Option<PeerId>,
654 peer: &TrustedPeerDescriptor,
655 ) -> Result<(), String> {
656 self.validate_add_operation(
657 GeneratedCommsTrustAuthorityOperation::PrivateAdd,
658 trust_store_peer_id,
659 peer,
660 "add a private trusted peer",
661 )
662 }
663
664 pub fn validate_private_remove(
665 &self,
666 trust_store_peer_id: Option<PeerId>,
667 peer_id: PeerId,
668 ) -> Result<(), String> {
669 self.validate_operation(
670 GeneratedCommsTrustAuthorityOperation::PrivateRemove,
671 trust_store_peer_id,
672 peer_id,
673 "remove a private trusted peer",
674 )
675 }
676
677 pub fn preflight_public_add(
678 &self,
679 trust_store_peer_id: Option<PeerId>,
680 peer: &TrustedPeerDescriptor,
681 ) -> Result<(), String> {
682 self.preflight_add_operation(
683 GeneratedCommsTrustAuthorityOperation::PublicAdd,
684 trust_store_peer_id,
685 peer,
686 "add a public trusted peer",
687 )
688 }
689
690 pub fn preflight_public_remove(
691 &self,
692 trust_store_peer_id: Option<PeerId>,
693 peer_id: PeerId,
694 ) -> Result<(), String> {
695 self.preflight_operation(
696 GeneratedCommsTrustAuthorityOperation::PublicRemove,
697 trust_store_peer_id,
698 peer_id,
699 "remove a public trusted peer",
700 )
701 }
702
703 fn validate_operation(
704 &self,
705 operation: GeneratedCommsTrustAuthorityOperation,
706 trust_store_peer_id: Option<PeerId>,
707 peer_id: PeerId,
708 action: &'static str,
709 ) -> Result<(), String> {
710 if self.operation != operation {
711 return Err(format!(
712 "trust authority from {:?} for {:?} cannot {action}",
713 self.source_kind, self.operation,
714 ));
715 }
716 self.validate_peer_match(peer_id)?;
717 self.validate_trust_store_peer_match(trust_store_peer_id)?;
718 self.consume_once()
719 }
720
721 fn preflight_operation(
722 &self,
723 operation: GeneratedCommsTrustAuthorityOperation,
724 trust_store_peer_id: Option<PeerId>,
725 peer_id: PeerId,
726 action: &'static str,
727 ) -> Result<(), String> {
728 if self.operation != operation {
729 return Err(format!(
730 "trust authority from {:?} for {:?} cannot {action}",
731 self.source_kind, self.operation,
732 ));
733 }
734 self.validate_peer_match(peer_id)?;
735 self.validate_trust_store_peer_match(trust_store_peer_id)
736 }
737
738 fn validate_add_operation(
739 &self,
740 operation: GeneratedCommsTrustAuthorityOperation,
741 trust_store_peer_id: Option<PeerId>,
742 peer: &TrustedPeerDescriptor,
743 action: &'static str,
744 ) -> Result<(), String> {
745 if self.operation != operation {
746 return Err(format!(
747 "trust authority from {:?} for {:?} cannot {action}",
748 self.source_kind, self.operation,
749 ));
750 }
751 self.validate_peer_match(peer.peer_id)?;
752 self.validate_peer_descriptor_match(peer)?;
753 self.validate_trust_store_peer_match(trust_store_peer_id)?;
754 self.consume_once()
755 }
756
757 fn preflight_add_operation(
758 &self,
759 operation: GeneratedCommsTrustAuthorityOperation,
760 trust_store_peer_id: Option<PeerId>,
761 peer: &TrustedPeerDescriptor,
762 action: &'static str,
763 ) -> Result<(), String> {
764 if self.operation != operation {
765 return Err(format!(
766 "trust authority from {:?} for {:?} cannot {action}",
767 self.source_kind, self.operation,
768 ));
769 }
770 self.validate_peer_match(peer.peer_id)?;
771 self.validate_peer_descriptor_match(peer)?;
772 self.validate_trust_store_peer_match(trust_store_peer_id)
773 }
774
775 fn consume_once(&self) -> Result<(), String> {
776 self.consumed
777 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
778 .map(|_| ())
779 .map_err(|_| "generated comms trust authority was already consumed".to_string())
780 }
781
782 fn validate_peer_match(&self, peer_id: PeerId) -> Result<(), String> {
783 let expected = self.peer_id();
784 if expected == peer_id.to_string() {
785 Ok(())
786 } else {
787 Err(format!(
788 "trust authority peer_id {expected:?} does not match mutation peer_id {peer_id}"
789 ))
790 }
791 }
792
793 fn validate_trust_store_peer_match(
794 &self,
795 trust_store_peer_id: Option<PeerId>,
796 ) -> Result<(), String> {
797 let Some(expected) = self.trust_store_peer_id.as_deref() else {
798 return Ok(());
799 };
800 let Some(actual) = trust_store_peer_id else {
801 return Err(format!(
802 "trust authority from {:?} requires trust-store peer_id {expected:?}, but the target runtime did not expose one",
803 self.source_kind,
804 ));
805 };
806 if expected == actual.to_string() {
807 Ok(())
808 } else {
809 Err(format!(
810 "trust authority from {:?} for peer {:?} targets trust-store peer_id {expected:?}, not {actual}",
811 self.source_kind,
812 self.peer_id(),
813 ))
814 }
815 }
816
817 fn validate_peer_descriptor_match(&self, peer: &TrustedPeerDescriptor) -> Result<(), String> {
818 let Some(expected) = self.peer_descriptor.as_ref() else {
819 return Err(format!(
820 "trust authority from {:?} for {:?} did not carry a generated peer descriptor",
821 self.source_kind, self.operation,
822 ));
823 };
824 if expected == peer {
825 Ok(())
826 } else {
827 Err(format!(
828 "trust authority descriptor for peer {:?} does not match mutation descriptor",
829 self.peer_id()
830 ))
831 }
832 }
833
834 fn peer_id(&self) -> &str {
835 self.peer_id.as_str()
836 }
837
838 pub fn source_epoch(&self) -> u64 {
839 self.source_epoch
840 }
841
842 pub fn validate_source_owner_token(
843 &self,
844 expected: Option<&GeneratedPeerCommsOwnerToken>,
845 ) -> Result<(), String> {
846 let Some(actual) = self.source_owner_token.as_ref() else {
847 return Err(format!(
848 "trust authority from {:?} did not carry a generated owner token",
849 self.source_kind,
850 ));
851 };
852 let Some(expected) = expected else {
853 return Err(format!(
854 "trust authority from {:?} requires the target runtime's generated owner token",
855 self.source_kind,
856 ));
857 };
858 if expected.matches_raw_owner(actual) {
859 Ok(())
860 } else {
861 Err(format!(
862 "trust authority from {:?} was minted by a different generated owner",
863 self.source_kind,
864 ))
865 }
866 }
867
868 pub fn validate_target_source_owner_token(
869 &self,
870 expected_meerkat_machine_owner: Option<&GeneratedPeerCommsOwnerToken>,
871 expected_mob_machine_owner: Option<&Arc<dyn Any + Send + Sync>>,
872 ) -> Result<(), String> {
873 if is_meerkat_machine_trust_source(self.source_kind) {
874 self.validate_source_owner_token(expected_meerkat_machine_owner)
875 } else if is_mob_machine_trust_source(self.source_kind) {
876 self.validate_raw_source_owner_token(expected_mob_machine_owner)
877 } else {
878 Err(format!(
879 "trust authority from {:?} has no target owner validator",
880 self.source_kind,
881 ))
882 }
883 }
884
885 pub fn validate_raw_source_owner_token(
886 &self,
887 expected: Option<&Arc<dyn Any + Send + Sync>>,
888 ) -> Result<(), String> {
889 let Some(actual) = self.source_owner_token.as_ref() else {
890 return Err(format!(
891 "trust authority from {:?} did not carry a generated owner token",
892 self.source_kind,
893 ));
894 };
895 let Some(expected) = expected else {
896 return Err(format!(
897 "trust authority from {:?} requires the target runtime's generated owner token",
898 self.source_kind,
899 ));
900 };
901 if Arc::ptr_eq(actual, expected) {
902 Ok(())
903 } else {
904 Err(format!(
905 "trust authority from {:?} was minted by a different generated owner",
906 self.source_kind,
907 ))
908 }
909 }
910
911 pub fn is_mob_machine_source(&self) -> bool {
912 is_mob_machine_trust_source(self.source_kind)
913 }
914
915 pub fn trust_row_owner_kind(&self) -> GeneratedCommsTrustAuthoritySourceKind {
916 self.trust_row_owner_kind
917 }
918}
919
920#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
921#[allow(improper_ctypes_definitions, unsafe_code)]
922unsafe extern "Rust" {
923 #[link_name = concat!(
924 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_comms_trust_reconcile_",
925 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
926 )]
927 fn runtime_comms_trust_reconcile_generated_authority_bridge_token_is_valid(
928 token: &(dyn std::any::Any + Send + Sync),
929 ) -> bool;
930
931 #[link_name = concat!(
932 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_supervisor_trust_publish_",
933 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
934 )]
935 fn runtime_supervisor_trust_publish_generated_authority_bridge_token_is_valid(
936 token: &(dyn std::any::Any + Send + Sync),
937 ) -> bool;
938
939 #[link_name = concat!(
940 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_supervisor_trust_revoke_",
941 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
942 )]
943 fn runtime_supervisor_trust_revoke_generated_authority_bridge_token_is_valid(
944 token: &(dyn std::any::Any + Send + Sync),
945 ) -> bool;
946
947 #[link_name = concat!(
948 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_member_trust_wiring_",
949 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
950 )]
951 fn mob_member_trust_wiring_generated_authority_bridge_token_is_valid(
952 token: &(dyn std::any::Any + Send + Sync),
953 ) -> bool;
954
955 #[link_name = concat!(
956 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_member_trust_unwiring_",
957 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
958 )]
959 fn mob_member_trust_unwiring_generated_authority_bridge_token_is_valid(
960 token: &(dyn std::any::Any + Send + Sync),
961 ) -> bool;
962
963 #[link_name = concat!(
964 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_trust_wiring_",
965 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
966 )]
967 fn mob_external_peer_trust_wiring_generated_authority_bridge_token_is_valid(
968 token: &(dyn std::any::Any + Send + Sync),
969 ) -> bool;
970
971 #[link_name = concat!(
972 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_trust_unwiring_",
973 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
974 )]
975 fn mob_external_peer_trust_unwiring_generated_authority_bridge_token_is_valid(
976 token: &(dyn std::any::Any + Send + Sync),
977 ) -> bool;
978
979 #[link_name = concat!(
980 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_trust_repair_",
981 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
982 )]
983 fn mob_external_peer_trust_repair_generated_authority_bridge_token_is_valid(
984 token: &(dyn std::any::Any + Send + Sync),
985 ) -> bool;
986
987 #[link_name = concat!(
988 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_reciprocal_trust_",
989 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
990 )]
991 fn mob_external_peer_reciprocal_trust_generated_authority_bridge_token_is_valid(
992 token: &(dyn std::any::Any + Send + Sync),
993 ) -> bool;
994}
995
996#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
997#[doc(hidden)]
998#[allow(improper_ctypes_definitions, unsafe_code)]
999#[allow(clippy::too_many_arguments)]
1000#[unsafe(export_name = concat!(
1001 "__meerkat_core_runtime_generated_comms_trust_authority_build_v1_",
1002 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1003))]
1004pub(crate) extern "Rust" fn runtime_generated_comms_trust_authority_build(
1005 token: &'static (dyn std::any::Any + Send + Sync),
1006 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1007 source_epoch: u64,
1008 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
1009 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1010 operation: GeneratedCommsTrustAuthorityOperation,
1011 peer_id: String,
1012 trust_store_peer_id: Option<String>,
1013 peer_descriptor: Option<TrustedPeerDescriptor>,
1014) -> Result<CommsTrustMutationAuthority, String> {
1015 validate_runtime_generated_authority_bridge_token(source_kind, token)?;
1016 validate_meerkat_machine_trust_source(source_kind, trust_row_owner_kind)?;
1017 CommsTrustMutationAuthority::from_generated_parts(
1018 source_kind,
1019 source_epoch,
1020 source_owner_token,
1021 trust_row_owner_kind,
1022 operation,
1023 peer_id,
1024 trust_store_peer_id,
1025 peer_descriptor,
1026 )
1027}
1028
1029#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1030#[doc(hidden)]
1031#[allow(improper_ctypes_definitions, unsafe_code)]
1032#[allow(clippy::too_many_arguments)]
1033#[unsafe(export_name = concat!(
1034 "__meerkat_core_mob_generated_comms_trust_authority_build_v1_",
1035 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1036))]
1037pub(crate) extern "Rust" fn mob_generated_comms_trust_authority_build(
1038 token: &'static (dyn std::any::Any + Send + Sync),
1039 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1040 source_epoch: u64,
1041 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
1042 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1043 operation: GeneratedCommsTrustAuthorityOperation,
1044 peer_id: String,
1045 trust_store_peer_id: Option<String>,
1046 peer_descriptor: Option<TrustedPeerDescriptor>,
1047) -> Result<CommsTrustMutationAuthority, String> {
1048 validate_mob_generated_authority_bridge_token(source_kind, token)?;
1049 validate_mob_machine_trust_source(source_kind, trust_row_owner_kind)?;
1050 CommsTrustMutationAuthority::from_generated_parts(
1051 source_kind,
1052 source_epoch,
1053 source_owner_token,
1054 trust_row_owner_kind,
1055 operation,
1056 peer_id,
1057 trust_store_peer_id,
1058 peer_descriptor,
1059 )
1060}
1061
1062#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1063fn validate_runtime_generated_authority_bridge_token(
1064 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1065 token: &(dyn std::any::Any + Send + Sync),
1066) -> Result<(), String> {
1067 #[allow(unsafe_code)]
1068 let valid = unsafe {
1069 match source_kind {
1070 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection => {
1071 runtime_comms_trust_reconcile_generated_authority_bridge_token_is_valid(token)
1072 }
1073 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorPublish => {
1074 runtime_supervisor_trust_publish_generated_authority_bridge_token_is_valid(token)
1075 }
1076 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorRevoke => {
1077 runtime_supervisor_trust_revoke_generated_authority_bridge_token_is_valid(token)
1078 }
1079 _ => false,
1080 }
1081 };
1082 if valid {
1083 Ok(())
1084 } else {
1085 Err("generated comms trust authority requires the matching generated runtime protocol bridge token".into())
1086 }
1087}
1088
1089#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1090fn validate_mob_generated_authority_bridge_token(
1091 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1092 token: &(dyn std::any::Any + Send + Sync),
1093) -> Result<(), String> {
1094 #[allow(unsafe_code)]
1095 let valid = unsafe {
1096 match source_kind {
1097 GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring => {
1098 mob_member_trust_wiring_generated_authority_bridge_token_is_valid(token)
1099 }
1100 GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustUnwiring => {
1101 mob_member_trust_unwiring_generated_authority_bridge_token_is_valid(token)
1102 }
1103 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustWiring => {
1104 mob_external_peer_trust_wiring_generated_authority_bridge_token_is_valid(token)
1105 }
1106 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustUnwiring => {
1107 mob_external_peer_trust_unwiring_generated_authority_bridge_token_is_valid(token)
1108 }
1109 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustRepair => {
1110 mob_external_peer_trust_repair_generated_authority_bridge_token_is_valid(token)
1111 }
1112 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerReciprocalTrust => {
1113 mob_external_peer_reciprocal_trust_generated_authority_bridge_token_is_valid(token)
1114 }
1115 _ => false,
1116 }
1117 };
1118 if valid {
1119 Ok(())
1120 } else {
1121 Err("generated comms trust authority requires the matching generated MobMachine protocol bridge token".into())
1122 }
1123}
1124
1125#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1126fn validate_meerkat_machine_trust_source(
1127 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1128 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1129) -> Result<(), String> {
1130 if is_meerkat_machine_trust_source(source_kind)
1131 && is_meerkat_machine_trust_source(trust_row_owner_kind)
1132 {
1133 Ok(())
1134 } else {
1135 Err(format!(
1136 "runtime generated comms trust authority cannot package source {source_kind:?} with row owner {trust_row_owner_kind:?}"
1137 ))
1138 }
1139}
1140
1141#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1142fn validate_mob_machine_trust_source(
1143 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1144 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1145) -> Result<(), String> {
1146 if is_mob_machine_trust_source(source_kind) && is_mob_machine_trust_source(trust_row_owner_kind)
1147 {
1148 Ok(())
1149 } else {
1150 Err(format!(
1151 "mob generated comms trust authority cannot package source {source_kind:?} with row owner {trust_row_owner_kind:?}"
1152 ))
1153 }
1154}
1155
1156fn is_meerkat_machine_trust_source(kind: GeneratedCommsTrustAuthoritySourceKind) -> bool {
1157 matches!(
1158 kind,
1159 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection
1160 | GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorPublish
1161 | GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorRevoke
1162 )
1163}
1164
1165fn is_mob_machine_trust_source(kind: GeneratedCommsTrustAuthoritySourceKind) -> bool {
1166 matches!(
1167 kind,
1168 GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring
1169 | GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustUnwiring
1170 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustWiring
1171 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustUnwiring
1172 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustRepair
1173 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerReciprocalTrust
1174 )
1175}
1176
1177#[derive(Debug, Clone)]
1179pub enum CommsTrustMutation {
1180 AddTrustedPeer {
1181 peer: TrustedPeerDescriptor,
1182 authority: CommsTrustMutationAuthority,
1183 },
1184 RemoveTrustedPeer {
1185 peer_id: String,
1186 authority: CommsTrustMutationAuthority,
1187 },
1188 AddPrivateTrustedPeer {
1189 peer: TrustedPeerDescriptor,
1190 authority: CommsTrustMutationAuthority,
1191 },
1192 RemovePrivateTrustedPeer {
1193 peer_id: String,
1194 authority: CommsTrustMutationAuthority,
1195 },
1196}
1197
1198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1201pub enum CommsTrustMutationResult {
1202 Added { created: bool },
1203 Removed { removed: bool },
1204}
1205
1206impl TrustedPeerDescriptor {
1207 pub fn pubkey_is_zero(pubkey: &[u8; 32]) -> bool {
1208 *pubkey == [0u8; 32]
1209 }
1210
1211 pub fn has_zero_pubkey(&self) -> bool {
1212 Self::pubkey_is_zero(&self.pubkey)
1213 }
1214
1215 pub fn validate_pubkey_for_peer_id(peer_id: PeerId, pubkey: &[u8; 32]) -> Result<(), String> {
1216 if Self::pubkey_is_zero(pubkey) {
1217 return Err("TrustedPeerDescriptor.pubkey must be non-zero".to_string());
1218 }
1219 let derived = PeerId::from_ed25519_pubkey(pubkey);
1220 if derived != peer_id {
1221 return Err(format!(
1222 "peer_id {peer_id} does not match pubkey-derived id {derived}"
1223 ));
1224 }
1225 Ok(())
1226 }
1227
1228 pub fn test_only_unsigned(
1241 name: impl Into<String>,
1242 peer_id: impl AsRef<str>,
1243 address: impl AsRef<str>,
1244 ) -> Result<Self, String> {
1245 let name = PeerName::new(name).map_err(|e| format!("invalid peer name: {e}"))?;
1246 let peer_id =
1247 PeerId::parse(peer_id.as_ref()).map_err(|e| format!("invalid peer_id: {e}"))?;
1248 let address = PeerAddress::parse(address.as_ref()).map_err(|e| e.to_string())?;
1249 Ok(Self {
1250 peer_id,
1251 name,
1252 address,
1253 pubkey: [0u8; 32],
1254 })
1255 }
1256
1257 pub fn test_only_unsigned_typed(
1277 name: impl Into<String>,
1278 peer_id: PeerId,
1279 address: impl AsRef<str>,
1280 ) -> Result<Self, String> {
1281 let name = PeerName::new(name).map_err(|e| format!("invalid peer name: {e}"))?;
1282 let address = PeerAddress::parse(address.as_ref()).map_err(|e| e.to_string())?;
1283 Ok(Self {
1284 peer_id,
1285 name,
1286 address,
1287 pubkey: [0u8; 32],
1288 })
1289 }
1290
1291 pub fn with_pubkey(mut self, pubkey: [u8; 32]) -> Self {
1297 self.pubkey = pubkey;
1298 self
1299 }
1300
1301 pub fn unsigned_with_pubkey(
1318 name: impl Into<String>,
1319 peer_id: impl AsRef<str>,
1320 pubkey: [u8; 32],
1321 address: impl AsRef<str>,
1322 ) -> Result<Self, String> {
1323 let mut descriptor = Self::test_only_unsigned(name, peer_id, address)?;
1324 Self::validate_pubkey_for_peer_id(descriptor.peer_id, &pubkey)?;
1325 descriptor.pubkey = pubkey;
1326 Ok(descriptor)
1327 }
1328}
1329
1330#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1336#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1337pub enum PeerLifecycleKind {
1338 #[serde(rename = "mob.peer_added")]
1339 PeerAdded,
1340 #[serde(rename = "mob.peer_retired")]
1341 PeerRetired,
1342 #[serde(rename = "mob.peer_unwired")]
1343 PeerUnwired,
1344 #[serde(rename = "mob.dismiss")]
1350 Dismiss,
1351}
1352
1353impl PeerLifecycleKind {
1354 pub const fn as_str(self) -> &'static str {
1355 match self {
1356 Self::PeerAdded => "mob.peer_added",
1357 Self::PeerRetired => "mob.peer_retired",
1358 Self::PeerUnwired => "mob.peer_unwired",
1359 Self::Dismiss => "mob.dismiss",
1360 }
1361 }
1362}
1363
1364impl std::fmt::Display for PeerLifecycleKind {
1365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1366 f.write_str(self.as_str())
1367 }
1368}
1369
1370#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1383#[serde(rename_all = "snake_case")]
1384pub enum SenderContentTaint {
1385 Clean,
1387 Tainted,
1390}
1391
1392impl SenderContentTaint {
1393 pub const fn as_str(self) -> &'static str {
1394 match self {
1395 Self::Clean => "clean",
1396 Self::Tainted => "tainted",
1397 }
1398 }
1399}
1400
1401impl std::fmt::Display for SenderContentTaint {
1402 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1403 f.write_str(self.as_str())
1404 }
1405}
1406
1407#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1416#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1417#[serde(rename_all = "snake_case")]
1418pub enum SendTaintOverride {
1419 Declare(SenderContentTaint),
1421 Undeclared,
1423}
1424
1425#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1437#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1438#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1439pub enum CommsCommandRequest {
1440 Input {
1442 body: String,
1443 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 blocks: Option<Vec<ContentBlock>>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1446 source: Option<InputSource>,
1447 #[serde(default, skip_serializing_if = "Option::is_none")]
1448 stream: Option<InputStreamMode>,
1449 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 handling_mode: Option<HandlingMode>,
1451 #[serde(default, skip_serializing_if = "Option::is_none")]
1452 allow_self_session: Option<bool>,
1453 },
1454 PeerMessage {
1456 to: PeerId,
1457 body: String,
1458 #[serde(default, skip_serializing_if = "Option::is_none")]
1459 blocks: Option<Vec<ContentBlock>>,
1460 #[serde(default, skip_serializing_if = "Option::is_none")]
1461 content_taint: Option<SendTaintOverride>,
1462 #[serde(default, skip_serializing_if = "Option::is_none")]
1463 handling_mode: Option<HandlingMode>,
1464 },
1465 PeerLifecycle {
1467 to: PeerId,
1468 lifecycle_kind: PeerLifecycleKind,
1469 #[serde(default)]
1470 params: serde_json::Value,
1471 },
1472 PeerRequest {
1474 to: PeerId,
1475 intent: CommsPeerRequestIntent,
1478 #[serde(default)]
1479 params: serde_json::Value,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 blocks: Option<Vec<ContentBlock>>,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 content_taint: Option<SendTaintOverride>,
1484 #[serde(default, skip_serializing_if = "Option::is_none")]
1485 handling_mode: Option<HandlingMode>,
1486 #[serde(default, skip_serializing_if = "Option::is_none")]
1487 stream: Option<InputStreamMode>,
1488 },
1489 PeerResponse {
1491 to: PeerId,
1492 in_reply_to: InteractionId,
1493 status: ResponseStatus,
1494 #[serde(default)]
1495 result: serde_json::Value,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1497 blocks: Option<Vec<ContentBlock>>,
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1499 content_taint: Option<SendTaintOverride>,
1500 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 handling_mode: Option<HandlingMode>,
1502 },
1503}
1504
1505#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1510pub enum CommsCommandError {
1511 #[error("handling_mode is forbidden on progress peer responses")]
1516 HandlingModeForbiddenForProgressResponse,
1517}
1518
1519impl CommsCommandRequest {
1520 pub fn into_command(
1525 self,
1526 session_id: &crate::types::SessionId,
1527 ) -> Result<CommsCommand, CommsCommandError> {
1528 Ok(match self {
1529 CommsCommandRequest::Input {
1530 body,
1531 blocks,
1532 source,
1533 stream,
1534 handling_mode,
1535 allow_self_session,
1536 } => CommsCommand::Input {
1537 session_id: session_id.clone(),
1538 body,
1539 blocks,
1540 handling_mode: handling_mode.unwrap_or_default(),
1541 source: source.unwrap_or(InputSource::Rpc),
1542 stream: stream.unwrap_or(InputStreamMode::None),
1543 allow_self_session: allow_self_session.unwrap_or(false),
1544 },
1545 CommsCommandRequest::PeerMessage {
1546 to,
1547 body,
1548 blocks,
1549 content_taint,
1550 handling_mode,
1551 } => CommsCommand::PeerMessage {
1552 to: PeerRoute::new(to),
1553 body,
1554 blocks,
1555 content_taint,
1556 handling_mode: handling_mode.unwrap_or_default(),
1557 objective_id: None,
1558 },
1559 CommsCommandRequest::PeerLifecycle {
1560 to,
1561 lifecycle_kind,
1562 params,
1563 } => CommsCommand::PeerLifecycle {
1564 to: PeerRoute::new(to),
1565 kind: lifecycle_kind,
1566 params,
1567 },
1568 CommsCommandRequest::PeerRequest {
1569 to,
1570 intent,
1571 params,
1572 blocks,
1573 content_taint,
1574 handling_mode,
1575 stream,
1576 } => CommsCommand::PeerRequest {
1577 to: PeerRoute::new(to),
1578 intent: intent.as_str().to_string(),
1584 params,
1585 blocks,
1586 content_taint,
1587 handling_mode: handling_mode.unwrap_or_default(),
1588 stream: stream.unwrap_or(InputStreamMode::None),
1589 objective_id: None,
1590 },
1591 CommsCommandRequest::PeerResponse {
1592 to,
1593 in_reply_to,
1594 status,
1595 result,
1596 blocks,
1597 content_taint,
1598 handling_mode,
1599 } => CommsCommand::PeerResponse {
1600 to: PeerRoute::new(to),
1601 in_reply_to,
1602 status,
1603 result,
1604 blocks,
1605 content_taint,
1606 handling_mode,
1607 objective_id: None,
1608 },
1609 })
1610 }
1611
1612 pub fn kind(&self) -> &'static str {
1614 match self {
1615 Self::Input { .. } => "input",
1616 Self::PeerMessage { .. } => "peer_message",
1617 Self::PeerLifecycle { .. } => "peer_lifecycle",
1618 Self::PeerRequest { .. } => "peer_request",
1619 Self::PeerResponse { .. } => "peer_response",
1620 }
1621 }
1622}
1623#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1625#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1626#[serde(rename_all = "lowercase")]
1627pub enum InputSource {
1628 Tcp,
1629 Uds,
1630 Stdin,
1631 Webhook,
1632 Rpc,
1633}
1634
1635impl From<crate::config::PlainEventSource> for InputSource {
1636 fn from(source: crate::config::PlainEventSource) -> Self {
1637 match source {
1638 crate::config::PlainEventSource::Tcp => Self::Tcp,
1639 crate::config::PlainEventSource::Uds => Self::Uds,
1640 crate::config::PlainEventSource::Stdin => Self::Stdin,
1641 crate::config::PlainEventSource::Webhook => Self::Webhook,
1642 crate::config::PlainEventSource::Rpc => Self::Rpc,
1643 }
1644 }
1645}
1646
1647impl From<InputSource> for crate::config::PlainEventSource {
1648 fn from(source: InputSource) -> Self {
1649 match source {
1650 InputSource::Tcp => Self::Tcp,
1651 InputSource::Uds => Self::Uds,
1652 InputSource::Stdin => Self::Stdin,
1653 InputSource::Webhook => Self::Webhook,
1654 InputSource::Rpc => Self::Rpc,
1655 }
1656 }
1657}
1658
1659#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1661#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1662#[serde(rename_all = "snake_case")]
1663pub enum InputStreamMode {
1664 None,
1666 ReserveInteraction,
1668}
1669
1670#[derive(Debug, Clone, PartialEq, Eq)]
1672pub enum CommsCommand {
1673 Input {
1675 session_id: crate::types::SessionId,
1676 body: String,
1677 blocks: Option<Vec<ContentBlock>>,
1678 handling_mode: HandlingMode,
1679 source: InputSource,
1680 stream: InputStreamMode,
1681 allow_self_session: bool,
1682 },
1683 PeerMessage {
1685 to: PeerRoute,
1686 body: String,
1687 blocks: Option<Vec<ContentBlock>>,
1688 content_taint: Option<SendTaintOverride>,
1690 handling_mode: HandlingMode,
1691 objective_id: Option<crate::interaction::ObjectiveId>,
1692 },
1693 IncarnationFencedPeerMessage {
1696 to: PeerRoute,
1697 body: String,
1698 blocks: Option<Vec<ContentBlock>>,
1699 content_taint: Option<SendTaintOverride>,
1700 handling_mode: HandlingMode,
1701 objective_id: Option<crate::interaction::ObjectiveId>,
1702 expected_recipient: PeerRecipientIncarnation,
1703 },
1704 PeerLifecycle {
1706 to: PeerRoute,
1707 kind: PeerLifecycleKind,
1708 params: serde_json::Value,
1709 },
1710 PeerRequest {
1712 to: PeerRoute,
1713 intent: String,
1714 params: serde_json::Value,
1715 blocks: Option<Vec<ContentBlock>>,
1716 content_taint: Option<SendTaintOverride>,
1718 handling_mode: HandlingMode,
1719 stream: InputStreamMode,
1720 objective_id: Option<crate::interaction::ObjectiveId>,
1721 },
1722 PeerResponse {
1724 to: PeerRoute,
1725 in_reply_to: InteractionId,
1726 status: ResponseStatus,
1727 result: serde_json::Value,
1728 blocks: Option<Vec<ContentBlock>>,
1729 content_taint: Option<SendTaintOverride>,
1731 handling_mode: Option<HandlingMode>,
1732 objective_id: Option<crate::interaction::ObjectiveId>,
1733 },
1734}
1735
1736impl CommsCommand {
1737 #[must_use]
1738 pub fn with_objective_id(
1739 mut self,
1740 objective_id: Option<crate::interaction::ObjectiveId>,
1741 ) -> Self {
1742 match &mut self {
1743 Self::PeerMessage {
1744 objective_id: slot, ..
1745 }
1746 | Self::IncarnationFencedPeerMessage {
1747 objective_id: slot, ..
1748 }
1749 | Self::PeerRequest {
1750 objective_id: slot, ..
1751 }
1752 | Self::PeerResponse {
1753 objective_id: slot, ..
1754 } => *slot = objective_id,
1755 Self::Input { .. } | Self::PeerLifecycle { .. } => {}
1756 }
1757 self
1758 }
1759
1760 pub fn command_kind(&self) -> &'static str {
1761 match self {
1762 Self::Input { .. } => "input",
1763 Self::PeerMessage { .. } => "peer_message",
1764 Self::IncarnationFencedPeerMessage { .. } => "incarnation_fenced_peer_message",
1765 Self::PeerLifecycle { .. } => "peer_lifecycle",
1766 Self::PeerRequest { .. } => "peer_request",
1767 Self::PeerResponse { .. } => "peer_response",
1768 }
1769 }
1770}
1771
1772#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1774#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1775#[serde(rename_all = "snake_case")]
1776#[non_exhaustive]
1777pub enum PeerDeliveryOutcome {
1778 Acked,
1780 HandedOff,
1782 Queued,
1785}
1786
1787#[derive(Debug, Clone, PartialEq, Eq)]
1789pub enum SendReceipt {
1790 InputAccepted {
1791 interaction_id: InteractionId,
1792 stream_reserved: bool,
1793 },
1794 PeerMessageSent {
1795 envelope_id: uuid::Uuid,
1796 delivery: PeerDeliveryOutcome,
1797 },
1798 PeerLifecycleSent {
1799 envelope_id: uuid::Uuid,
1800 },
1801 PeerRequestSent {
1802 envelope_id: uuid::Uuid,
1803 interaction_id: InteractionId,
1804 stream_reserved: bool,
1805 },
1806 PeerResponseSent {
1807 envelope_id: uuid::Uuid,
1808 in_reply_to: InteractionId,
1809 },
1810}
1811
1812pub const COMMS_PEER_REPLY_DISPATCH_CONTEXT_KEY: &str = "comms.peer_reply";
1820
1821#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1823#[serde(rename_all = "snake_case")]
1824#[non_exhaustive]
1825pub enum PeerReplyDeliveryKind {
1826 Message,
1828}
1829
1830#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1836pub struct PeerReplyCapability {
1837 pub in_reply_to: InteractionId,
1839 pub peer_id: PeerId,
1841 #[serde(default, skip_serializing_if = "Option::is_none")]
1844 pub display_name: Option<String>,
1845 pub kind: PeerReplyDeliveryKind,
1847}
1848
1849#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1853#[serde(deny_unknown_fields)]
1854pub struct PeerReplyDispatchContext {
1855 pub deliveries: Vec<PeerReplyCapability>,
1857}
1858
1859#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1860#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1861#[serde(rename_all = "snake_case")]
1862pub enum PeerDirectorySource {
1863 Trusted,
1864 Inproc,
1865 TrustedAndInproc,
1866 Unknown,
1867}
1868
1869impl PeerDirectorySource {
1870 pub const fn as_str(&self) -> &'static str {
1871 match self {
1872 Self::Trusted => "trusted",
1873 Self::Inproc => "inproc",
1874 Self::TrustedAndInproc => "trusted_and_inproc",
1875 Self::Unknown => "unknown",
1876 }
1877 }
1878}
1879
1880impl std::fmt::Display for PeerDirectorySource {
1881 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1882 f.write_str(self.as_str())
1883 }
1884}
1885
1886#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1887#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1888#[serde(rename_all = "snake_case")]
1889pub enum PeerSendability {
1890 PeerMessage,
1891 PeerRequest,
1892 PeerResponse,
1893}
1894
1895impl PeerSendability {
1896 pub const DIRECTORY_DEFAULTS: [Self; 3] =
1897 [Self::PeerMessage, Self::PeerRequest, Self::PeerResponse];
1898
1899 pub fn directory_defaults() -> Vec<Self> {
1900 Self::DIRECTORY_DEFAULTS.to_vec()
1901 }
1902
1903 pub const fn as_str(&self) -> &'static str {
1904 match self {
1905 Self::PeerMessage => "peer_message",
1906 Self::PeerRequest => "peer_request",
1907 Self::PeerResponse => "peer_response",
1908 }
1909 }
1910}
1911
1912impl std::fmt::Display for PeerSendability {
1913 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1914 f.write_str(self.as_str())
1915 }
1916}
1917
1918#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1924#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1925pub struct PeerCapabilitySet {
1926 #[serde(default = "PeerCapabilitySet::default_version")]
1927 pub version: u16,
1928 #[serde(default)]
1929 pub extensions: BTreeMap<String, serde_json::Value>,
1930}
1931
1932impl PeerCapabilitySet {
1933 pub const CURRENT_VERSION: u16 = 1;
1934
1935 const fn default_version() -> u16 {
1936 Self::CURRENT_VERSION
1937 }
1938
1939 pub fn with_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
1940 self.extensions.insert(key.into(), value);
1941 self
1942 }
1943}
1944
1945impl Default for PeerCapabilitySet {
1946 fn default() -> Self {
1947 Self {
1948 version: Self::CURRENT_VERSION,
1949 extensions: BTreeMap::new(),
1950 }
1951 }
1952}
1953
1954#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1955#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1956pub struct PeerDirectoryEntry {
1957 pub peer_id: PeerId,
1959 pub name: PeerName,
1962 pub address: PeerAddress,
1966 pub source: PeerDirectorySource,
1967 pub sendable_kinds: Vec<PeerSendability>,
1968 pub capabilities: PeerCapabilitySet,
1969 pub meta: crate::PeerMeta,
1971}
1972
1973#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1974#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1975pub struct PeerDirectoryListing {
1976 pub peers: Vec<PeerDirectoryEntry>,
1977}
1978
1979impl PeerDirectoryListing {
1980 pub fn new(peers: Vec<PeerDirectoryEntry>) -> Self {
1981 Self { peers }
1982 }
1983}
1984
1985impl From<Vec<PeerDirectoryEntry>> for PeerDirectoryListing {
1986 fn from(peers: Vec<PeerDirectoryEntry>) -> Self {
1987 Self::new(peers)
1988 }
1989}
1990
1991#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1993pub enum StreamScope {
1994 Session(crate::types::SessionId),
1995 Interaction(InteractionId),
1996}
1997
1998pub type EventStream = Pin<Box<dyn Stream<Item = EventEnvelope<AgentEvent>> + Send>>;
2000
2001#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
2003pub enum StreamError {
2004 #[error("interaction not reserved: {0}")]
2005 NotReserved(InteractionId),
2006 #[error("stream not found: {0}")]
2007 NotFound(String),
2008 #[error("already attached: {0}")]
2009 AlreadyAttached(InteractionId),
2010 #[error("interaction stream {interaction_id} abandoned: {reason}")]
2011 Abandoned {
2012 interaction_id: InteractionId,
2013 reason: crate::InteractionStreamAbandonReason,
2014 },
2015 #[error("stream closed")]
2016 Closed,
2017 #[error("permission denied: {0}")]
2018 PermissionDenied(String),
2019 #[error("timeout: {0}")]
2020 Timeout(String),
2021 #[error("internal: {0}")]
2022 Internal(String),
2023}
2024
2025#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2033#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2034#[serde(rename_all = "snake_case")]
2035#[non_exhaustive]
2036pub enum AdmissionDropReason {
2037 UntrustedSender,
2040 ClassificationRejected,
2042 SessionClosed,
2044 InboxFull,
2046}
2047
2048impl AdmissionDropReason {
2049 pub fn as_code(&self) -> &'static str {
2052 match self {
2053 AdmissionDropReason::UntrustedSender => "untrusted_sender",
2054 AdmissionDropReason::ClassificationRejected => "classification_rejected",
2055 AdmissionDropReason::SessionClosed => "session_closed",
2056 AdmissionDropReason::InboxFull => "inbox_full",
2057 }
2058 }
2059}
2060
2061#[derive(Debug, Clone, thiserror::Error)]
2062#[non_exhaustive]
2063pub enum SendError {
2064 #[error("peer not found: {0}")]
2065 PeerNotFound(String),
2066 #[error("peer offline")]
2067 PeerOffline,
2068 #[error("peer not sendable")]
2069 PeerNotSendable(String),
2070 #[error("input stream closed")]
2071 InputClosed,
2072 #[error("unsupported command: {0}")]
2073 Unsupported(String),
2074 #[error("validation failed: {0}")]
2075 Validation(String),
2076 #[error("internal: {0}")]
2077 Internal(String),
2078 #[error("transport error: {0}")]
2085 Transport(String),
2086 #[error("peer dropped at admission: {reason:?}")]
2091 AdmissionDropped { reason: AdmissionDropReason },
2092}
2093
2094#[derive(Debug, Clone, thiserror::Error)]
2095pub enum SendAndStreamError {
2096 #[error("send failed: {0}")]
2097 Send(#[from] SendError),
2098 #[error("stream attach failed: receipt={receipt:?}, error={error}")]
2099 StreamAttach {
2100 receipt: SendReceipt,
2101 error: StreamError,
2102 },
2103}
2104
2105#[cfg(test)]
2106#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2107mod tests {
2108 use super::*;
2109
2110 #[test]
2111 fn peer_id_pubkey_derivation_matches_uuid_v5() {
2112 let pubkey = [42u8; 32];
2113 assert_eq!(
2114 PeerId::from_ed25519_pubkey(&pubkey).as_uuid(),
2115 &Uuid::new_v5(&PEER_ID_ED25519_PUBKEY_NAMESPACE, &pubkey)
2116 );
2117 }
2118
2119 #[test]
2120 fn peer_name_validation() {
2121 assert!(PeerName::new("alice").is_ok());
2122 assert!(PeerName::new("".to_string()).is_err());
2123 assert!(PeerName::new("bad\x00name").is_err());
2124 }
2125
2126 #[test]
2127 fn incarnation_fenced_peer_message_accepts_objective_stamp() {
2128 let objective_id = crate::interaction::ObjectiveId::new();
2129 let command = CommsCommand::IncarnationFencedPeerMessage {
2130 to: PeerRoute::new(PeerId::new()),
2131 body: "fenced work".to_string(),
2132 blocks: None,
2133 content_taint: None,
2134 handling_mode: HandlingMode::Queue,
2135 objective_id: None,
2136 expected_recipient: PeerRecipientIncarnation {
2137 mob_id: "mob".to_string(),
2138 agent_identity: "worker".to_string(),
2139 host_id: "host".to_string(),
2140 binding_generation: 1,
2141 member_session_id: "session".to_string(),
2142 generation: 1,
2143 fence_token: 7,
2144 },
2145 }
2146 .with_objective_id(Some(objective_id));
2147
2148 assert!(matches!(
2149 command,
2150 CommsCommand::IncarnationFencedPeerMessage {
2151 objective_id: Some(actual),
2152 ..
2153 } if actual == objective_id
2154 ));
2155 }
2156
2157 #[test]
2158 fn peer_directory_entry_fields() -> Result<(), String> {
2159 let entry = PeerDirectoryEntry {
2160 peer_id: PeerId::new(),
2161 name: PeerName::new("agent")?,
2162 address: PeerAddress::new(PeerTransport::Inproc, "agent"),
2163 source: PeerDirectorySource::Inproc,
2164 sendable_kinds: vec![PeerSendability::PeerMessage],
2165 capabilities: PeerCapabilitySet::default(),
2166 meta: crate::PeerMeta::default(),
2167 };
2168 assert_eq!(entry.name.as_str(), "agent");
2169 assert_eq!(entry.address.transport(), PeerTransport::Inproc);
2170 assert_eq!(entry.address.endpoint(), "agent");
2171 assert_eq!(entry.source, PeerDirectorySource::Inproc);
2172 Ok(())
2173 }
2174
2175 #[test]
2176 fn peer_directory_listing_serializes_typed_source_sendability_and_capabilities()
2177 -> Result<(), String> {
2178 let entry = PeerDirectoryEntry {
2179 peer_id: PeerId::new(),
2180 name: PeerName::new("agent")?,
2181 address: PeerAddress::new(PeerTransport::Inproc, "agent"),
2182 source: PeerDirectorySource::Inproc,
2183 sendable_kinds: vec![PeerSendability::PeerMessage, PeerSendability::PeerRequest],
2184 capabilities: PeerCapabilitySet::default()
2185 .with_extension("vendor.echo", serde_json::json!({ "enabled": true })),
2186 meta: crate::PeerMeta::default(),
2187 };
2188
2189 let value = serde_json::to_value(PeerDirectoryListing::new(vec![entry]))
2190 .map_err(|err| err.to_string())?;
2191 let peer = &value["peers"][0];
2192
2193 assert_eq!(peer["source"], "inproc");
2194 assert_eq!(
2195 peer["sendable_kinds"],
2196 serde_json::json!(["peer_message", "peer_request"])
2197 );
2198 assert_eq!(peer["capabilities"]["version"], 1);
2199 assert_eq!(
2200 peer["capabilities"]["extensions"]["vendor.echo"]["enabled"],
2201 true
2202 );
2203 Ok(())
2204 }
2205
2206 #[test]
2207 fn generated_trust_authority_rejects_descriptor_peer_mismatch() {
2208 let pubkey = [1u8; 32];
2209 let descriptor_peer_id = PeerId::from_ed25519_pubkey(&pubkey);
2210 let requested_peer_id = PeerId::from_ed25519_pubkey(&[2u8; 32]);
2211 let descriptor = TrustedPeerDescriptor::unsigned_with_pubkey(
2212 "fake",
2213 descriptor_peer_id.to_string(),
2214 pubkey,
2215 "inproc://fake",
2216 )
2217 .expect("valid descriptor");
2218 let err = CommsTrustMutationAuthority::from_generated_parts(
2219 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
2220 1,
2221 None,
2222 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
2223 GeneratedCommsTrustAuthorityOperation::PublicAdd,
2224 requested_peer_id.to_string(),
2225 Some(requested_peer_id.to_string()),
2226 Some(descriptor),
2227 )
2228 .expect_err("descriptor for another peer must not mint authority");
2229 assert!(
2230 err.contains("does not match requested"),
2231 "unexpected rejection: {err}"
2232 );
2233 }
2234
2235 #[test]
2236 fn peer_id_parse_round_trip() {
2237 let id = PeerId::new();
2238 let parsed = PeerId::parse(&id.as_str()).expect("parse");
2239 assert_eq!(id, parsed);
2240 }
2241
2242 #[test]
2243 fn peer_id_parse_rejects_garbage() {
2244 let err = PeerId::parse("not-a-uuid").expect_err("parse must reject");
2245 match err {
2246 PeerIdError::Invalid { input, .. } => assert_eq!(input, "not-a-uuid"),
2247 }
2248 }
2249
2250 #[test]
2251 fn peer_address_display() {
2252 let addr = PeerAddress::new(PeerTransport::Tcp, "127.0.0.1:4200");
2253 assert_eq!(addr.to_string(), "tcp://127.0.0.1:4200");
2254 }
2255
2256 #[test]
2257 fn peer_address_parse_round_trips_supported_schemes() {
2258 let cases = [
2259 ("inproc://agent-a", PeerTransport::Inproc, "agent-a"),
2260 (
2261 "uds:///tmp/meerkat.sock",
2262 PeerTransport::Uds,
2263 "/tmp/meerkat.sock",
2264 ),
2265 ("tcp://127.0.0.1:4200", PeerTransport::Tcp, "127.0.0.1:4200"),
2266 ];
2267
2268 for (raw, transport, endpoint) in cases {
2269 let parsed = PeerAddress::parse(raw).expect("supported address parses");
2270 assert_eq!(parsed.transport(), transport);
2271 assert_eq!(parsed.endpoint(), endpoint);
2272 assert_eq!(parsed.to_string(), raw);
2273 }
2274 }
2275
2276 #[test]
2277 fn peer_address_parse_rejects_unknown_scheme() {
2278 let err = PeerAddress::parse("http://127.0.0.1:4200")
2279 .expect_err("unknown transport schemes must fail closed");
2280 assert!(
2281 err.to_string().contains("unknown peer address transport"),
2282 "unexpected error: {err}",
2283 );
2284 }
2285
2286 #[test]
2287 fn peer_address_parse_rejects_schemeless_input() {
2288 let err = PeerAddress::parse("127.0.0.1:4200")
2289 .expect_err("strict parser requires an address scheme");
2290 assert!(
2291 err.to_string().contains("missing transport scheme"),
2292 "unexpected error: {err}",
2293 );
2294 }
2295
2296 #[test]
2297 fn input_stream_mode_roundtrip() -> Result<(), serde_json::Error> {
2298 let mode = InputStreamMode::ReserveInteraction;
2299 let serialized = serde_json::to_value(mode)?;
2300 assert_eq!(serialized.as_str(), Some("reserve_interaction"));
2301 assert_eq!(serde_json::from_value::<InputStreamMode>(serialized)?, mode);
2302 Ok(())
2303 }
2304
2305 #[test]
2306 fn deserialize_input_with_typed_source() -> Result<(), serde_json::Error> {
2307 let json = r#"{"kind":"input","body":"hello","source":"webhook","handling_mode":"steer"}"#;
2308 let req: CommsCommandRequest = serde_json::from_str(json)?;
2309 match req {
2310 CommsCommandRequest::Input {
2311 body,
2312 source,
2313 handling_mode,
2314 ..
2315 } => {
2316 assert_eq!(body, "hello");
2317 assert_eq!(source, Some(InputSource::Webhook));
2318 assert_eq!(handling_mode, Some(HandlingMode::Steer));
2319 }
2320 other => panic!("expected input command request, got {other:?}"),
2321 }
2322 Ok(())
2323 }
2324
2325 #[test]
2326 fn deserialize_input_invalid_source_rejects_at_serde_boundary() {
2327 let json = r#"{"kind":"input","body":"hello","source":"webhookd"}"#;
2328 let err = serde_json::from_str::<CommsCommandRequest>(json)
2329 .expect_err("invalid source must fail deserialization");
2330 let msg = err.to_string();
2331 assert!(
2333 msg.contains("webhookd"),
2334 "error should name the rejected value, got: {msg}"
2335 );
2336 }
2337
2338 #[test]
2339 fn deserialize_unknown_kind_rejects_at_serde_boundary() {
2340 let json = r#"{"kind":"foobar","body":"hello"}"#;
2341 let err = serde_json::from_str::<CommsCommandRequest>(json)
2342 .expect_err("unknown kind must fail deserialization");
2343 let msg = err.to_string();
2344 assert!(
2345 msg.contains("foobar") || msg.contains("variant"),
2346 "error should mention unknown variant, got: {msg}"
2347 );
2348 }
2349}