Skip to main content

meerkat_core/
comms.rs

1//! Canonical communication API types for Meerkat.
2//!
3//! This module defines the public contract for comms command, response, and stream
4//! controls. It intentionally stays transport-agnostic and keeps names stable for
5//! the host and SDK surface migration work.
6
7use 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
21/// Comms request intent used for all supervisor bridge commands.
22///
23/// This is auth-exempt at peer ingress so a supervisor can complete the
24/// bootstrap handshake before the private trust edge exists. Keep the literal
25/// under core ingress authority; transport crates should compare through typed
26/// core policy rather than owning a local string exemption.
27pub const SUPERVISOR_BRIDGE_INTENT: &str = "supervisor.bridge";
28
29/// Closed request-intent vocabulary for [`CommsCommandRequest::PeerRequest`].
30///
31/// This is the canonical, core-owned set of intents a public `peer_request`
32/// command may carry. Unknown strings fail at the serde deserialization
33/// boundary and cannot fall through to a local match or string default — the
34/// closed set is enforced structurally, not by a runtime string comparison.
35///
36/// The domain envelope [`CommsCommand::PeerRequest`] intentionally keeps a wider
37/// open intent space (it also carries mob topology intents such as
38/// `mob.peer_added`); this enum is the narrow vocabulary admitted at the public
39/// request surface. Surfaces that accept the public comms contract re-import
40/// this type so they share the same fail-closed guarantee.
41#[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    /// Stable wire literal for this intent.
52    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/// Canonical runtime identity for a peer.
67///
68/// `PeerId` is the routing key: the router and trust store key by `PeerId`,
69/// never by `PeerName`. Two peers may legitimately share a display `PeerName`
70/// (per the Wave-B V5 dogma note), but their `PeerId`s never collide — the
71/// underlying UUID is globally unique.
72///
73/// Constructed freshly (`PeerId::new`) for a peer minted locally, parsed
74/// from a hyphenated UUID (`PeerId::parse`) when we've been given an identity
75/// over the wire, or derived from a 32-byte Ed25519 public key when a transport
76/// still authenticates by raw signing key.
77#[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
81/// UUIDv5 namespace for deriving [`PeerId`] from an Ed25519 signing pubkey.
82///
83/// `PeerId` is the canonical runtime routing key: both the router and the
84/// trust store index peers by `PeerId`, never by display name. The derivation
85/// is a content hash of the 32-byte public key so a given key always resolves
86/// to the same `PeerId` across runtimes.
87const PEER_ID_ED25519_PUBKEY_NAMESPACE: Uuid =
88    Uuid::from_u128(0x6d65_6572_6b61_7450_6565_7249_6430_0001);
89
90impl PeerId {
91    /// Mint a new `PeerId` with a fresh UUID v7 (time-ordered).
92    pub fn new() -> Self {
93        Self(crate::time_compat::new_uuid_v7())
94    }
95
96    /// Wrap an existing UUID.
97    pub const fn from_uuid(uuid: Uuid) -> Self {
98        Self(uuid)
99    }
100
101    /// Parse a hyphenated UUID string into a `PeerId`.
102    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    /// Derive the canonical routing id for a 32-byte Ed25519 public key.
112    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    /// Hyphenated UUID string form.
120    pub fn as_str(&self) -> String {
121        self.0.to_string()
122    }
123
124    /// Borrow the underlying UUID.
125    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/// Error parsing a [`PeerId`] from a string.
233#[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/// Typed transport atom for a peer address.
244///
245/// Replaces the old free-form `address: String` on `PeerDirectoryEntry` so
246/// callers cannot accidentally invent new transports by string concatenation
247/// at a call site.
248#[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    /// In-process routing within this runtime (no network hop).
254    Inproc,
255    /// Unix domain socket.
256    Uds,
257    /// TCP endpoint.
258    Tcp,
259}
260
261impl PeerTransport {
262    /// Stable short code used as the URI scheme half of a peer address.
263    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/// Typed peer address: transport atom plus endpoint string.
279///
280/// The `endpoint` is transport-specific (path for `Uds`, `host:port` for
281/// `Tcp`, agent name for `Inproc`) but is carried as a validated `String`
282/// so the transport atom can be branched on without re-parsing.
283#[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/// Error parsing a typed [`PeerAddress`] from its URI-shaped string form.
291#[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    /// Strictly parse `scheme://endpoint` peer addresses.
316    ///
317    /// Only the currently supported transport schemes are accepted. Unknown
318    /// schemes and schemeless input fail closed so callers cannot silently
319    /// reinterpret address truth as TCP.
320    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/// Display-only slug for a peer.
373///
374/// `PeerName` is **not** a routing key after Wave-B V5: the router resolves
375/// sends by [`PeerId`], and trust stores are keyed by [`PeerId`]. `PeerName`
376/// is retained so human-facing surfaces (CLI, REST `comms.peers`, logs) can
377/// render a recognisable handle next to the opaque id.
378#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
379#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
380pub struct PeerName(String);
381
382impl PeerName {
383    /// Create a new peer name if it passes basic validation.
384    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/// Canonical outbound peer route.
423///
424/// `peer_id` is the only routing key. `display_name` is optional presentation
425/// metadata retained for diagnostics after a boundary resolves a name through
426/// trust or discovery.
427#[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/// Exact recipient residency carried by an incarnation-fenced peer message.
457/// Kept in core because the actual member runtime signs the peer envelope;
458/// mob-specific bridge types project into this transport-neutral carrier.
459#[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/// Routing-subset descriptor for a trusted peer — the identity fields that
473/// traverse the core seam.
474///
475/// Replaces the old stringly trusted-peer spec `{ name, peer_id, address }`
476/// with typed atoms: `PeerId` (runtime routing key), `PeerName` (display
477/// slug), `PeerAddress` (transport + endpoint), and a 32-byte signing
478/// public key that lets the receiver verify envelope signatures. Richer
479/// trust-store metadata (discovery labels) stays in
480/// `meerkat-comms::trust::TrustedPeer` — this descriptor is the
481/// minimal typed subset the core seam needs to route and admit a peer.
482#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484pub struct TrustedPeerDescriptor {
485    /// Canonical runtime identity — the routing key. Never collides.
486    pub peer_id: PeerId,
487    /// Display-only slug for humans. Two peers may legitimately share a
488    /// name; their `peer_id` values still differ.
489    pub name: PeerName,
490    /// Typed transport atom + endpoint. Transport cannot be invented by
491    /// string concatenation at a call site.
492    pub address: PeerAddress,
493    /// Ed25519 signing public key (32 bytes). The receiver needs this to
494    /// verify envelope signatures; the router derives `PeerId` from it
495    /// via UUIDv5 so `peer_id` and `pubkey` are consistent.
496    pub pubkey: [u8; 32],
497}
498
499/// Generated authority context for mutating a comms trust projection.
500///
501/// The comms runtime stores the transport-level peer table, but it must not
502/// decide trust semantics itself. Callers that need to add or remove trust
503/// must carry the generated machine/composition handoff that authorized the
504/// mutation.
505#[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/// Trust-store projection mutation requested by generated authority.
1178#[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/// Result from applying a generated trust-store projection mutation.
1199#[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    /// Build a descriptor with a **zero Ed25519 signing pubkey** from
1229    /// typed identity atoms.
1230    ///
1231    /// The zero-pubkey default is **test-only** — envelope signature
1232    /// verification trivially fails against it. In-process `inproc`
1233    /// tests use this shape because the router identity map is what
1234    /// authorizes the peer; production paths construct
1235    /// `TrustedPeerDescriptor` via the struct literal with an explicit
1236    /// pubkey (or use [`Self::with_pubkey`] to stamp one onto a
1237    /// test-built descriptor). The loud name keeps the hazard surface
1238    /// explicit — a production call site using this helper is always
1239    /// wrong and will read wrong at review.
1240    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    /// Typed sibling of [`Self::test_only_unsigned`]: build a descriptor
1258    /// from an already-typed [`PeerId`] instead of a stringly-typed peer-id
1259    /// argument.
1260    ///
1261    /// Post-#24 `PeerId` is a typed UUID; `PeerId::parse` only accepts
1262    /// hyphenated UUID strings. The stringly-typed
1263    /// [`Self::test_only_unsigned`] accepts anything `AsRef<str>` and
1264    /// round-trips through `PeerId::parse`, which is the right contract
1265    /// for call sites whose peer-id comes off the wire (comms-drain
1266    /// supervisor reconcile, ops lifecycle) — they receive a UUID string
1267    /// and the helper validates it.
1268    ///
1269    /// Test fixtures that mint a peer locally do NOT have a UUID string
1270    /// to start from. They have a debug-friendly alias (`"remote-agent-b"`,
1271    /// `"stale-peer"`) and want a random `PeerId`. The stringly form
1272    /// forced them to either (a) stamp the alias in as an invalid UUID
1273    /// (which rejects post-#24) or (b) reach outside the helper to mint
1274    /// a UUID separately. This typed sibling accepts the typed `PeerId`
1275    /// directly, skipping the parse round-trip.
1276    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    /// Attach a non-zero Ed25519 signing pubkey. Test and production
1292    /// paths that already have a derived `PeerId` + pubkey use the
1293    /// field-literal constructor directly; this helper is for
1294    /// retroactively stamping a pubkey onto a descriptor built via
1295    /// [`Self::test_only_unsigned`].
1296    pub fn with_pubkey(mut self, pubkey: [u8; 32]) -> Self {
1297        self.pubkey = pubkey;
1298        self
1299    }
1300
1301    /// Build a descriptor with a caller-supplied Ed25519 signing pubkey
1302    /// from typed identity atoms.
1303    ///
1304    /// This is the dogma-clean alternative to
1305    /// [`Self::test_only_unsigned`] for live-comms paths where the
1306    /// caller has a real pubkey (e.g. from
1307    /// `CommsRuntime::public_key().as_bytes()`). The supervisor needs
1308    /// a non-zero-pubkey trust entry for signed-envelope replies to
1309    /// admit past `is_trusted(&envelope.from)` at ingress.
1310    ///
1311    /// Like [`Self::test_only_unsigned`], this accepts a stringly
1312    /// `peer_id` that must parse as a UUID (post-#24 `PeerId::parse`
1313    /// only accepts hyphenated UUID strings). The hashed consistency
1314    /// check in [`crate::comms`] enforces that the supplied `peer_id`
1315    /// matches `PubKey::from(pubkey).to_peer_id()` at descriptor →
1316    /// trust conversion.
1317    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/// One-way peer lifecycle notification kind.
1331///
1332/// These notifications are control-plane topology updates, not correlated
1333/// peer work requests. They intentionally do not create request/response
1334/// lifecycles and must never require an LLM-authored reply.
1335#[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    /// Supervisor-directed dismissal: a typed terminal lifecycle signal that
1345    /// retires a live executor. The dismissal authority is the supervisor /
1346    /// runtime drain-lifecycle owner, never a peer-authored message body — a
1347    /// "DISMISS" string in a peer message is ordinary content, not a control
1348    /// signal.
1349    #[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/// Sender-declared content-taint classification for peer content.
1371///
1372/// This is the typed vocabulary for the optional taint declaration a sender
1373/// stamps onto content-bearing comms envelopes (`Message` / `Request` /
1374/// `Response`). `Clean` and `Tainted` are the two DECLARED states.
1375///
1376/// `None` at the carriers (`MessageKind::*.content_taint`,
1377/// `SystemNoticeBlock::Comms.sender_taint`, runtime `PeerInput.sender_taint`)
1378/// means "the sender made no declaration" — a REAL third state. Receivers
1379/// must never coalesce `None` into `Clean`: an absent declaration carries no
1380/// trust information, while `Clean` is an affirmative sender claim.
1381#[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    /// The sender affirmatively declares this content clean.
1386    Clean,
1387    /// The sender declares this content tainted (e.g. it embeds unvetted
1388    /// third-party material such as web content or tool output).
1389    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/// Per-send tri-state override for the outbound content-taint declaration.
1408///
1409/// Carried as `Option<SendTaintOverride>` on the comms send surfaces: an
1410/// ABSENT override (`None`) inherits the runtime-level declaration installed
1411/// via `set_outbound_content_taint`; `Undeclared` strips the declaration for
1412/// this send (the envelope carries no taint field); `Declare(taint)` stamps
1413/// exactly `taint`. Inherit, disable, and set are three different facts — a
1414/// two-state override would collapse them.
1415#[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 exactly this taint state for this send.
1420    Declare(SenderContentTaint),
1421    /// Send no declaration, even when a runtime-level declaration is set.
1422    Undeclared,
1423}
1424
1425/// Typed wire request for `comms/send`.
1426///
1427/// Variants are serde-tagged on `kind` and validated structurally at the
1428/// deserialization boundary. Required fields per kind are enforced by the
1429/// type system; invalid discriminators (`source`, `stream`, `handling_mode`,
1430/// `status`) become serde deserialization errors rather than runtime
1431/// string-match failures.
1432///
1433/// Cross-field invariants that depend on machine-owned semantics, such as
1434/// progress-vs-terminal peer response handling, are checked by the runtime
1435/// after generated authority emits the typed classification.
1436#[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    /// Inject input into the local session.
1441    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    /// Send a one-way peer message.
1455    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    /// Send a one-way peer lifecycle notification.
1466    PeerLifecycle {
1467        to: PeerId,
1468        lifecycle_kind: PeerLifecycleKind,
1469        #[serde(default)]
1470        params: serde_json::Value,
1471    },
1472    /// Send a request to a peer.
1473    PeerRequest {
1474        to: PeerId,
1475        /// Closed, structurally-validated request intent. Unknown strings fail
1476        /// at the serde boundary rather than projecting through a string match.
1477        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    /// Send a response to a prior peer request.
1490    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/// Cross-field validation failure for [`CommsCommandRequest::into_command`].
1506///
1507/// Per-field discriminator validation is enforced by serde at deserialization
1508/// — only invariants that span multiple fields surface here.
1509#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1510pub enum CommsCommandError {
1511    /// `handling_mode` is set on a `peer_response` whose machine-classified
1512    /// terminality is progress. Progress responses cannot carry a handling
1513    /// mode — the receiver's admission gate would drop them, so reject after
1514    /// generated terminality feedback is available.
1515    #[error("handling_mode is forbidden on progress peer responses")]
1516    HandlingModeForbiddenForProgressResponse,
1517}
1518
1519impl CommsCommandRequest {
1520    /// Convert the typed wire request into a [`CommsCommand`] domain envelope.
1521    ///
1522    /// `session_id` is supplied separately because it is owned by the
1523    /// surface that received the request, not the wire payload.
1524    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                // The domain envelope carries a wider, open intent vocabulary
1579                // (it also routes mob topology intents such as `mob.peer_added`),
1580                // so the closed public-request intent projects to its stable
1581                // wire literal here. This is the request -> open-envelope seam,
1582                // not a typed -> string downgrade at the public wire boundary.
1583                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    /// Stable wire discriminant for telemetry / logging.
1613    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/// Source for an input event posted to an agent.
1624#[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/// Whether this input/peer command should reserve a local interaction stream.
1660#[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    /// Do not reserve any stream.
1665    None,
1666    /// Reserve an interaction stream for the command.
1667    ReserveInteraction,
1668}
1669
1670/// Transport-independent comms command envelope.
1671#[derive(Debug, Clone, PartialEq, Eq)]
1672pub enum CommsCommand {
1673    /// Inject input into the local session.
1674    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    /// Send a one-way peer message.
1684    PeerMessage {
1685        to: PeerRoute,
1686        body: String,
1687        blocks: Option<Vec<ContentBlock>>,
1688        /// Per-send taint override: `None` inherits the runtime declaration.
1689        content_taint: Option<SendTaintOverride>,
1690        handling_mode: HandlingMode,
1691        objective_id: Option<crate::interaction::ObjectiveId>,
1692    },
1693    /// One-way peer message whose actual member sender is preserved while the
1694    /// receiver must match a mandatory exact residency before admission.
1695    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    /// Send a one-way peer lifecycle notification.
1705    PeerLifecycle {
1706        to: PeerRoute,
1707        kind: PeerLifecycleKind,
1708        params: serde_json::Value,
1709    },
1710    /// Send a request to a peer.
1711    PeerRequest {
1712        to: PeerRoute,
1713        intent: String,
1714        params: serde_json::Value,
1715        blocks: Option<Vec<ContentBlock>>,
1716        /// Per-send taint override: `None` inherits the runtime declaration.
1717        content_taint: Option<SendTaintOverride>,
1718        handling_mode: HandlingMode,
1719        stream: InputStreamMode,
1720        objective_id: Option<crate::interaction::ObjectiveId>,
1721    },
1722    /// Send a response to a prior peer request.
1723    PeerResponse {
1724        to: PeerRoute,
1725        in_reply_to: InteractionId,
1726        status: ResponseStatus,
1727        result: serde_json::Value,
1728        blocks: Option<Vec<ContentBlock>>,
1729        /// Per-send taint override: `None` inherits the runtime declaration.
1730        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/// Strongest successful peer-delivery fact proved by the selected transport.
1773#[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    /// A peer acknowledgement was received and verified.
1779    Acked,
1780    /// The receiver's local inbox admitted the envelope directly.
1781    HandedOff,
1782    /// The envelope was written to a transport that does not acknowledge this
1783    /// message kind; receiver admission is not known.
1784    Queued,
1785}
1786
1787/// Receipt returned after accepting a comms command.
1788#[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
1812/// Turn-scoped dispatch-context key carrying the typed peer-reply capability.
1813///
1814/// The runtime mints a [`PeerReplyDispatchContext`] under this key on the
1815/// per-turn tool overlay when the turn was triggered by one or more peer
1816/// message deliveries; the comms `reply_to_peer` tool reads it back at
1817/// dispatch time. The value under this key is always the serialized
1818/// [`PeerReplyDispatchContext`] — no other producer may write this key.
1819pub const COMMS_PEER_REPLY_DISPATCH_CONTEXT_KEY: &str = "comms.peer_reply";
1820
1821/// Delivery kind a peer-reply capability was minted for.
1822#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1823#[serde(rename_all = "snake_case")]
1824#[non_exhaustive]
1825pub enum PeerReplyDeliveryKind {
1826    /// A one-way peer message delivery (the `PeerMessage` input grouping).
1827    Message,
1828}
1829
1830/// Typed capability to reply to one peer delivery that triggered this turn.
1831///
1832/// Minted by the runtime at batch admission — never by tool input — so the
1833/// `reply_to_peer` tool is pre-addressed: the model supplies no peer identity,
1834/// only content. Trust is still re-validated at dispatch time.
1835#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1836pub struct PeerReplyCapability {
1837    /// Interaction id of the triggering delivery (the reply selector).
1838    pub in_reply_to: InteractionId,
1839    /// Canonical routing identity of the sending peer.
1840    pub peer_id: PeerId,
1841    /// Optional display label admitted at peer ingress. Presentation
1842    /// metadata only; routing uses `peer_id`.
1843    #[serde(default, skip_serializing_if = "Option::is_none")]
1844    pub display_name: Option<String>,
1845    /// Delivery kind the capability was minted for.
1846    pub kind: PeerReplyDeliveryKind,
1847}
1848
1849/// Turn-scoped peer-reply dispatch context: every peer message delivery in
1850/// the admitted batch that can be answered with `reply_to_peer`, in batch
1851/// delivery order.
1852#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1853#[serde(deny_unknown_fields)]
1854pub struct PeerReplyDispatchContext {
1855    /// Reply capabilities in batch delivery order.
1856    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/// Typed peer capability envelope for peer-directory output.
1919///
1920/// Extensions are intentionally opaque display/integration metadata. Core
1921/// routing, admission, and policy decisions must use typed fields such as
1922/// [`PeerDirectoryEntry::sendable_kinds`] instead of consulting this bag.
1923#[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    /// Canonical runtime identity — the routing key.
1958    pub peer_id: PeerId,
1959    /// Display-only slug. Multiple entries may share a name; none share a
1960    /// `peer_id`.
1961    pub name: PeerName,
1962    /// Typed transport atom + endpoint. Replaces the prior free-form
1963    /// `address: String` so the transport cannot be invented by string
1964    /// concatenation at a call site.
1965    pub address: PeerAddress,
1966    pub source: PeerDirectorySource,
1967    pub sendable_kinds: Vec<PeerSendability>,
1968    pub capabilities: PeerCapabilitySet,
1969    /// Supplementary discovery metadata (description, labels).
1970    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/// Scope for streaming event output.
1992#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1993pub enum StreamScope {
1994    Session(crate::types::SessionId),
1995    Interaction(InteractionId),
1996}
1997
1998/// Typed stream over enveloped agent events.
1999pub type EventStream = Pin<Box<dyn Stream<Item = EventEnvelope<AgentEvent>> + Send>>;
2000
2001/// Errors for stream attachment and lookup.
2002#[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/// Typed reason a peer rejected our envelope at its ingress admission gate.
2026///
2027/// This mirrors `meerkat_comms::DropReason` across the core boundary so
2028/// `SendError::AdmissionDropped` can carry the typed cause all the way to
2029/// REST/RPC/MCP error payloads. Callers distinguish transport-level failure
2030/// (`PeerOffline`) from policy-level rejection (`AdmissionDropped { reason }`)
2031/// without collapsing both into "peer unreachable".
2032#[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    /// `require_peer_auth` is on, the sender is not in the trusted set, and
2038    /// the envelope is not auth-exempt (e.g. supervisor-bridge bootstrap).
2039    UntrustedSender,
2040    /// Classification rejected the item before the admission gate ran.
2041    ClassificationRejected,
2042    /// The receiver's classified inbox is closed (receiver dropped).
2043    SessionClosed,
2044    /// The receiver's classified inbox is at capacity.
2045    InboxFull,
2046}
2047
2048impl AdmissionDropReason {
2049    /// Stable wire code for this drop reason, suitable for REST/RPC/MCP
2050    /// error payloads. Callers-facing discriminant — must stay stable.
2051    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    /// The envelope could not reach the peer because the underlying transport
2079    /// (socket/IO) failed. Semantically distinct from `PeerOffline` (peer
2080    /// reachable but did not ack) and from `Internal` (host-side logic
2081    /// error): this is a connectivity failure surfaced as the
2082    /// `peer_unreachable` / `transport_error` wire class. Carries the
2083    /// transport cause for diagnostics (`details` payloads).
2084    #[error("transport error: {0}")]
2085    Transport(String),
2086    /// Receiver admitted the envelope-transport but rejected it at ingress
2087    /// for a typed policy reason (untrusted sender, full inbox, etc.). This
2088    /// is semantically distinct from `PeerOffline` — transport worked,
2089    /// policy refused.
2090    #[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        // serde reports "unknown variant `webhookd`, expected one of ...".
2332        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}