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))]
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468pub struct TrustedPeerDescriptor {
469 pub peer_id: PeerId,
471 pub name: PeerName,
474 pub address: PeerAddress,
477 pub pubkey: [u8; 32],
481}
482
483#[derive(Debug, Clone)]
490pub struct CommsTrustMutationAuthority {
491 source_kind: GeneratedCommsTrustAuthoritySourceKind,
492 source_epoch: u64,
493 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
494 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
495 operation: GeneratedCommsTrustAuthorityOperation,
496 peer_id: String,
497 trust_store_peer_id: Option<String>,
498 peer_descriptor: Option<TrustedPeerDescriptor>,
499 consumed: Arc<AtomicBool>,
500}
501
502#[derive(Clone)]
503pub struct GeneratedPeerCommsOwnerToken {
504 inner: Arc<dyn Any + Send + Sync>,
505}
506
507impl std::fmt::Debug for GeneratedPeerCommsOwnerToken {
508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509 f.debug_struct("GeneratedPeerCommsOwnerToken").finish()
510 }
511}
512
513impl GeneratedPeerCommsOwnerToken {
514 #[cfg_attr(
515 any(test, not(meerkat_internal_generated_authority_bridge)),
516 allow(dead_code)
517 )]
518 pub(crate) fn from_generated_owner_token(inner: Arc<dyn Any + Send + Sync>) -> Self {
519 Self { inner }
520 }
521
522 pub fn same_owner(&self, other: &Self) -> bool {
523 Arc::ptr_eq(&self.inner, &other.inner)
524 }
525
526 fn matches_raw_owner(&self, other: &Arc<dyn Any + Send + Sync>) -> bool {
527 Arc::ptr_eq(&self.inner, other)
528 }
529}
530
531#[doc(hidden)]
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
533pub enum GeneratedCommsTrustAuthoritySourceKind {
534 MeerkatMachinePeerProjection,
535 MeerkatMachineSupervisorPublish,
536 MeerkatMachineSupervisorRevoke,
537 MobMachineMemberTrustWiring,
538 MobMachineMemberTrustUnwiring,
539 MobMachineExternalPeerTrustWiring,
540 MobMachineExternalPeerTrustUnwiring,
541 MobMachineExternalPeerTrustRepair,
542 MobMachineExternalPeerReciprocalTrust,
543}
544
545#[doc(hidden)]
546#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
547pub enum GeneratedCommsTrustAuthorityOperation {
548 PublicAdd,
549 PublicRemove,
550 PrivateAdd,
551 PrivateRemove,
552}
553
554impl CommsTrustMutationAuthority {
555 #[cfg_attr(not(meerkat_internal_generated_authority_bridge), allow(dead_code))]
556 #[allow(clippy::too_many_arguments)]
557 fn from_generated_parts(
558 source_kind: GeneratedCommsTrustAuthoritySourceKind,
559 source_epoch: u64,
560 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
561 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
562 operation: GeneratedCommsTrustAuthorityOperation,
563 peer_id: impl Into<String>,
564 trust_store_peer_id: Option<String>,
565 peer_descriptor: Option<TrustedPeerDescriptor>,
566 ) -> Result<Self, String> {
567 let peer_id = peer_id.into();
568 if matches!(
569 operation,
570 GeneratedCommsTrustAuthorityOperation::PublicAdd
571 | GeneratedCommsTrustAuthorityOperation::PrivateAdd
572 ) && peer_descriptor.is_none()
573 {
574 return Err(format!(
575 "generated comms trust add for peer {peer_id:?} requires a trusted peer descriptor"
576 ));
577 }
578 if let Some(peer) = peer_descriptor.as_ref()
579 && peer.peer_id.to_string() != peer_id
580 {
581 return Err(format!(
582 "generated comms trust descriptor peer_id {} does not match requested {:?}",
583 peer.peer_id, peer_id,
584 ));
585 }
586 if matches!(
587 operation,
588 GeneratedCommsTrustAuthorityOperation::PublicRemove
589 | GeneratedCommsTrustAuthorityOperation::PrivateRemove
590 ) && peer_descriptor.is_some()
591 {
592 return Err(format!(
593 "generated comms trust remove for peer {peer_id:?} must not carry a trusted peer descriptor"
594 ));
595 }
596 Ok(Self {
597 source_kind,
598 source_epoch,
599 source_owner_token,
600 trust_row_owner_kind,
601 operation,
602 peer_id,
603 trust_store_peer_id,
604 peer_descriptor,
605 consumed: Arc::new(AtomicBool::new(false)),
606 })
607 }
608
609 pub fn validate_public_add(
610 &self,
611 trust_store_peer_id: Option<PeerId>,
612 peer: &TrustedPeerDescriptor,
613 ) -> Result<(), String> {
614 self.validate_add_operation(
615 GeneratedCommsTrustAuthorityOperation::PublicAdd,
616 trust_store_peer_id,
617 peer,
618 "add a public trusted peer",
619 )
620 }
621
622 pub fn validate_public_remove(
623 &self,
624 trust_store_peer_id: Option<PeerId>,
625 peer_id: PeerId,
626 ) -> Result<(), String> {
627 self.validate_operation(
628 GeneratedCommsTrustAuthorityOperation::PublicRemove,
629 trust_store_peer_id,
630 peer_id,
631 "remove a public trusted peer",
632 )
633 }
634
635 pub fn validate_private_add(
636 &self,
637 trust_store_peer_id: Option<PeerId>,
638 peer: &TrustedPeerDescriptor,
639 ) -> Result<(), String> {
640 self.validate_add_operation(
641 GeneratedCommsTrustAuthorityOperation::PrivateAdd,
642 trust_store_peer_id,
643 peer,
644 "add a private trusted peer",
645 )
646 }
647
648 pub fn validate_private_remove(
649 &self,
650 trust_store_peer_id: Option<PeerId>,
651 peer_id: PeerId,
652 ) -> Result<(), String> {
653 self.validate_operation(
654 GeneratedCommsTrustAuthorityOperation::PrivateRemove,
655 trust_store_peer_id,
656 peer_id,
657 "remove a private trusted peer",
658 )
659 }
660
661 pub fn preflight_public_add(
662 &self,
663 trust_store_peer_id: Option<PeerId>,
664 peer: &TrustedPeerDescriptor,
665 ) -> Result<(), String> {
666 self.preflight_add_operation(
667 GeneratedCommsTrustAuthorityOperation::PublicAdd,
668 trust_store_peer_id,
669 peer,
670 "add a public trusted peer",
671 )
672 }
673
674 pub fn preflight_public_remove(
675 &self,
676 trust_store_peer_id: Option<PeerId>,
677 peer_id: PeerId,
678 ) -> Result<(), String> {
679 self.preflight_operation(
680 GeneratedCommsTrustAuthorityOperation::PublicRemove,
681 trust_store_peer_id,
682 peer_id,
683 "remove a public trusted peer",
684 )
685 }
686
687 fn validate_operation(
688 &self,
689 operation: GeneratedCommsTrustAuthorityOperation,
690 trust_store_peer_id: Option<PeerId>,
691 peer_id: PeerId,
692 action: &'static str,
693 ) -> Result<(), String> {
694 if self.operation != operation {
695 return Err(format!(
696 "trust authority from {:?} for {:?} cannot {action}",
697 self.source_kind, self.operation,
698 ));
699 }
700 self.validate_peer_match(peer_id)?;
701 self.validate_trust_store_peer_match(trust_store_peer_id)?;
702 self.consume_once()
703 }
704
705 fn preflight_operation(
706 &self,
707 operation: GeneratedCommsTrustAuthorityOperation,
708 trust_store_peer_id: Option<PeerId>,
709 peer_id: PeerId,
710 action: &'static str,
711 ) -> Result<(), String> {
712 if self.operation != operation {
713 return Err(format!(
714 "trust authority from {:?} for {:?} cannot {action}",
715 self.source_kind, self.operation,
716 ));
717 }
718 self.validate_peer_match(peer_id)?;
719 self.validate_trust_store_peer_match(trust_store_peer_id)
720 }
721
722 fn validate_add_operation(
723 &self,
724 operation: GeneratedCommsTrustAuthorityOperation,
725 trust_store_peer_id: Option<PeerId>,
726 peer: &TrustedPeerDescriptor,
727 action: &'static str,
728 ) -> Result<(), String> {
729 if self.operation != operation {
730 return Err(format!(
731 "trust authority from {:?} for {:?} cannot {action}",
732 self.source_kind, self.operation,
733 ));
734 }
735 self.validate_peer_match(peer.peer_id)?;
736 self.validate_peer_descriptor_match(peer)?;
737 self.validate_trust_store_peer_match(trust_store_peer_id)?;
738 self.consume_once()
739 }
740
741 fn preflight_add_operation(
742 &self,
743 operation: GeneratedCommsTrustAuthorityOperation,
744 trust_store_peer_id: Option<PeerId>,
745 peer: &TrustedPeerDescriptor,
746 action: &'static str,
747 ) -> Result<(), String> {
748 if self.operation != operation {
749 return Err(format!(
750 "trust authority from {:?} for {:?} cannot {action}",
751 self.source_kind, self.operation,
752 ));
753 }
754 self.validate_peer_match(peer.peer_id)?;
755 self.validate_peer_descriptor_match(peer)?;
756 self.validate_trust_store_peer_match(trust_store_peer_id)
757 }
758
759 fn consume_once(&self) -> Result<(), String> {
760 self.consumed
761 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
762 .map(|_| ())
763 .map_err(|_| "generated comms trust authority was already consumed".to_string())
764 }
765
766 fn validate_peer_match(&self, peer_id: PeerId) -> Result<(), String> {
767 let expected = self.peer_id();
768 if expected == peer_id.to_string() {
769 Ok(())
770 } else {
771 Err(format!(
772 "trust authority peer_id {expected:?} does not match mutation peer_id {peer_id}"
773 ))
774 }
775 }
776
777 fn validate_trust_store_peer_match(
778 &self,
779 trust_store_peer_id: Option<PeerId>,
780 ) -> Result<(), String> {
781 let Some(expected) = self.trust_store_peer_id.as_deref() else {
782 return Ok(());
783 };
784 let Some(actual) = trust_store_peer_id else {
785 return Err(format!(
786 "trust authority from {:?} requires trust-store peer_id {expected:?}, but the target runtime did not expose one",
787 self.source_kind,
788 ));
789 };
790 if expected == actual.to_string() {
791 Ok(())
792 } else {
793 Err(format!(
794 "trust authority from {:?} for peer {:?} targets trust-store peer_id {expected:?}, not {actual}",
795 self.source_kind,
796 self.peer_id(),
797 ))
798 }
799 }
800
801 fn validate_peer_descriptor_match(&self, peer: &TrustedPeerDescriptor) -> Result<(), String> {
802 let Some(expected) = self.peer_descriptor.as_ref() else {
803 return Err(format!(
804 "trust authority from {:?} for {:?} did not carry a generated peer descriptor",
805 self.source_kind, self.operation,
806 ));
807 };
808 if expected == peer {
809 Ok(())
810 } else {
811 Err(format!(
812 "trust authority descriptor for peer {:?} does not match mutation descriptor",
813 self.peer_id()
814 ))
815 }
816 }
817
818 fn peer_id(&self) -> &str {
819 self.peer_id.as_str()
820 }
821
822 pub fn source_epoch(&self) -> u64 {
823 self.source_epoch
824 }
825
826 pub fn validate_source_owner_token(
827 &self,
828 expected: Option<&GeneratedPeerCommsOwnerToken>,
829 ) -> Result<(), String> {
830 let Some(actual) = self.source_owner_token.as_ref() else {
831 return Err(format!(
832 "trust authority from {:?} did not carry a generated owner token",
833 self.source_kind,
834 ));
835 };
836 let Some(expected) = expected else {
837 return Err(format!(
838 "trust authority from {:?} requires the target runtime's generated owner token",
839 self.source_kind,
840 ));
841 };
842 if expected.matches_raw_owner(actual) {
843 Ok(())
844 } else {
845 Err(format!(
846 "trust authority from {:?} was minted by a different generated owner",
847 self.source_kind,
848 ))
849 }
850 }
851
852 pub fn validate_target_source_owner_token(
853 &self,
854 expected_meerkat_machine_owner: Option<&GeneratedPeerCommsOwnerToken>,
855 expected_mob_machine_owner: Option<&Arc<dyn Any + Send + Sync>>,
856 ) -> Result<(), String> {
857 if is_meerkat_machine_trust_source(self.source_kind) {
858 self.validate_source_owner_token(expected_meerkat_machine_owner)
859 } else if is_mob_machine_trust_source(self.source_kind) {
860 self.validate_raw_source_owner_token(expected_mob_machine_owner)
861 } else {
862 Err(format!(
863 "trust authority from {:?} has no target owner validator",
864 self.source_kind,
865 ))
866 }
867 }
868
869 pub fn validate_raw_source_owner_token(
870 &self,
871 expected: Option<&Arc<dyn Any + Send + Sync>>,
872 ) -> Result<(), String> {
873 let Some(actual) = self.source_owner_token.as_ref() else {
874 return Err(format!(
875 "trust authority from {:?} did not carry a generated owner token",
876 self.source_kind,
877 ));
878 };
879 let Some(expected) = expected else {
880 return Err(format!(
881 "trust authority from {:?} requires the target runtime's generated owner token",
882 self.source_kind,
883 ));
884 };
885 if Arc::ptr_eq(actual, expected) {
886 Ok(())
887 } else {
888 Err(format!(
889 "trust authority from {:?} was minted by a different generated owner",
890 self.source_kind,
891 ))
892 }
893 }
894
895 pub fn is_mob_machine_source(&self) -> bool {
896 is_mob_machine_trust_source(self.source_kind)
897 }
898
899 pub fn trust_row_owner_kind(&self) -> GeneratedCommsTrustAuthoritySourceKind {
900 self.trust_row_owner_kind
901 }
902}
903
904#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
905#[allow(improper_ctypes_definitions, unsafe_code)]
906unsafe extern "Rust" {
907 #[link_name = concat!(
908 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_comms_trust_reconcile_",
909 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
910 )]
911 fn runtime_comms_trust_reconcile_generated_authority_bridge_token_is_valid(
912 token: &(dyn std::any::Any + Send + Sync),
913 ) -> bool;
914
915 #[link_name = concat!(
916 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_supervisor_trust_publish_",
917 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
918 )]
919 fn runtime_supervisor_trust_publish_generated_authority_bridge_token_is_valid(
920 token: &(dyn std::any::Any + Send + Sync),
921 ) -> bool;
922
923 #[link_name = concat!(
924 "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_supervisor_trust_revoke_",
925 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
926 )]
927 fn runtime_supervisor_trust_revoke_generated_authority_bridge_token_is_valid(
928 token: &(dyn std::any::Any + Send + Sync),
929 ) -> bool;
930
931 #[link_name = concat!(
932 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_member_trust_wiring_",
933 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
934 )]
935 fn mob_member_trust_wiring_generated_authority_bridge_token_is_valid(
936 token: &(dyn std::any::Any + Send + Sync),
937 ) -> bool;
938
939 #[link_name = concat!(
940 "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_member_trust_unwiring_",
941 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
942 )]
943 fn mob_member_trust_unwiring_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_external_peer_trust_wiring_",
949 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
950 )]
951 fn mob_external_peer_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_external_peer_trust_unwiring_",
957 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
958 )]
959 fn mob_external_peer_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_repair_",
965 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
966 )]
967 fn mob_external_peer_trust_repair_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_reciprocal_trust_",
973 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
974 )]
975 fn mob_external_peer_reciprocal_trust_generated_authority_bridge_token_is_valid(
976 token: &(dyn std::any::Any + Send + Sync),
977 ) -> bool;
978}
979
980#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
981#[doc(hidden)]
982#[allow(improper_ctypes_definitions, unsafe_code)]
983#[allow(clippy::too_many_arguments)]
984#[unsafe(export_name = concat!(
985 "__meerkat_core_runtime_generated_comms_trust_authority_build_v1_",
986 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
987))]
988pub(crate) extern "Rust" fn runtime_generated_comms_trust_authority_build(
989 token: &'static (dyn std::any::Any + Send + Sync),
990 source_kind: GeneratedCommsTrustAuthoritySourceKind,
991 source_epoch: u64,
992 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
993 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
994 operation: GeneratedCommsTrustAuthorityOperation,
995 peer_id: String,
996 trust_store_peer_id: Option<String>,
997 peer_descriptor: Option<TrustedPeerDescriptor>,
998) -> Result<CommsTrustMutationAuthority, String> {
999 validate_runtime_generated_authority_bridge_token(source_kind, token)?;
1000 validate_meerkat_machine_trust_source(source_kind, trust_row_owner_kind)?;
1001 CommsTrustMutationAuthority::from_generated_parts(
1002 source_kind,
1003 source_epoch,
1004 source_owner_token,
1005 trust_row_owner_kind,
1006 operation,
1007 peer_id,
1008 trust_store_peer_id,
1009 peer_descriptor,
1010 )
1011}
1012
1013#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1014#[doc(hidden)]
1015#[allow(improper_ctypes_definitions, unsafe_code)]
1016#[allow(clippy::too_many_arguments)]
1017#[unsafe(export_name = concat!(
1018 "__meerkat_core_mob_generated_comms_trust_authority_build_v1_",
1019 env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1020))]
1021pub(crate) extern "Rust" fn mob_generated_comms_trust_authority_build(
1022 token: &'static (dyn std::any::Any + Send + Sync),
1023 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1024 source_epoch: u64,
1025 source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
1026 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1027 operation: GeneratedCommsTrustAuthorityOperation,
1028 peer_id: String,
1029 trust_store_peer_id: Option<String>,
1030 peer_descriptor: Option<TrustedPeerDescriptor>,
1031) -> Result<CommsTrustMutationAuthority, String> {
1032 validate_mob_generated_authority_bridge_token(source_kind, token)?;
1033 validate_mob_machine_trust_source(source_kind, trust_row_owner_kind)?;
1034 CommsTrustMutationAuthority::from_generated_parts(
1035 source_kind,
1036 source_epoch,
1037 source_owner_token,
1038 trust_row_owner_kind,
1039 operation,
1040 peer_id,
1041 trust_store_peer_id,
1042 peer_descriptor,
1043 )
1044}
1045
1046#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1047fn validate_runtime_generated_authority_bridge_token(
1048 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1049 token: &(dyn std::any::Any + Send + Sync),
1050) -> Result<(), String> {
1051 #[allow(unsafe_code)]
1052 let valid = unsafe {
1053 match source_kind {
1054 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection => {
1055 runtime_comms_trust_reconcile_generated_authority_bridge_token_is_valid(token)
1056 }
1057 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorPublish => {
1058 runtime_supervisor_trust_publish_generated_authority_bridge_token_is_valid(token)
1059 }
1060 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorRevoke => {
1061 runtime_supervisor_trust_revoke_generated_authority_bridge_token_is_valid(token)
1062 }
1063 _ => false,
1064 }
1065 };
1066 if valid {
1067 Ok(())
1068 } else {
1069 Err("generated comms trust authority requires the matching generated runtime protocol bridge token".into())
1070 }
1071}
1072
1073#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1074fn validate_mob_generated_authority_bridge_token(
1075 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1076 token: &(dyn std::any::Any + Send + Sync),
1077) -> Result<(), String> {
1078 #[allow(unsafe_code)]
1079 let valid = unsafe {
1080 match source_kind {
1081 GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring => {
1082 mob_member_trust_wiring_generated_authority_bridge_token_is_valid(token)
1083 }
1084 GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustUnwiring => {
1085 mob_member_trust_unwiring_generated_authority_bridge_token_is_valid(token)
1086 }
1087 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustWiring => {
1088 mob_external_peer_trust_wiring_generated_authority_bridge_token_is_valid(token)
1089 }
1090 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustUnwiring => {
1091 mob_external_peer_trust_unwiring_generated_authority_bridge_token_is_valid(token)
1092 }
1093 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustRepair => {
1094 mob_external_peer_trust_repair_generated_authority_bridge_token_is_valid(token)
1095 }
1096 GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerReciprocalTrust => {
1097 mob_external_peer_reciprocal_trust_generated_authority_bridge_token_is_valid(token)
1098 }
1099 _ => false,
1100 }
1101 };
1102 if valid {
1103 Ok(())
1104 } else {
1105 Err("generated comms trust authority requires the matching generated MobMachine protocol bridge token".into())
1106 }
1107}
1108
1109#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1110fn validate_meerkat_machine_trust_source(
1111 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1112 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1113) -> Result<(), String> {
1114 if is_meerkat_machine_trust_source(source_kind)
1115 && is_meerkat_machine_trust_source(trust_row_owner_kind)
1116 {
1117 Ok(())
1118 } else {
1119 Err(format!(
1120 "runtime generated comms trust authority cannot package source {source_kind:?} with row owner {trust_row_owner_kind:?}"
1121 ))
1122 }
1123}
1124
1125#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1126fn validate_mob_machine_trust_source(
1127 source_kind: GeneratedCommsTrustAuthoritySourceKind,
1128 trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1129) -> Result<(), String> {
1130 if is_mob_machine_trust_source(source_kind) && is_mob_machine_trust_source(trust_row_owner_kind)
1131 {
1132 Ok(())
1133 } else {
1134 Err(format!(
1135 "mob generated comms trust authority cannot package source {source_kind:?} with row owner {trust_row_owner_kind:?}"
1136 ))
1137 }
1138}
1139
1140fn is_meerkat_machine_trust_source(kind: GeneratedCommsTrustAuthoritySourceKind) -> bool {
1141 matches!(
1142 kind,
1143 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection
1144 | GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorPublish
1145 | GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorRevoke
1146 )
1147}
1148
1149fn is_mob_machine_trust_source(kind: GeneratedCommsTrustAuthoritySourceKind) -> bool {
1150 matches!(
1151 kind,
1152 GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring
1153 | GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustUnwiring
1154 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustWiring
1155 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustUnwiring
1156 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustRepair
1157 | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerReciprocalTrust
1158 )
1159}
1160
1161#[derive(Debug, Clone)]
1163pub enum CommsTrustMutation {
1164 AddTrustedPeer {
1165 peer: TrustedPeerDescriptor,
1166 authority: CommsTrustMutationAuthority,
1167 },
1168 RemoveTrustedPeer {
1169 peer_id: String,
1170 authority: CommsTrustMutationAuthority,
1171 },
1172 AddPrivateTrustedPeer {
1173 peer: TrustedPeerDescriptor,
1174 authority: CommsTrustMutationAuthority,
1175 },
1176 RemovePrivateTrustedPeer {
1177 peer_id: String,
1178 authority: CommsTrustMutationAuthority,
1179 },
1180}
1181
1182#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1185pub enum CommsTrustMutationResult {
1186 Added { created: bool },
1187 Removed { removed: bool },
1188}
1189
1190impl TrustedPeerDescriptor {
1191 pub fn pubkey_is_zero(pubkey: &[u8; 32]) -> bool {
1192 *pubkey == [0u8; 32]
1193 }
1194
1195 pub fn has_zero_pubkey(&self) -> bool {
1196 Self::pubkey_is_zero(&self.pubkey)
1197 }
1198
1199 pub fn validate_pubkey_for_peer_id(peer_id: PeerId, pubkey: &[u8; 32]) -> Result<(), String> {
1200 if Self::pubkey_is_zero(pubkey) {
1201 return Err("TrustedPeerDescriptor.pubkey must be non-zero".to_string());
1202 }
1203 let derived = PeerId::from_ed25519_pubkey(pubkey);
1204 if derived != peer_id {
1205 return Err(format!(
1206 "peer_id {peer_id} does not match pubkey-derived id {derived}"
1207 ));
1208 }
1209 Ok(())
1210 }
1211
1212 pub fn test_only_unsigned(
1225 name: impl Into<String>,
1226 peer_id: impl AsRef<str>,
1227 address: impl AsRef<str>,
1228 ) -> Result<Self, String> {
1229 let name = PeerName::new(name).map_err(|e| format!("invalid peer name: {e}"))?;
1230 let peer_id =
1231 PeerId::parse(peer_id.as_ref()).map_err(|e| format!("invalid peer_id: {e}"))?;
1232 let address = PeerAddress::parse(address.as_ref()).map_err(|e| e.to_string())?;
1233 Ok(Self {
1234 peer_id,
1235 name,
1236 address,
1237 pubkey: [0u8; 32],
1238 })
1239 }
1240
1241 pub fn test_only_unsigned_typed(
1261 name: impl Into<String>,
1262 peer_id: PeerId,
1263 address: impl AsRef<str>,
1264 ) -> Result<Self, String> {
1265 let name = PeerName::new(name).map_err(|e| format!("invalid peer name: {e}"))?;
1266 let address = PeerAddress::parse(address.as_ref()).map_err(|e| e.to_string())?;
1267 Ok(Self {
1268 peer_id,
1269 name,
1270 address,
1271 pubkey: [0u8; 32],
1272 })
1273 }
1274
1275 pub fn with_pubkey(mut self, pubkey: [u8; 32]) -> Self {
1281 self.pubkey = pubkey;
1282 self
1283 }
1284
1285 pub fn unsigned_with_pubkey(
1302 name: impl Into<String>,
1303 peer_id: impl AsRef<str>,
1304 pubkey: [u8; 32],
1305 address: impl AsRef<str>,
1306 ) -> Result<Self, String> {
1307 let mut descriptor = Self::test_only_unsigned(name, peer_id, address)?;
1308 Self::validate_pubkey_for_peer_id(descriptor.peer_id, &pubkey)?;
1309 descriptor.pubkey = pubkey;
1310 Ok(descriptor)
1311 }
1312}
1313
1314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1320#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1321pub enum PeerLifecycleKind {
1322 #[serde(rename = "mob.peer_added")]
1323 PeerAdded,
1324 #[serde(rename = "mob.peer_retired")]
1325 PeerRetired,
1326 #[serde(rename = "mob.peer_unwired")]
1327 PeerUnwired,
1328 #[serde(rename = "mob.dismiss")]
1334 Dismiss,
1335}
1336
1337impl PeerLifecycleKind {
1338 pub const fn as_str(self) -> &'static str {
1339 match self {
1340 Self::PeerAdded => "mob.peer_added",
1341 Self::PeerRetired => "mob.peer_retired",
1342 Self::PeerUnwired => "mob.peer_unwired",
1343 Self::Dismiss => "mob.dismiss",
1344 }
1345 }
1346}
1347
1348impl std::fmt::Display for PeerLifecycleKind {
1349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1350 f.write_str(self.as_str())
1351 }
1352}
1353
1354#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1366#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1367#[serde(rename_all = "snake_case")]
1368pub enum SenderContentTaint {
1369 Clean,
1371 Tainted,
1374}
1375
1376impl SenderContentTaint {
1377 pub const fn as_str(self) -> &'static str {
1378 match self {
1379 Self::Clean => "clean",
1380 Self::Tainted => "tainted",
1381 }
1382 }
1383}
1384
1385impl std::fmt::Display for SenderContentTaint {
1386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1387 f.write_str(self.as_str())
1388 }
1389}
1390
1391#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1401#[serde(rename_all = "snake_case")]
1402pub enum SendTaintOverride {
1403 Declare(SenderContentTaint),
1405 Undeclared,
1407}
1408
1409#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1422#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1423pub enum CommsCommandRequest {
1424 Input {
1426 body: String,
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 blocks: Option<Vec<ContentBlock>>,
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1430 source: Option<InputSource>,
1431 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 stream: Option<InputStreamMode>,
1433 #[serde(default, skip_serializing_if = "Option::is_none")]
1434 handling_mode: Option<HandlingMode>,
1435 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 allow_self_session: Option<bool>,
1437 },
1438 PeerMessage {
1440 to: PeerId,
1441 body: String,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 blocks: Option<Vec<ContentBlock>>,
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 content_taint: Option<SendTaintOverride>,
1446 #[serde(default, skip_serializing_if = "Option::is_none")]
1447 handling_mode: Option<HandlingMode>,
1448 },
1449 PeerLifecycle {
1451 to: PeerId,
1452 lifecycle_kind: PeerLifecycleKind,
1453 #[serde(default)]
1454 params: serde_json::Value,
1455 },
1456 PeerRequest {
1458 to: PeerId,
1459 intent: CommsPeerRequestIntent,
1462 #[serde(default)]
1463 params: serde_json::Value,
1464 #[serde(default, skip_serializing_if = "Option::is_none")]
1465 blocks: Option<Vec<ContentBlock>>,
1466 #[serde(default, skip_serializing_if = "Option::is_none")]
1467 content_taint: Option<SendTaintOverride>,
1468 #[serde(default, skip_serializing_if = "Option::is_none")]
1469 handling_mode: Option<HandlingMode>,
1470 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 stream: Option<InputStreamMode>,
1472 },
1473 PeerResponse {
1475 to: PeerId,
1476 in_reply_to: InteractionId,
1477 status: ResponseStatus,
1478 #[serde(default)]
1479 result: 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 },
1487}
1488
1489#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1494pub enum CommsCommandError {
1495 #[error("handling_mode is forbidden on progress peer responses")]
1500 HandlingModeForbiddenForProgressResponse,
1501}
1502
1503impl CommsCommandRequest {
1504 pub fn into_command(
1509 self,
1510 session_id: &crate::types::SessionId,
1511 ) -> Result<CommsCommand, CommsCommandError> {
1512 Ok(match self {
1513 CommsCommandRequest::Input {
1514 body,
1515 blocks,
1516 source,
1517 stream,
1518 handling_mode,
1519 allow_self_session,
1520 } => CommsCommand::Input {
1521 session_id: session_id.clone(),
1522 body,
1523 blocks,
1524 handling_mode: handling_mode.unwrap_or_default(),
1525 source: source.unwrap_or(InputSource::Rpc),
1526 stream: stream.unwrap_or(InputStreamMode::None),
1527 allow_self_session: allow_self_session.unwrap_or(false),
1528 },
1529 CommsCommandRequest::PeerMessage {
1530 to,
1531 body,
1532 blocks,
1533 content_taint,
1534 handling_mode,
1535 } => CommsCommand::PeerMessage {
1536 to: PeerRoute::new(to),
1537 body,
1538 blocks,
1539 content_taint,
1540 handling_mode: handling_mode.unwrap_or_default(),
1541 },
1542 CommsCommandRequest::PeerLifecycle {
1543 to,
1544 lifecycle_kind,
1545 params,
1546 } => CommsCommand::PeerLifecycle {
1547 to: PeerRoute::new(to),
1548 kind: lifecycle_kind,
1549 params,
1550 },
1551 CommsCommandRequest::PeerRequest {
1552 to,
1553 intent,
1554 params,
1555 blocks,
1556 content_taint,
1557 handling_mode,
1558 stream,
1559 } => CommsCommand::PeerRequest {
1560 to: PeerRoute::new(to),
1561 intent: intent.as_str().to_string(),
1567 params,
1568 blocks,
1569 content_taint,
1570 handling_mode: handling_mode.unwrap_or_default(),
1571 stream: stream.unwrap_or(InputStreamMode::None),
1572 },
1573 CommsCommandRequest::PeerResponse {
1574 to,
1575 in_reply_to,
1576 status,
1577 result,
1578 blocks,
1579 content_taint,
1580 handling_mode,
1581 } => CommsCommand::PeerResponse {
1582 to: PeerRoute::new(to),
1583 in_reply_to,
1584 status,
1585 result,
1586 blocks,
1587 content_taint,
1588 handling_mode,
1589 },
1590 })
1591 }
1592
1593 pub fn kind(&self) -> &'static str {
1595 match self {
1596 Self::Input { .. } => "input",
1597 Self::PeerMessage { .. } => "peer_message",
1598 Self::PeerLifecycle { .. } => "peer_lifecycle",
1599 Self::PeerRequest { .. } => "peer_request",
1600 Self::PeerResponse { .. } => "peer_response",
1601 }
1602 }
1603}
1604#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1607#[serde(rename_all = "lowercase")]
1608pub enum InputSource {
1609 Tcp,
1610 Uds,
1611 Stdin,
1612 Webhook,
1613 Rpc,
1614}
1615
1616impl From<crate::config::PlainEventSource> for InputSource {
1617 fn from(source: crate::config::PlainEventSource) -> Self {
1618 match source {
1619 crate::config::PlainEventSource::Tcp => Self::Tcp,
1620 crate::config::PlainEventSource::Uds => Self::Uds,
1621 crate::config::PlainEventSource::Stdin => Self::Stdin,
1622 crate::config::PlainEventSource::Webhook => Self::Webhook,
1623 crate::config::PlainEventSource::Rpc => Self::Rpc,
1624 }
1625 }
1626}
1627
1628impl From<InputSource> for crate::config::PlainEventSource {
1629 fn from(source: InputSource) -> Self {
1630 match source {
1631 InputSource::Tcp => Self::Tcp,
1632 InputSource::Uds => Self::Uds,
1633 InputSource::Stdin => Self::Stdin,
1634 InputSource::Webhook => Self::Webhook,
1635 InputSource::Rpc => Self::Rpc,
1636 }
1637 }
1638}
1639
1640#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1643#[serde(rename_all = "snake_case")]
1644pub enum InputStreamMode {
1645 None,
1647 ReserveInteraction,
1649}
1650
1651#[derive(Debug, Clone, PartialEq, Eq)]
1653pub enum CommsCommand {
1654 Input {
1656 session_id: crate::types::SessionId,
1657 body: String,
1658 blocks: Option<Vec<ContentBlock>>,
1659 handling_mode: HandlingMode,
1660 source: InputSource,
1661 stream: InputStreamMode,
1662 allow_self_session: bool,
1663 },
1664 PeerMessage {
1666 to: PeerRoute,
1667 body: String,
1668 blocks: Option<Vec<ContentBlock>>,
1669 content_taint: Option<SendTaintOverride>,
1671 handling_mode: HandlingMode,
1672 },
1673 PeerLifecycle {
1675 to: PeerRoute,
1676 kind: PeerLifecycleKind,
1677 params: serde_json::Value,
1678 },
1679 PeerRequest {
1681 to: PeerRoute,
1682 intent: String,
1683 params: serde_json::Value,
1684 blocks: Option<Vec<ContentBlock>>,
1685 content_taint: Option<SendTaintOverride>,
1687 handling_mode: HandlingMode,
1688 stream: InputStreamMode,
1689 },
1690 PeerResponse {
1692 to: PeerRoute,
1693 in_reply_to: InteractionId,
1694 status: ResponseStatus,
1695 result: serde_json::Value,
1696 blocks: Option<Vec<ContentBlock>>,
1697 content_taint: Option<SendTaintOverride>,
1699 handling_mode: Option<HandlingMode>,
1700 },
1701}
1702
1703impl CommsCommand {
1704 pub fn command_kind(&self) -> &'static str {
1705 match self {
1706 Self::Input { .. } => "input",
1707 Self::PeerMessage { .. } => "peer_message",
1708 Self::PeerLifecycle { .. } => "peer_lifecycle",
1709 Self::PeerRequest { .. } => "peer_request",
1710 Self::PeerResponse { .. } => "peer_response",
1711 }
1712 }
1713}
1714
1715#[derive(Debug, Clone, PartialEq, Eq)]
1717pub enum SendReceipt {
1718 InputAccepted {
1719 interaction_id: InteractionId,
1720 stream_reserved: bool,
1721 },
1722 PeerMessageSent {
1723 envelope_id: uuid::Uuid,
1724 acked: bool,
1725 },
1726 PeerLifecycleSent {
1727 envelope_id: uuid::Uuid,
1728 },
1729 PeerRequestSent {
1730 envelope_id: uuid::Uuid,
1731 interaction_id: InteractionId,
1732 stream_reserved: bool,
1733 },
1734 PeerResponseSent {
1735 envelope_id: uuid::Uuid,
1736 in_reply_to: InteractionId,
1737 },
1738}
1739
1740#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1741#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1742#[serde(rename_all = "snake_case")]
1743pub enum PeerDirectorySource {
1744 Trusted,
1745 Inproc,
1746 TrustedAndInproc,
1747 Unknown,
1748}
1749
1750impl PeerDirectorySource {
1751 pub const fn as_str(&self) -> &'static str {
1752 match self {
1753 Self::Trusted => "trusted",
1754 Self::Inproc => "inproc",
1755 Self::TrustedAndInproc => "trusted_and_inproc",
1756 Self::Unknown => "unknown",
1757 }
1758 }
1759}
1760
1761impl std::fmt::Display for PeerDirectorySource {
1762 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1763 f.write_str(self.as_str())
1764 }
1765}
1766
1767#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1768#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1769#[serde(rename_all = "snake_case")]
1770pub enum PeerSendability {
1771 PeerMessage,
1772 PeerRequest,
1773 PeerResponse,
1774}
1775
1776impl PeerSendability {
1777 pub const DIRECTORY_DEFAULTS: [Self; 3] =
1778 [Self::PeerMessage, Self::PeerRequest, Self::PeerResponse];
1779
1780 pub fn directory_defaults() -> Vec<Self> {
1781 Self::DIRECTORY_DEFAULTS.to_vec()
1782 }
1783
1784 pub const fn as_str(&self) -> &'static str {
1785 match self {
1786 Self::PeerMessage => "peer_message",
1787 Self::PeerRequest => "peer_request",
1788 Self::PeerResponse => "peer_response",
1789 }
1790 }
1791}
1792
1793impl std::fmt::Display for PeerSendability {
1794 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1795 f.write_str(self.as_str())
1796 }
1797}
1798
1799#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1805#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1806pub struct PeerCapabilitySet {
1807 #[serde(default = "PeerCapabilitySet::default_version")]
1808 pub version: u16,
1809 #[serde(default)]
1810 pub extensions: BTreeMap<String, serde_json::Value>,
1811}
1812
1813impl PeerCapabilitySet {
1814 pub const CURRENT_VERSION: u16 = 1;
1815
1816 const fn default_version() -> u16 {
1817 Self::CURRENT_VERSION
1818 }
1819
1820 pub fn with_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
1821 self.extensions.insert(key.into(), value);
1822 self
1823 }
1824}
1825
1826impl Default for PeerCapabilitySet {
1827 fn default() -> Self {
1828 Self {
1829 version: Self::CURRENT_VERSION,
1830 extensions: BTreeMap::new(),
1831 }
1832 }
1833}
1834
1835#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1836#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1837pub struct PeerDirectoryEntry {
1838 pub peer_id: PeerId,
1840 pub name: PeerName,
1843 pub address: PeerAddress,
1847 pub source: PeerDirectorySource,
1848 pub sendable_kinds: Vec<PeerSendability>,
1849 pub capabilities: PeerCapabilitySet,
1850 pub meta: crate::PeerMeta,
1852}
1853
1854#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1856pub struct PeerDirectoryListing {
1857 pub peers: Vec<PeerDirectoryEntry>,
1858}
1859
1860impl PeerDirectoryListing {
1861 pub fn new(peers: Vec<PeerDirectoryEntry>) -> Self {
1862 Self { peers }
1863 }
1864}
1865
1866impl From<Vec<PeerDirectoryEntry>> for PeerDirectoryListing {
1867 fn from(peers: Vec<PeerDirectoryEntry>) -> Self {
1868 Self::new(peers)
1869 }
1870}
1871
1872#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1874pub enum StreamScope {
1875 Session(crate::types::SessionId),
1876 Interaction(InteractionId),
1877}
1878
1879pub type EventStream = Pin<Box<dyn Stream<Item = EventEnvelope<AgentEvent>> + Send>>;
1881
1882#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
1884pub enum StreamError {
1885 #[error("interaction not reserved: {0}")]
1886 NotReserved(InteractionId),
1887 #[error("stream not found: {0}")]
1888 NotFound(String),
1889 #[error("already attached: {0}")]
1890 AlreadyAttached(InteractionId),
1891 #[error("stream closed")]
1892 Closed,
1893 #[error("permission denied: {0}")]
1894 PermissionDenied(String),
1895 #[error("timeout: {0}")]
1896 Timeout(String),
1897 #[error("internal: {0}")]
1898 Internal(String),
1899}
1900
1901#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1909#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1910#[serde(rename_all = "snake_case")]
1911#[non_exhaustive]
1912pub enum AdmissionDropReason {
1913 UntrustedSender,
1916 ClassificationRejected,
1918 SessionClosed,
1920 InboxFull,
1922}
1923
1924impl AdmissionDropReason {
1925 pub fn as_code(&self) -> &'static str {
1928 match self {
1929 AdmissionDropReason::UntrustedSender => "untrusted_sender",
1930 AdmissionDropReason::ClassificationRejected => "classification_rejected",
1931 AdmissionDropReason::SessionClosed => "session_closed",
1932 AdmissionDropReason::InboxFull => "inbox_full",
1933 }
1934 }
1935}
1936
1937#[derive(Debug, Clone, thiserror::Error)]
1938#[non_exhaustive]
1939pub enum SendError {
1940 #[error("peer not found: {0}")]
1941 PeerNotFound(String),
1942 #[error("peer offline")]
1943 PeerOffline,
1944 #[error("peer not sendable")]
1945 PeerNotSendable(String),
1946 #[error("input stream closed")]
1947 InputClosed,
1948 #[error("unsupported command: {0}")]
1949 Unsupported(String),
1950 #[error("validation failed: {0}")]
1951 Validation(String),
1952 #[error("internal: {0}")]
1953 Internal(String),
1954 #[error("transport error: {0}")]
1961 Transport(String),
1962 #[error("peer dropped at admission: {reason:?}")]
1967 AdmissionDropped { reason: AdmissionDropReason },
1968}
1969
1970#[derive(Debug, Clone, thiserror::Error)]
1971pub enum SendAndStreamError {
1972 #[error("send failed: {0}")]
1973 Send(#[from] SendError),
1974 #[error("stream attach failed: receipt={receipt:?}, error={error}")]
1975 StreamAttach {
1976 receipt: SendReceipt,
1977 error: StreamError,
1978 },
1979}
1980
1981#[cfg(test)]
1982#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1983mod tests {
1984 use super::*;
1985
1986 #[test]
1987 fn peer_id_pubkey_derivation_matches_uuid_v5() {
1988 let pubkey = [42u8; 32];
1989 assert_eq!(
1990 PeerId::from_ed25519_pubkey(&pubkey).as_uuid(),
1991 &Uuid::new_v5(&PEER_ID_ED25519_PUBKEY_NAMESPACE, &pubkey)
1992 );
1993 }
1994
1995 #[test]
1996 fn peer_name_validation() {
1997 assert!(PeerName::new("alice").is_ok());
1998 assert!(PeerName::new("".to_string()).is_err());
1999 assert!(PeerName::new("bad\x00name").is_err());
2000 }
2001
2002 #[test]
2003 fn peer_directory_entry_fields() -> Result<(), String> {
2004 let entry = PeerDirectoryEntry {
2005 peer_id: PeerId::new(),
2006 name: PeerName::new("agent")?,
2007 address: PeerAddress::new(PeerTransport::Inproc, "agent"),
2008 source: PeerDirectorySource::Inproc,
2009 sendable_kinds: vec![PeerSendability::PeerMessage],
2010 capabilities: PeerCapabilitySet::default(),
2011 meta: crate::PeerMeta::default(),
2012 };
2013 assert_eq!(entry.name.as_str(), "agent");
2014 assert_eq!(entry.address.transport(), PeerTransport::Inproc);
2015 assert_eq!(entry.address.endpoint(), "agent");
2016 assert_eq!(entry.source, PeerDirectorySource::Inproc);
2017 Ok(())
2018 }
2019
2020 #[test]
2021 fn peer_directory_listing_serializes_typed_source_sendability_and_capabilities()
2022 -> Result<(), String> {
2023 let entry = PeerDirectoryEntry {
2024 peer_id: PeerId::new(),
2025 name: PeerName::new("agent")?,
2026 address: PeerAddress::new(PeerTransport::Inproc, "agent"),
2027 source: PeerDirectorySource::Inproc,
2028 sendable_kinds: vec![PeerSendability::PeerMessage, PeerSendability::PeerRequest],
2029 capabilities: PeerCapabilitySet::default()
2030 .with_extension("vendor.echo", serde_json::json!({ "enabled": true })),
2031 meta: crate::PeerMeta::default(),
2032 };
2033
2034 let value = serde_json::to_value(PeerDirectoryListing::new(vec![entry]))
2035 .map_err(|err| err.to_string())?;
2036 let peer = &value["peers"][0];
2037
2038 assert_eq!(peer["source"], "inproc");
2039 assert_eq!(
2040 peer["sendable_kinds"],
2041 serde_json::json!(["peer_message", "peer_request"])
2042 );
2043 assert_eq!(peer["capabilities"]["version"], 1);
2044 assert_eq!(
2045 peer["capabilities"]["extensions"]["vendor.echo"]["enabled"],
2046 true
2047 );
2048 Ok(())
2049 }
2050
2051 #[test]
2052 fn generated_trust_authority_rejects_descriptor_peer_mismatch() {
2053 let pubkey = [1u8; 32];
2054 let descriptor_peer_id = PeerId::from_ed25519_pubkey(&pubkey);
2055 let requested_peer_id = PeerId::from_ed25519_pubkey(&[2u8; 32]);
2056 let descriptor = TrustedPeerDescriptor::unsigned_with_pubkey(
2057 "fake",
2058 descriptor_peer_id.to_string(),
2059 pubkey,
2060 "inproc://fake",
2061 )
2062 .expect("valid descriptor");
2063 let err = CommsTrustMutationAuthority::from_generated_parts(
2064 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
2065 1,
2066 None,
2067 GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
2068 GeneratedCommsTrustAuthorityOperation::PublicAdd,
2069 requested_peer_id.to_string(),
2070 Some(requested_peer_id.to_string()),
2071 Some(descriptor),
2072 )
2073 .expect_err("descriptor for another peer must not mint authority");
2074 assert!(
2075 err.contains("does not match requested"),
2076 "unexpected rejection: {err}"
2077 );
2078 }
2079
2080 #[test]
2081 fn peer_id_parse_round_trip() {
2082 let id = PeerId::new();
2083 let parsed = PeerId::parse(&id.as_str()).expect("parse");
2084 assert_eq!(id, parsed);
2085 }
2086
2087 #[test]
2088 fn peer_id_parse_rejects_garbage() {
2089 let err = PeerId::parse("not-a-uuid").expect_err("parse must reject");
2090 match err {
2091 PeerIdError::Invalid { input, .. } => assert_eq!(input, "not-a-uuid"),
2092 }
2093 }
2094
2095 #[test]
2096 fn peer_address_display() {
2097 let addr = PeerAddress::new(PeerTransport::Tcp, "127.0.0.1:4200");
2098 assert_eq!(addr.to_string(), "tcp://127.0.0.1:4200");
2099 }
2100
2101 #[test]
2102 fn peer_address_parse_round_trips_supported_schemes() {
2103 let cases = [
2104 ("inproc://agent-a", PeerTransport::Inproc, "agent-a"),
2105 (
2106 "uds:///tmp/meerkat.sock",
2107 PeerTransport::Uds,
2108 "/tmp/meerkat.sock",
2109 ),
2110 ("tcp://127.0.0.1:4200", PeerTransport::Tcp, "127.0.0.1:4200"),
2111 ];
2112
2113 for (raw, transport, endpoint) in cases {
2114 let parsed = PeerAddress::parse(raw).expect("supported address parses");
2115 assert_eq!(parsed.transport(), transport);
2116 assert_eq!(parsed.endpoint(), endpoint);
2117 assert_eq!(parsed.to_string(), raw);
2118 }
2119 }
2120
2121 #[test]
2122 fn peer_address_parse_rejects_unknown_scheme() {
2123 let err = PeerAddress::parse("http://127.0.0.1:4200")
2124 .expect_err("unknown transport schemes must fail closed");
2125 assert!(
2126 err.to_string().contains("unknown peer address transport"),
2127 "unexpected error: {err}",
2128 );
2129 }
2130
2131 #[test]
2132 fn peer_address_parse_rejects_schemeless_input() {
2133 let err = PeerAddress::parse("127.0.0.1:4200")
2134 .expect_err("strict parser requires an address scheme");
2135 assert!(
2136 err.to_string().contains("missing transport scheme"),
2137 "unexpected error: {err}",
2138 );
2139 }
2140
2141 #[test]
2142 fn input_stream_mode_roundtrip() -> Result<(), serde_json::Error> {
2143 let mode = InputStreamMode::ReserveInteraction;
2144 let serialized = serde_json::to_value(mode)?;
2145 assert_eq!(serialized.as_str(), Some("reserve_interaction"));
2146 assert_eq!(serde_json::from_value::<InputStreamMode>(serialized)?, mode);
2147 Ok(())
2148 }
2149
2150 #[test]
2151 fn deserialize_input_with_typed_source() -> Result<(), serde_json::Error> {
2152 let json = r#"{"kind":"input","body":"hello","source":"webhook","handling_mode":"steer"}"#;
2153 let req: CommsCommandRequest = serde_json::from_str(json)?;
2154 match req {
2155 CommsCommandRequest::Input {
2156 body,
2157 source,
2158 handling_mode,
2159 ..
2160 } => {
2161 assert_eq!(body, "hello");
2162 assert_eq!(source, Some(InputSource::Webhook));
2163 assert_eq!(handling_mode, Some(HandlingMode::Steer));
2164 }
2165 other => panic!("expected input command request, got {other:?}"),
2166 }
2167 Ok(())
2168 }
2169
2170 #[test]
2171 fn deserialize_input_invalid_source_rejects_at_serde_boundary() {
2172 let json = r#"{"kind":"input","body":"hello","source":"webhookd"}"#;
2173 let err = serde_json::from_str::<CommsCommandRequest>(json)
2174 .expect_err("invalid source must fail deserialization");
2175 let msg = err.to_string();
2176 assert!(
2178 msg.contains("webhookd"),
2179 "error should name the rejected value, got: {msg}"
2180 );
2181 }
2182
2183 #[test]
2184 fn deserialize_unknown_kind_rejects_at_serde_boundary() {
2185 let json = r#"{"kind":"foobar","body":"hello"}"#;
2186 let err = serde_json::from_str::<CommsCommandRequest>(json)
2187 .expect_err("unknown kind must fail deserialization");
2188 let msg = err.to_string();
2189 assert!(
2190 msg.contains("foobar") || msg.contains("variant"),
2191 "error should mention unknown variant, got: {msg}"
2192 );
2193 }
2194}