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/// Routing-subset descriptor for a trusted peer — the identity fields that
457/// traverse the core seam.
458///
459/// Replaces the old stringly trusted-peer spec `{ name, peer_id, address }`
460/// with typed atoms: `PeerId` (runtime routing key), `PeerName` (display
461/// slug), `PeerAddress` (transport + endpoint), and a 32-byte signing
462/// public key that lets the receiver verify envelope signatures. Richer
463/// trust-store metadata (discovery labels) stays in
464/// `meerkat-comms::trust::TrustedPeer` — this descriptor is the
465/// minimal typed subset the core seam needs to route and admit a peer.
466#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468pub struct TrustedPeerDescriptor {
469    /// Canonical runtime identity — the routing key. Never collides.
470    pub peer_id: PeerId,
471    /// Display-only slug for humans. Two peers may legitimately share a
472    /// name; their `peer_id` values still differ.
473    pub name: PeerName,
474    /// Typed transport atom + endpoint. Transport cannot be invented by
475    /// string concatenation at a call site.
476    pub address: PeerAddress,
477    /// Ed25519 signing public key (32 bytes). The receiver needs this to
478    /// verify envelope signatures; the router derives `PeerId` from it
479    /// via UUIDv5 so `peer_id` and `pubkey` are consistent.
480    pub pubkey: [u8; 32],
481}
482
483/// Generated authority context for mutating a comms trust projection.
484///
485/// The comms runtime stores the transport-level peer table, but it must not
486/// decide trust semantics itself. Callers that need to add or remove trust
487/// must carry the generated machine/composition handoff that authorized the
488/// mutation.
489#[derive(Debug, Clone)]
490pub struct CommsTrustMutationAuthority {
491    source_kind: GeneratedCommsTrustAuthoritySourceKind,
492    source_epoch: u64,
493    source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
494    trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
495    operation: GeneratedCommsTrustAuthorityOperation,
496    peer_id: String,
497    trust_store_peer_id: Option<String>,
498    peer_descriptor: Option<TrustedPeerDescriptor>,
499    consumed: Arc<AtomicBool>,
500}
501
502#[derive(Clone)]
503pub struct GeneratedPeerCommsOwnerToken {
504    inner: Arc<dyn Any + Send + Sync>,
505}
506
507impl std::fmt::Debug for GeneratedPeerCommsOwnerToken {
508    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509        f.debug_struct("GeneratedPeerCommsOwnerToken").finish()
510    }
511}
512
513impl GeneratedPeerCommsOwnerToken {
514    #[cfg_attr(
515        any(test, not(meerkat_internal_generated_authority_bridge)),
516        allow(dead_code)
517    )]
518    pub(crate) fn from_generated_owner_token(inner: Arc<dyn Any + Send + Sync>) -> Self {
519        Self { inner }
520    }
521
522    pub fn same_owner(&self, other: &Self) -> bool {
523        Arc::ptr_eq(&self.inner, &other.inner)
524    }
525
526    fn matches_raw_owner(&self, other: &Arc<dyn Any + Send + Sync>) -> bool {
527        Arc::ptr_eq(&self.inner, other)
528    }
529}
530
531#[doc(hidden)]
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
533pub enum GeneratedCommsTrustAuthoritySourceKind {
534    MeerkatMachinePeerProjection,
535    MeerkatMachineSupervisorPublish,
536    MeerkatMachineSupervisorRevoke,
537    MobMachineMemberTrustWiring,
538    MobMachineMemberTrustUnwiring,
539    MobMachineExternalPeerTrustWiring,
540    MobMachineExternalPeerTrustUnwiring,
541    MobMachineExternalPeerTrustRepair,
542    MobMachineExternalPeerReciprocalTrust,
543}
544
545#[doc(hidden)]
546#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
547pub enum GeneratedCommsTrustAuthorityOperation {
548    PublicAdd,
549    PublicRemove,
550    PrivateAdd,
551    PrivateRemove,
552}
553
554impl CommsTrustMutationAuthority {
555    #[cfg_attr(not(meerkat_internal_generated_authority_bridge), allow(dead_code))]
556    #[allow(clippy::too_many_arguments)]
557    fn from_generated_parts(
558        source_kind: GeneratedCommsTrustAuthoritySourceKind,
559        source_epoch: u64,
560        source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
561        trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
562        operation: GeneratedCommsTrustAuthorityOperation,
563        peer_id: impl Into<String>,
564        trust_store_peer_id: Option<String>,
565        peer_descriptor: Option<TrustedPeerDescriptor>,
566    ) -> Result<Self, String> {
567        let peer_id = peer_id.into();
568        if matches!(
569            operation,
570            GeneratedCommsTrustAuthorityOperation::PublicAdd
571                | GeneratedCommsTrustAuthorityOperation::PrivateAdd
572        ) && peer_descriptor.is_none()
573        {
574            return Err(format!(
575                "generated comms trust add for peer {peer_id:?} requires a trusted peer descriptor"
576            ));
577        }
578        if let Some(peer) = peer_descriptor.as_ref()
579            && peer.peer_id.to_string() != peer_id
580        {
581            return Err(format!(
582                "generated comms trust descriptor peer_id {} does not match requested {:?}",
583                peer.peer_id, peer_id,
584            ));
585        }
586        if matches!(
587            operation,
588            GeneratedCommsTrustAuthorityOperation::PublicRemove
589                | GeneratedCommsTrustAuthorityOperation::PrivateRemove
590        ) && peer_descriptor.is_some()
591        {
592            return Err(format!(
593                "generated comms trust remove for peer {peer_id:?} must not carry a trusted peer descriptor"
594            ));
595        }
596        Ok(Self {
597            source_kind,
598            source_epoch,
599            source_owner_token,
600            trust_row_owner_kind,
601            operation,
602            peer_id,
603            trust_store_peer_id,
604            peer_descriptor,
605            consumed: Arc::new(AtomicBool::new(false)),
606        })
607    }
608
609    pub fn validate_public_add(
610        &self,
611        trust_store_peer_id: Option<PeerId>,
612        peer: &TrustedPeerDescriptor,
613    ) -> Result<(), String> {
614        self.validate_add_operation(
615            GeneratedCommsTrustAuthorityOperation::PublicAdd,
616            trust_store_peer_id,
617            peer,
618            "add a public trusted peer",
619        )
620    }
621
622    pub fn validate_public_remove(
623        &self,
624        trust_store_peer_id: Option<PeerId>,
625        peer_id: PeerId,
626    ) -> Result<(), String> {
627        self.validate_operation(
628            GeneratedCommsTrustAuthorityOperation::PublicRemove,
629            trust_store_peer_id,
630            peer_id,
631            "remove a public trusted peer",
632        )
633    }
634
635    pub fn validate_private_add(
636        &self,
637        trust_store_peer_id: Option<PeerId>,
638        peer: &TrustedPeerDescriptor,
639    ) -> Result<(), String> {
640        self.validate_add_operation(
641            GeneratedCommsTrustAuthorityOperation::PrivateAdd,
642            trust_store_peer_id,
643            peer,
644            "add a private trusted peer",
645        )
646    }
647
648    pub fn validate_private_remove(
649        &self,
650        trust_store_peer_id: Option<PeerId>,
651        peer_id: PeerId,
652    ) -> Result<(), String> {
653        self.validate_operation(
654            GeneratedCommsTrustAuthorityOperation::PrivateRemove,
655            trust_store_peer_id,
656            peer_id,
657            "remove a private trusted peer",
658        )
659    }
660
661    pub fn preflight_public_add(
662        &self,
663        trust_store_peer_id: Option<PeerId>,
664        peer: &TrustedPeerDescriptor,
665    ) -> Result<(), String> {
666        self.preflight_add_operation(
667            GeneratedCommsTrustAuthorityOperation::PublicAdd,
668            trust_store_peer_id,
669            peer,
670            "add a public trusted peer",
671        )
672    }
673
674    pub fn preflight_public_remove(
675        &self,
676        trust_store_peer_id: Option<PeerId>,
677        peer_id: PeerId,
678    ) -> Result<(), String> {
679        self.preflight_operation(
680            GeneratedCommsTrustAuthorityOperation::PublicRemove,
681            trust_store_peer_id,
682            peer_id,
683            "remove a public trusted peer",
684        )
685    }
686
687    fn validate_operation(
688        &self,
689        operation: GeneratedCommsTrustAuthorityOperation,
690        trust_store_peer_id: Option<PeerId>,
691        peer_id: PeerId,
692        action: &'static str,
693    ) -> Result<(), String> {
694        if self.operation != operation {
695            return Err(format!(
696                "trust authority from {:?} for {:?} cannot {action}",
697                self.source_kind, self.operation,
698            ));
699        }
700        self.validate_peer_match(peer_id)?;
701        self.validate_trust_store_peer_match(trust_store_peer_id)?;
702        self.consume_once()
703    }
704
705    fn preflight_operation(
706        &self,
707        operation: GeneratedCommsTrustAuthorityOperation,
708        trust_store_peer_id: Option<PeerId>,
709        peer_id: PeerId,
710        action: &'static str,
711    ) -> Result<(), String> {
712        if self.operation != operation {
713            return Err(format!(
714                "trust authority from {:?} for {:?} cannot {action}",
715                self.source_kind, self.operation,
716            ));
717        }
718        self.validate_peer_match(peer_id)?;
719        self.validate_trust_store_peer_match(trust_store_peer_id)
720    }
721
722    fn validate_add_operation(
723        &self,
724        operation: GeneratedCommsTrustAuthorityOperation,
725        trust_store_peer_id: Option<PeerId>,
726        peer: &TrustedPeerDescriptor,
727        action: &'static str,
728    ) -> Result<(), String> {
729        if self.operation != operation {
730            return Err(format!(
731                "trust authority from {:?} for {:?} cannot {action}",
732                self.source_kind, self.operation,
733            ));
734        }
735        self.validate_peer_match(peer.peer_id)?;
736        self.validate_peer_descriptor_match(peer)?;
737        self.validate_trust_store_peer_match(trust_store_peer_id)?;
738        self.consume_once()
739    }
740
741    fn preflight_add_operation(
742        &self,
743        operation: GeneratedCommsTrustAuthorityOperation,
744        trust_store_peer_id: Option<PeerId>,
745        peer: &TrustedPeerDescriptor,
746        action: &'static str,
747    ) -> Result<(), String> {
748        if self.operation != operation {
749            return Err(format!(
750                "trust authority from {:?} for {:?} cannot {action}",
751                self.source_kind, self.operation,
752            ));
753        }
754        self.validate_peer_match(peer.peer_id)?;
755        self.validate_peer_descriptor_match(peer)?;
756        self.validate_trust_store_peer_match(trust_store_peer_id)
757    }
758
759    fn consume_once(&self) -> Result<(), String> {
760        self.consumed
761            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
762            .map(|_| ())
763            .map_err(|_| "generated comms trust authority was already consumed".to_string())
764    }
765
766    fn validate_peer_match(&self, peer_id: PeerId) -> Result<(), String> {
767        let expected = self.peer_id();
768        if expected == peer_id.to_string() {
769            Ok(())
770        } else {
771            Err(format!(
772                "trust authority peer_id {expected:?} does not match mutation peer_id {peer_id}"
773            ))
774        }
775    }
776
777    fn validate_trust_store_peer_match(
778        &self,
779        trust_store_peer_id: Option<PeerId>,
780    ) -> Result<(), String> {
781        let Some(expected) = self.trust_store_peer_id.as_deref() else {
782            return Ok(());
783        };
784        let Some(actual) = trust_store_peer_id else {
785            return Err(format!(
786                "trust authority from {:?} requires trust-store peer_id {expected:?}, but the target runtime did not expose one",
787                self.source_kind,
788            ));
789        };
790        if expected == actual.to_string() {
791            Ok(())
792        } else {
793            Err(format!(
794                "trust authority from {:?} for peer {:?} targets trust-store peer_id {expected:?}, not {actual}",
795                self.source_kind,
796                self.peer_id(),
797            ))
798        }
799    }
800
801    fn validate_peer_descriptor_match(&self, peer: &TrustedPeerDescriptor) -> Result<(), String> {
802        let Some(expected) = self.peer_descriptor.as_ref() else {
803            return Err(format!(
804                "trust authority from {:?} for {:?} did not carry a generated peer descriptor",
805                self.source_kind, self.operation,
806            ));
807        };
808        if expected == peer {
809            Ok(())
810        } else {
811            Err(format!(
812                "trust authority descriptor for peer {:?} does not match mutation descriptor",
813                self.peer_id()
814            ))
815        }
816    }
817
818    fn peer_id(&self) -> &str {
819        self.peer_id.as_str()
820    }
821
822    pub fn source_epoch(&self) -> u64 {
823        self.source_epoch
824    }
825
826    pub fn validate_source_owner_token(
827        &self,
828        expected: Option<&GeneratedPeerCommsOwnerToken>,
829    ) -> Result<(), String> {
830        let Some(actual) = self.source_owner_token.as_ref() else {
831            return Err(format!(
832                "trust authority from {:?} did not carry a generated owner token",
833                self.source_kind,
834            ));
835        };
836        let Some(expected) = expected else {
837            return Err(format!(
838                "trust authority from {:?} requires the target runtime's generated owner token",
839                self.source_kind,
840            ));
841        };
842        if expected.matches_raw_owner(actual) {
843            Ok(())
844        } else {
845            Err(format!(
846                "trust authority from {:?} was minted by a different generated owner",
847                self.source_kind,
848            ))
849        }
850    }
851
852    pub fn validate_target_source_owner_token(
853        &self,
854        expected_meerkat_machine_owner: Option<&GeneratedPeerCommsOwnerToken>,
855        expected_mob_machine_owner: Option<&Arc<dyn Any + Send + Sync>>,
856    ) -> Result<(), String> {
857        if is_meerkat_machine_trust_source(self.source_kind) {
858            self.validate_source_owner_token(expected_meerkat_machine_owner)
859        } else if is_mob_machine_trust_source(self.source_kind) {
860            self.validate_raw_source_owner_token(expected_mob_machine_owner)
861        } else {
862            Err(format!(
863                "trust authority from {:?} has no target owner validator",
864                self.source_kind,
865            ))
866        }
867    }
868
869    pub fn validate_raw_source_owner_token(
870        &self,
871        expected: Option<&Arc<dyn Any + Send + Sync>>,
872    ) -> Result<(), String> {
873        let Some(actual) = self.source_owner_token.as_ref() else {
874            return Err(format!(
875                "trust authority from {:?} did not carry a generated owner token",
876                self.source_kind,
877            ));
878        };
879        let Some(expected) = expected else {
880            return Err(format!(
881                "trust authority from {:?} requires the target runtime's generated owner token",
882                self.source_kind,
883            ));
884        };
885        if Arc::ptr_eq(actual, expected) {
886            Ok(())
887        } else {
888            Err(format!(
889                "trust authority from {:?} was minted by a different generated owner",
890                self.source_kind,
891            ))
892        }
893    }
894
895    pub fn is_mob_machine_source(&self) -> bool {
896        is_mob_machine_trust_source(self.source_kind)
897    }
898
899    pub fn trust_row_owner_kind(&self) -> GeneratedCommsTrustAuthoritySourceKind {
900        self.trust_row_owner_kind
901    }
902}
903
904#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
905#[allow(improper_ctypes_definitions, unsafe_code)]
906unsafe extern "Rust" {
907    #[link_name = concat!(
908        "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_comms_trust_reconcile_",
909        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
910    )]
911    fn runtime_comms_trust_reconcile_generated_authority_bridge_token_is_valid(
912        token: &(dyn std::any::Any + Send + Sync),
913    ) -> bool;
914
915    #[link_name = concat!(
916        "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_supervisor_trust_publish_",
917        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
918    )]
919    fn runtime_supervisor_trust_publish_generated_authority_bridge_token_is_valid(
920        token: &(dyn std::any::Any + Send + Sync),
921    ) -> bool;
922
923    #[link_name = concat!(
924        "__meerkat_runtime_generated_authority_bridge_token_is_valid_v1_supervisor_trust_revoke_",
925        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
926    )]
927    fn runtime_supervisor_trust_revoke_generated_authority_bridge_token_is_valid(
928        token: &(dyn std::any::Any + Send + Sync),
929    ) -> bool;
930
931    #[link_name = concat!(
932        "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_member_trust_wiring_",
933        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
934    )]
935    fn mob_member_trust_wiring_generated_authority_bridge_token_is_valid(
936        token: &(dyn std::any::Any + Send + Sync),
937    ) -> bool;
938
939    #[link_name = concat!(
940        "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_member_trust_unwiring_",
941        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
942    )]
943    fn mob_member_trust_unwiring_generated_authority_bridge_token_is_valid(
944        token: &(dyn std::any::Any + Send + Sync),
945    ) -> bool;
946
947    #[link_name = concat!(
948        "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_trust_wiring_",
949        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
950    )]
951    fn mob_external_peer_trust_wiring_generated_authority_bridge_token_is_valid(
952        token: &(dyn std::any::Any + Send + Sync),
953    ) -> bool;
954
955    #[link_name = concat!(
956        "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_trust_unwiring_",
957        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
958    )]
959    fn mob_external_peer_trust_unwiring_generated_authority_bridge_token_is_valid(
960        token: &(dyn std::any::Any + Send + Sync),
961    ) -> bool;
962
963    #[link_name = concat!(
964        "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_trust_repair_",
965        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
966    )]
967    fn mob_external_peer_trust_repair_generated_authority_bridge_token_is_valid(
968        token: &(dyn std::any::Any + Send + Sync),
969    ) -> bool;
970
971    #[link_name = concat!(
972        "__meerkat_mob_generated_authority_bridge_token_is_valid_v1_mob_external_peer_reciprocal_trust_",
973        env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
974    )]
975    fn mob_external_peer_reciprocal_trust_generated_authority_bridge_token_is_valid(
976        token: &(dyn std::any::Any + Send + Sync),
977    ) -> bool;
978}
979
980#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
981#[doc(hidden)]
982#[allow(improper_ctypes_definitions, unsafe_code)]
983#[allow(clippy::too_many_arguments)]
984#[unsafe(export_name = concat!(
985    "__meerkat_core_runtime_generated_comms_trust_authority_build_v1_",
986    env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
987))]
988pub(crate) extern "Rust" fn runtime_generated_comms_trust_authority_build(
989    token: &'static (dyn std::any::Any + Send + Sync),
990    source_kind: GeneratedCommsTrustAuthoritySourceKind,
991    source_epoch: u64,
992    source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
993    trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
994    operation: GeneratedCommsTrustAuthorityOperation,
995    peer_id: String,
996    trust_store_peer_id: Option<String>,
997    peer_descriptor: Option<TrustedPeerDescriptor>,
998) -> Result<CommsTrustMutationAuthority, String> {
999    validate_runtime_generated_authority_bridge_token(source_kind, token)?;
1000    validate_meerkat_machine_trust_source(source_kind, trust_row_owner_kind)?;
1001    CommsTrustMutationAuthority::from_generated_parts(
1002        source_kind,
1003        source_epoch,
1004        source_owner_token,
1005        trust_row_owner_kind,
1006        operation,
1007        peer_id,
1008        trust_store_peer_id,
1009        peer_descriptor,
1010    )
1011}
1012
1013#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1014#[doc(hidden)]
1015#[allow(improper_ctypes_definitions, unsafe_code)]
1016#[allow(clippy::too_many_arguments)]
1017#[unsafe(export_name = concat!(
1018    "__meerkat_core_mob_generated_comms_trust_authority_build_v1_",
1019    env!("MEERKAT_GENERATED_AUTHORITY_BRIDGE_SYMBOL_SUFFIX")
1020))]
1021pub(crate) extern "Rust" fn mob_generated_comms_trust_authority_build(
1022    token: &'static (dyn std::any::Any + Send + Sync),
1023    source_kind: GeneratedCommsTrustAuthoritySourceKind,
1024    source_epoch: u64,
1025    source_owner_token: Option<Arc<dyn Any + Send + Sync>>,
1026    trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1027    operation: GeneratedCommsTrustAuthorityOperation,
1028    peer_id: String,
1029    trust_store_peer_id: Option<String>,
1030    peer_descriptor: Option<TrustedPeerDescriptor>,
1031) -> Result<CommsTrustMutationAuthority, String> {
1032    validate_mob_generated_authority_bridge_token(source_kind, token)?;
1033    validate_mob_machine_trust_source(source_kind, trust_row_owner_kind)?;
1034    CommsTrustMutationAuthority::from_generated_parts(
1035        source_kind,
1036        source_epoch,
1037        source_owner_token,
1038        trust_row_owner_kind,
1039        operation,
1040        peer_id,
1041        trust_store_peer_id,
1042        peer_descriptor,
1043    )
1044}
1045
1046#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1047fn validate_runtime_generated_authority_bridge_token(
1048    source_kind: GeneratedCommsTrustAuthoritySourceKind,
1049    token: &(dyn std::any::Any + Send + Sync),
1050) -> Result<(), String> {
1051    #[allow(unsafe_code)]
1052    let valid = unsafe {
1053        match source_kind {
1054            GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection => {
1055                runtime_comms_trust_reconcile_generated_authority_bridge_token_is_valid(token)
1056            }
1057            GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorPublish => {
1058                runtime_supervisor_trust_publish_generated_authority_bridge_token_is_valid(token)
1059            }
1060            GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorRevoke => {
1061                runtime_supervisor_trust_revoke_generated_authority_bridge_token_is_valid(token)
1062            }
1063            _ => false,
1064        }
1065    };
1066    if valid {
1067        Ok(())
1068    } else {
1069        Err("generated comms trust authority requires the matching generated runtime protocol bridge token".into())
1070    }
1071}
1072
1073#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1074fn validate_mob_generated_authority_bridge_token(
1075    source_kind: GeneratedCommsTrustAuthoritySourceKind,
1076    token: &(dyn std::any::Any + Send + Sync),
1077) -> Result<(), String> {
1078    #[allow(unsafe_code)]
1079    let valid = unsafe {
1080        match source_kind {
1081            GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring => {
1082                mob_member_trust_wiring_generated_authority_bridge_token_is_valid(token)
1083            }
1084            GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustUnwiring => {
1085                mob_member_trust_unwiring_generated_authority_bridge_token_is_valid(token)
1086            }
1087            GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustWiring => {
1088                mob_external_peer_trust_wiring_generated_authority_bridge_token_is_valid(token)
1089            }
1090            GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustUnwiring => {
1091                mob_external_peer_trust_unwiring_generated_authority_bridge_token_is_valid(token)
1092            }
1093            GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustRepair => {
1094                mob_external_peer_trust_repair_generated_authority_bridge_token_is_valid(token)
1095            }
1096            GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerReciprocalTrust => {
1097                mob_external_peer_reciprocal_trust_generated_authority_bridge_token_is_valid(token)
1098            }
1099            _ => false,
1100        }
1101    };
1102    if valid {
1103        Ok(())
1104    } else {
1105        Err("generated comms trust authority requires the matching generated MobMachine protocol bridge token".into())
1106    }
1107}
1108
1109#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1110fn validate_meerkat_machine_trust_source(
1111    source_kind: GeneratedCommsTrustAuthoritySourceKind,
1112    trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1113) -> Result<(), String> {
1114    if is_meerkat_machine_trust_source(source_kind)
1115        && is_meerkat_machine_trust_source(trust_row_owner_kind)
1116    {
1117        Ok(())
1118    } else {
1119        Err(format!(
1120            "runtime generated comms trust authority cannot package source {source_kind:?} with row owner {trust_row_owner_kind:?}"
1121        ))
1122    }
1123}
1124
1125#[cfg(all(meerkat_internal_generated_authority_bridge, not(test)))]
1126fn validate_mob_machine_trust_source(
1127    source_kind: GeneratedCommsTrustAuthoritySourceKind,
1128    trust_row_owner_kind: GeneratedCommsTrustAuthoritySourceKind,
1129) -> Result<(), String> {
1130    if is_mob_machine_trust_source(source_kind) && is_mob_machine_trust_source(trust_row_owner_kind)
1131    {
1132        Ok(())
1133    } else {
1134        Err(format!(
1135            "mob generated comms trust authority cannot package source {source_kind:?} with row owner {trust_row_owner_kind:?}"
1136        ))
1137    }
1138}
1139
1140fn is_meerkat_machine_trust_source(kind: GeneratedCommsTrustAuthoritySourceKind) -> bool {
1141    matches!(
1142        kind,
1143        GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection
1144            | GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorPublish
1145            | GeneratedCommsTrustAuthoritySourceKind::MeerkatMachineSupervisorRevoke
1146    )
1147}
1148
1149fn is_mob_machine_trust_source(kind: GeneratedCommsTrustAuthoritySourceKind) -> bool {
1150    matches!(
1151        kind,
1152        GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustWiring
1153            | GeneratedCommsTrustAuthoritySourceKind::MobMachineMemberTrustUnwiring
1154            | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustWiring
1155            | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustUnwiring
1156            | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerTrustRepair
1157            | GeneratedCommsTrustAuthoritySourceKind::MobMachineExternalPeerReciprocalTrust
1158    )
1159}
1160
1161/// Trust-store projection mutation requested by generated authority.
1162#[derive(Debug, Clone)]
1163pub enum CommsTrustMutation {
1164    AddTrustedPeer {
1165        peer: TrustedPeerDescriptor,
1166        authority: CommsTrustMutationAuthority,
1167    },
1168    RemoveTrustedPeer {
1169        peer_id: String,
1170        authority: CommsTrustMutationAuthority,
1171    },
1172    AddPrivateTrustedPeer {
1173        peer: TrustedPeerDescriptor,
1174        authority: CommsTrustMutationAuthority,
1175    },
1176    RemovePrivateTrustedPeer {
1177        peer_id: String,
1178        authority: CommsTrustMutationAuthority,
1179    },
1180}
1181
1182/// Result from applying a generated trust-store projection mutation.
1183#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1185pub enum CommsTrustMutationResult {
1186    Added { created: bool },
1187    Removed { removed: bool },
1188}
1189
1190impl TrustedPeerDescriptor {
1191    pub fn pubkey_is_zero(pubkey: &[u8; 32]) -> bool {
1192        *pubkey == [0u8; 32]
1193    }
1194
1195    pub fn has_zero_pubkey(&self) -> bool {
1196        Self::pubkey_is_zero(&self.pubkey)
1197    }
1198
1199    pub fn validate_pubkey_for_peer_id(peer_id: PeerId, pubkey: &[u8; 32]) -> Result<(), String> {
1200        if Self::pubkey_is_zero(pubkey) {
1201            return Err("TrustedPeerDescriptor.pubkey must be non-zero".to_string());
1202        }
1203        let derived = PeerId::from_ed25519_pubkey(pubkey);
1204        if derived != peer_id {
1205            return Err(format!(
1206                "peer_id {peer_id} does not match pubkey-derived id {derived}"
1207            ));
1208        }
1209        Ok(())
1210    }
1211
1212    /// Build a descriptor with a **zero Ed25519 signing pubkey** from
1213    /// typed identity atoms.
1214    ///
1215    /// The zero-pubkey default is **test-only** — envelope signature
1216    /// verification trivially fails against it. In-process `inproc`
1217    /// tests use this shape because the router identity map is what
1218    /// authorizes the peer; production paths construct
1219    /// `TrustedPeerDescriptor` via the struct literal with an explicit
1220    /// pubkey (or use [`Self::with_pubkey`] to stamp one onto a
1221    /// test-built descriptor). The loud name keeps the hazard surface
1222    /// explicit — a production call site using this helper is always
1223    /// wrong and will read wrong at review.
1224    pub fn test_only_unsigned(
1225        name: impl Into<String>,
1226        peer_id: impl AsRef<str>,
1227        address: impl AsRef<str>,
1228    ) -> Result<Self, String> {
1229        let name = PeerName::new(name).map_err(|e| format!("invalid peer name: {e}"))?;
1230        let peer_id =
1231            PeerId::parse(peer_id.as_ref()).map_err(|e| format!("invalid peer_id: {e}"))?;
1232        let address = PeerAddress::parse(address.as_ref()).map_err(|e| e.to_string())?;
1233        Ok(Self {
1234            peer_id,
1235            name,
1236            address,
1237            pubkey: [0u8; 32],
1238        })
1239    }
1240
1241    /// Typed sibling of [`Self::test_only_unsigned`]: build a descriptor
1242    /// from an already-typed [`PeerId`] instead of a stringly-typed peer-id
1243    /// argument.
1244    ///
1245    /// Post-#24 `PeerId` is a typed UUID; `PeerId::parse` only accepts
1246    /// hyphenated UUID strings. The stringly-typed
1247    /// [`Self::test_only_unsigned`] accepts anything `AsRef<str>` and
1248    /// round-trips through `PeerId::parse`, which is the right contract
1249    /// for call sites whose peer-id comes off the wire (comms-drain
1250    /// supervisor reconcile, ops lifecycle) — they receive a UUID string
1251    /// and the helper validates it.
1252    ///
1253    /// Test fixtures that mint a peer locally do NOT have a UUID string
1254    /// to start from. They have a debug-friendly alias (`"remote-agent-b"`,
1255    /// `"stale-peer"`) and want a random `PeerId`. The stringly form
1256    /// forced them to either (a) stamp the alias in as an invalid UUID
1257    /// (which rejects post-#24) or (b) reach outside the helper to mint
1258    /// a UUID separately. This typed sibling accepts the typed `PeerId`
1259    /// directly, skipping the parse round-trip.
1260    pub fn test_only_unsigned_typed(
1261        name: impl Into<String>,
1262        peer_id: PeerId,
1263        address: impl AsRef<str>,
1264    ) -> Result<Self, String> {
1265        let name = PeerName::new(name).map_err(|e| format!("invalid peer name: {e}"))?;
1266        let address = PeerAddress::parse(address.as_ref()).map_err(|e| e.to_string())?;
1267        Ok(Self {
1268            peer_id,
1269            name,
1270            address,
1271            pubkey: [0u8; 32],
1272        })
1273    }
1274
1275    /// Attach a non-zero Ed25519 signing pubkey. Test and production
1276    /// paths that already have a derived `PeerId` + pubkey use the
1277    /// field-literal constructor directly; this helper is for
1278    /// retroactively stamping a pubkey onto a descriptor built via
1279    /// [`Self::test_only_unsigned`].
1280    pub fn with_pubkey(mut self, pubkey: [u8; 32]) -> Self {
1281        self.pubkey = pubkey;
1282        self
1283    }
1284
1285    /// Build a descriptor with a caller-supplied Ed25519 signing pubkey
1286    /// from typed identity atoms.
1287    ///
1288    /// This is the dogma-clean alternative to
1289    /// [`Self::test_only_unsigned`] for live-comms paths where the
1290    /// caller has a real pubkey (e.g. from
1291    /// `CommsRuntime::public_key().as_bytes()`). The supervisor needs
1292    /// a non-zero-pubkey trust entry for signed-envelope replies to
1293    /// admit past `is_trusted(&envelope.from)` at ingress.
1294    ///
1295    /// Like [`Self::test_only_unsigned`], this accepts a stringly
1296    /// `peer_id` that must parse as a UUID (post-#24 `PeerId::parse`
1297    /// only accepts hyphenated UUID strings). The hashed consistency
1298    /// check in [`crate::comms`] enforces that the supplied `peer_id`
1299    /// matches `PubKey::from(pubkey).to_peer_id()` at descriptor →
1300    /// trust conversion.
1301    pub fn unsigned_with_pubkey(
1302        name: impl Into<String>,
1303        peer_id: impl AsRef<str>,
1304        pubkey: [u8; 32],
1305        address: impl AsRef<str>,
1306    ) -> Result<Self, String> {
1307        let mut descriptor = Self::test_only_unsigned(name, peer_id, address)?;
1308        Self::validate_pubkey_for_peer_id(descriptor.peer_id, &pubkey)?;
1309        descriptor.pubkey = pubkey;
1310        Ok(descriptor)
1311    }
1312}
1313
1314/// One-way peer lifecycle notification kind.
1315///
1316/// These notifications are control-plane topology updates, not correlated
1317/// peer work requests. They intentionally do not create request/response
1318/// lifecycles and must never require an LLM-authored reply.
1319#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1320#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1321pub enum PeerLifecycleKind {
1322    #[serde(rename = "mob.peer_added")]
1323    PeerAdded,
1324    #[serde(rename = "mob.peer_retired")]
1325    PeerRetired,
1326    #[serde(rename = "mob.peer_unwired")]
1327    PeerUnwired,
1328    /// Supervisor-directed dismissal: a typed terminal lifecycle signal that
1329    /// retires a live executor. The dismissal authority is the supervisor /
1330    /// runtime drain-lifecycle owner, never a peer-authored message body — a
1331    /// "DISMISS" string in a peer message is ordinary content, not a control
1332    /// signal.
1333    #[serde(rename = "mob.dismiss")]
1334    Dismiss,
1335}
1336
1337impl PeerLifecycleKind {
1338    pub const fn as_str(self) -> &'static str {
1339        match self {
1340            Self::PeerAdded => "mob.peer_added",
1341            Self::PeerRetired => "mob.peer_retired",
1342            Self::PeerUnwired => "mob.peer_unwired",
1343            Self::Dismiss => "mob.dismiss",
1344        }
1345    }
1346}
1347
1348impl std::fmt::Display for PeerLifecycleKind {
1349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1350        f.write_str(self.as_str())
1351    }
1352}
1353
1354/// Sender-declared content-taint classification for peer content.
1355///
1356/// This is the typed vocabulary for the optional taint declaration a sender
1357/// stamps onto content-bearing comms envelopes (`Message` / `Request` /
1358/// `Response`). `Clean` and `Tainted` are the two DECLARED states.
1359///
1360/// `None` at the carriers (`MessageKind::*.content_taint`,
1361/// `SystemNoticeBlock::Comms.sender_taint`, runtime `PeerInput.sender_taint`)
1362/// means "the sender made no declaration" — a REAL third state. Receivers
1363/// must never coalesce `None` into `Clean`: an absent declaration carries no
1364/// trust information, while `Clean` is an affirmative sender claim.
1365#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1366#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1367#[serde(rename_all = "snake_case")]
1368pub enum SenderContentTaint {
1369    /// The sender affirmatively declares this content clean.
1370    Clean,
1371    /// The sender declares this content tainted (e.g. it embeds unvetted
1372    /// third-party material such as web content or tool output).
1373    Tainted,
1374}
1375
1376impl SenderContentTaint {
1377    pub const fn as_str(self) -> &'static str {
1378        match self {
1379            Self::Clean => "clean",
1380            Self::Tainted => "tainted",
1381        }
1382    }
1383}
1384
1385impl std::fmt::Display for SenderContentTaint {
1386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1387        f.write_str(self.as_str())
1388    }
1389}
1390
1391/// Per-send tri-state override for the outbound content-taint declaration.
1392///
1393/// Carried as `Option<SendTaintOverride>` on the comms send surfaces: an
1394/// ABSENT override (`None`) inherits the runtime-level declaration installed
1395/// via `set_outbound_content_taint`; `Undeclared` strips the declaration for
1396/// this send (the envelope carries no taint field); `Declare(taint)` stamps
1397/// exactly `taint`. Inherit, disable, and set are three different facts — a
1398/// two-state override would collapse them.
1399#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1401#[serde(rename_all = "snake_case")]
1402pub enum SendTaintOverride {
1403    /// Declare exactly this taint state for this send.
1404    Declare(SenderContentTaint),
1405    /// Send no declaration, even when a runtime-level declaration is set.
1406    Undeclared,
1407}
1408
1409/// Typed wire request for `comms/send`.
1410///
1411/// Variants are serde-tagged on `kind` and validated structurally at the
1412/// deserialization boundary. Required fields per kind are enforced by the
1413/// type system; invalid discriminators (`source`, `stream`, `handling_mode`,
1414/// `status`) become serde deserialization errors rather than runtime
1415/// string-match failures.
1416///
1417/// Cross-field invariants that depend on machine-owned semantics, such as
1418/// progress-vs-terminal peer response handling, are checked by the runtime
1419/// after generated authority emits the typed classification.
1420#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1422#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1423pub enum CommsCommandRequest {
1424    /// Inject input into the local session.
1425    Input {
1426        body: String,
1427        #[serde(default, skip_serializing_if = "Option::is_none")]
1428        blocks: Option<Vec<ContentBlock>>,
1429        #[serde(default, skip_serializing_if = "Option::is_none")]
1430        source: Option<InputSource>,
1431        #[serde(default, skip_serializing_if = "Option::is_none")]
1432        stream: Option<InputStreamMode>,
1433        #[serde(default, skip_serializing_if = "Option::is_none")]
1434        handling_mode: Option<HandlingMode>,
1435        #[serde(default, skip_serializing_if = "Option::is_none")]
1436        allow_self_session: Option<bool>,
1437    },
1438    /// Send a one-way peer message.
1439    PeerMessage {
1440        to: PeerId,
1441        body: String,
1442        #[serde(default, skip_serializing_if = "Option::is_none")]
1443        blocks: Option<Vec<ContentBlock>>,
1444        #[serde(default, skip_serializing_if = "Option::is_none")]
1445        content_taint: Option<SendTaintOverride>,
1446        #[serde(default, skip_serializing_if = "Option::is_none")]
1447        handling_mode: Option<HandlingMode>,
1448    },
1449    /// Send a one-way peer lifecycle notification.
1450    PeerLifecycle {
1451        to: PeerId,
1452        lifecycle_kind: PeerLifecycleKind,
1453        #[serde(default)]
1454        params: serde_json::Value,
1455    },
1456    /// Send a request to a peer.
1457    PeerRequest {
1458        to: PeerId,
1459        /// Closed, structurally-validated request intent. Unknown strings fail
1460        /// at the serde boundary rather than projecting through a string match.
1461        intent: CommsPeerRequestIntent,
1462        #[serde(default)]
1463        params: serde_json::Value,
1464        #[serde(default, skip_serializing_if = "Option::is_none")]
1465        blocks: Option<Vec<ContentBlock>>,
1466        #[serde(default, skip_serializing_if = "Option::is_none")]
1467        content_taint: Option<SendTaintOverride>,
1468        #[serde(default, skip_serializing_if = "Option::is_none")]
1469        handling_mode: Option<HandlingMode>,
1470        #[serde(default, skip_serializing_if = "Option::is_none")]
1471        stream: Option<InputStreamMode>,
1472    },
1473    /// Send a response to a prior peer request.
1474    PeerResponse {
1475        to: PeerId,
1476        in_reply_to: InteractionId,
1477        status: ResponseStatus,
1478        #[serde(default)]
1479        result: serde_json::Value,
1480        #[serde(default, skip_serializing_if = "Option::is_none")]
1481        blocks: Option<Vec<ContentBlock>>,
1482        #[serde(default, skip_serializing_if = "Option::is_none")]
1483        content_taint: Option<SendTaintOverride>,
1484        #[serde(default, skip_serializing_if = "Option::is_none")]
1485        handling_mode: Option<HandlingMode>,
1486    },
1487}
1488
1489/// Cross-field validation failure for [`CommsCommandRequest::into_command`].
1490///
1491/// Per-field discriminator validation is enforced by serde at deserialization
1492/// — only invariants that span multiple fields surface here.
1493#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1494pub enum CommsCommandError {
1495    /// `handling_mode` is set on a `peer_response` whose machine-classified
1496    /// terminality is progress. Progress responses cannot carry a handling
1497    /// mode — the receiver's admission gate would drop them, so reject after
1498    /// generated terminality feedback is available.
1499    #[error("handling_mode is forbidden on progress peer responses")]
1500    HandlingModeForbiddenForProgressResponse,
1501}
1502
1503impl CommsCommandRequest {
1504    /// Convert the typed wire request into a [`CommsCommand`] domain envelope.
1505    ///
1506    /// `session_id` is supplied separately because it is owned by the
1507    /// surface that received the request, not the wire payload.
1508    pub fn into_command(
1509        self,
1510        session_id: &crate::types::SessionId,
1511    ) -> Result<CommsCommand, CommsCommandError> {
1512        Ok(match self {
1513            CommsCommandRequest::Input {
1514                body,
1515                blocks,
1516                source,
1517                stream,
1518                handling_mode,
1519                allow_self_session,
1520            } => CommsCommand::Input {
1521                session_id: session_id.clone(),
1522                body,
1523                blocks,
1524                handling_mode: handling_mode.unwrap_or_default(),
1525                source: source.unwrap_or(InputSource::Rpc),
1526                stream: stream.unwrap_or(InputStreamMode::None),
1527                allow_self_session: allow_self_session.unwrap_or(false),
1528            },
1529            CommsCommandRequest::PeerMessage {
1530                to,
1531                body,
1532                blocks,
1533                content_taint,
1534                handling_mode,
1535            } => CommsCommand::PeerMessage {
1536                to: PeerRoute::new(to),
1537                body,
1538                blocks,
1539                content_taint,
1540                handling_mode: handling_mode.unwrap_or_default(),
1541            },
1542            CommsCommandRequest::PeerLifecycle {
1543                to,
1544                lifecycle_kind,
1545                params,
1546            } => CommsCommand::PeerLifecycle {
1547                to: PeerRoute::new(to),
1548                kind: lifecycle_kind,
1549                params,
1550            },
1551            CommsCommandRequest::PeerRequest {
1552                to,
1553                intent,
1554                params,
1555                blocks,
1556                content_taint,
1557                handling_mode,
1558                stream,
1559            } => CommsCommand::PeerRequest {
1560                to: PeerRoute::new(to),
1561                // The domain envelope carries a wider, open intent vocabulary
1562                // (it also routes mob topology intents such as `mob.peer_added`),
1563                // so the closed public-request intent projects to its stable
1564                // wire literal here. This is the request -> open-envelope seam,
1565                // not a typed -> string downgrade at the public wire boundary.
1566                intent: intent.as_str().to_string(),
1567                params,
1568                blocks,
1569                content_taint,
1570                handling_mode: handling_mode.unwrap_or_default(),
1571                stream: stream.unwrap_or(InputStreamMode::None),
1572            },
1573            CommsCommandRequest::PeerResponse {
1574                to,
1575                in_reply_to,
1576                status,
1577                result,
1578                blocks,
1579                content_taint,
1580                handling_mode,
1581            } => CommsCommand::PeerResponse {
1582                to: PeerRoute::new(to),
1583                in_reply_to,
1584                status,
1585                result,
1586                blocks,
1587                content_taint,
1588                handling_mode,
1589            },
1590        })
1591    }
1592
1593    /// Stable wire discriminant for telemetry / logging.
1594    pub fn kind(&self) -> &'static str {
1595        match self {
1596            Self::Input { .. } => "input",
1597            Self::PeerMessage { .. } => "peer_message",
1598            Self::PeerLifecycle { .. } => "peer_lifecycle",
1599            Self::PeerRequest { .. } => "peer_request",
1600            Self::PeerResponse { .. } => "peer_response",
1601        }
1602    }
1603}
1604/// Source for an input event posted to an agent.
1605#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1607#[serde(rename_all = "lowercase")]
1608pub enum InputSource {
1609    Tcp,
1610    Uds,
1611    Stdin,
1612    Webhook,
1613    Rpc,
1614}
1615
1616impl From<crate::config::PlainEventSource> for InputSource {
1617    fn from(source: crate::config::PlainEventSource) -> Self {
1618        match source {
1619            crate::config::PlainEventSource::Tcp => Self::Tcp,
1620            crate::config::PlainEventSource::Uds => Self::Uds,
1621            crate::config::PlainEventSource::Stdin => Self::Stdin,
1622            crate::config::PlainEventSource::Webhook => Self::Webhook,
1623            crate::config::PlainEventSource::Rpc => Self::Rpc,
1624        }
1625    }
1626}
1627
1628impl From<InputSource> for crate::config::PlainEventSource {
1629    fn from(source: InputSource) -> Self {
1630        match source {
1631            InputSource::Tcp => Self::Tcp,
1632            InputSource::Uds => Self::Uds,
1633            InputSource::Stdin => Self::Stdin,
1634            InputSource::Webhook => Self::Webhook,
1635            InputSource::Rpc => Self::Rpc,
1636        }
1637    }
1638}
1639
1640/// Whether this input/peer command should reserve a local interaction stream.
1641#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1643#[serde(rename_all = "snake_case")]
1644pub enum InputStreamMode {
1645    /// Do not reserve any stream.
1646    None,
1647    /// Reserve an interaction stream for the command.
1648    ReserveInteraction,
1649}
1650
1651/// Transport-independent comms command envelope.
1652#[derive(Debug, Clone, PartialEq, Eq)]
1653pub enum CommsCommand {
1654    /// Inject input into the local session.
1655    Input {
1656        session_id: crate::types::SessionId,
1657        body: String,
1658        blocks: Option<Vec<ContentBlock>>,
1659        handling_mode: HandlingMode,
1660        source: InputSource,
1661        stream: InputStreamMode,
1662        allow_self_session: bool,
1663    },
1664    /// Send a one-way peer message.
1665    PeerMessage {
1666        to: PeerRoute,
1667        body: String,
1668        blocks: Option<Vec<ContentBlock>>,
1669        /// Per-send taint override: `None` inherits the runtime declaration.
1670        content_taint: Option<SendTaintOverride>,
1671        handling_mode: HandlingMode,
1672    },
1673    /// Send a one-way peer lifecycle notification.
1674    PeerLifecycle {
1675        to: PeerRoute,
1676        kind: PeerLifecycleKind,
1677        params: serde_json::Value,
1678    },
1679    /// Send a request to a peer.
1680    PeerRequest {
1681        to: PeerRoute,
1682        intent: String,
1683        params: serde_json::Value,
1684        blocks: Option<Vec<ContentBlock>>,
1685        /// Per-send taint override: `None` inherits the runtime declaration.
1686        content_taint: Option<SendTaintOverride>,
1687        handling_mode: HandlingMode,
1688        stream: InputStreamMode,
1689    },
1690    /// Send a response to a prior peer request.
1691    PeerResponse {
1692        to: PeerRoute,
1693        in_reply_to: InteractionId,
1694        status: ResponseStatus,
1695        result: serde_json::Value,
1696        blocks: Option<Vec<ContentBlock>>,
1697        /// Per-send taint override: `None` inherits the runtime declaration.
1698        content_taint: Option<SendTaintOverride>,
1699        handling_mode: Option<HandlingMode>,
1700    },
1701}
1702
1703impl CommsCommand {
1704    pub fn command_kind(&self) -> &'static str {
1705        match self {
1706            Self::Input { .. } => "input",
1707            Self::PeerMessage { .. } => "peer_message",
1708            Self::PeerLifecycle { .. } => "peer_lifecycle",
1709            Self::PeerRequest { .. } => "peer_request",
1710            Self::PeerResponse { .. } => "peer_response",
1711        }
1712    }
1713}
1714
1715/// Receipt returned after accepting a comms command.
1716#[derive(Debug, Clone, PartialEq, Eq)]
1717pub enum SendReceipt {
1718    InputAccepted {
1719        interaction_id: InteractionId,
1720        stream_reserved: bool,
1721    },
1722    PeerMessageSent {
1723        envelope_id: uuid::Uuid,
1724        acked: bool,
1725    },
1726    PeerLifecycleSent {
1727        envelope_id: uuid::Uuid,
1728    },
1729    PeerRequestSent {
1730        envelope_id: uuid::Uuid,
1731        interaction_id: InteractionId,
1732        stream_reserved: bool,
1733    },
1734    PeerResponseSent {
1735        envelope_id: uuid::Uuid,
1736        in_reply_to: InteractionId,
1737    },
1738}
1739
1740#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1741#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1742#[serde(rename_all = "snake_case")]
1743pub enum PeerDirectorySource {
1744    Trusted,
1745    Inproc,
1746    TrustedAndInproc,
1747    Unknown,
1748}
1749
1750impl PeerDirectorySource {
1751    pub const fn as_str(&self) -> &'static str {
1752        match self {
1753            Self::Trusted => "trusted",
1754            Self::Inproc => "inproc",
1755            Self::TrustedAndInproc => "trusted_and_inproc",
1756            Self::Unknown => "unknown",
1757        }
1758    }
1759}
1760
1761impl std::fmt::Display for PeerDirectorySource {
1762    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1763        f.write_str(self.as_str())
1764    }
1765}
1766
1767#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1768#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1769#[serde(rename_all = "snake_case")]
1770pub enum PeerSendability {
1771    PeerMessage,
1772    PeerRequest,
1773    PeerResponse,
1774}
1775
1776impl PeerSendability {
1777    pub const DIRECTORY_DEFAULTS: [Self; 3] =
1778        [Self::PeerMessage, Self::PeerRequest, Self::PeerResponse];
1779
1780    pub fn directory_defaults() -> Vec<Self> {
1781        Self::DIRECTORY_DEFAULTS.to_vec()
1782    }
1783
1784    pub const fn as_str(&self) -> &'static str {
1785        match self {
1786            Self::PeerMessage => "peer_message",
1787            Self::PeerRequest => "peer_request",
1788            Self::PeerResponse => "peer_response",
1789        }
1790    }
1791}
1792
1793impl std::fmt::Display for PeerSendability {
1794    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1795        f.write_str(self.as_str())
1796    }
1797}
1798
1799/// Typed peer capability envelope for peer-directory output.
1800///
1801/// Extensions are intentionally opaque display/integration metadata. Core
1802/// routing, admission, and policy decisions must use typed fields such as
1803/// [`PeerDirectoryEntry::sendable_kinds`] instead of consulting this bag.
1804#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1805#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1806pub struct PeerCapabilitySet {
1807    #[serde(default = "PeerCapabilitySet::default_version")]
1808    pub version: u16,
1809    #[serde(default)]
1810    pub extensions: BTreeMap<String, serde_json::Value>,
1811}
1812
1813impl PeerCapabilitySet {
1814    pub const CURRENT_VERSION: u16 = 1;
1815
1816    const fn default_version() -> u16 {
1817        Self::CURRENT_VERSION
1818    }
1819
1820    pub fn with_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
1821        self.extensions.insert(key.into(), value);
1822        self
1823    }
1824}
1825
1826impl Default for PeerCapabilitySet {
1827    fn default() -> Self {
1828        Self {
1829            version: Self::CURRENT_VERSION,
1830            extensions: BTreeMap::new(),
1831        }
1832    }
1833}
1834
1835#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1836#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1837pub struct PeerDirectoryEntry {
1838    /// Canonical runtime identity — the routing key.
1839    pub peer_id: PeerId,
1840    /// Display-only slug. Multiple entries may share a name; none share a
1841    /// `peer_id`.
1842    pub name: PeerName,
1843    /// Typed transport atom + endpoint. Replaces the prior free-form
1844    /// `address: String` so the transport cannot be invented by string
1845    /// concatenation at a call site.
1846    pub address: PeerAddress,
1847    pub source: PeerDirectorySource,
1848    pub sendable_kinds: Vec<PeerSendability>,
1849    pub capabilities: PeerCapabilitySet,
1850    /// Supplementary discovery metadata (description, labels).
1851    pub meta: crate::PeerMeta,
1852}
1853
1854#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1856pub struct PeerDirectoryListing {
1857    pub peers: Vec<PeerDirectoryEntry>,
1858}
1859
1860impl PeerDirectoryListing {
1861    pub fn new(peers: Vec<PeerDirectoryEntry>) -> Self {
1862        Self { peers }
1863    }
1864}
1865
1866impl From<Vec<PeerDirectoryEntry>> for PeerDirectoryListing {
1867    fn from(peers: Vec<PeerDirectoryEntry>) -> Self {
1868        Self::new(peers)
1869    }
1870}
1871
1872/// Scope for streaming event output.
1873#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1874pub enum StreamScope {
1875    Session(crate::types::SessionId),
1876    Interaction(InteractionId),
1877}
1878
1879/// Typed stream over enveloped agent events.
1880pub type EventStream = Pin<Box<dyn Stream<Item = EventEnvelope<AgentEvent>> + Send>>;
1881
1882/// Errors for stream attachment and lookup.
1883#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
1884pub enum StreamError {
1885    #[error("interaction not reserved: {0}")]
1886    NotReserved(InteractionId),
1887    #[error("stream not found: {0}")]
1888    NotFound(String),
1889    #[error("already attached: {0}")]
1890    AlreadyAttached(InteractionId),
1891    #[error("stream closed")]
1892    Closed,
1893    #[error("permission denied: {0}")]
1894    PermissionDenied(String),
1895    #[error("timeout: {0}")]
1896    Timeout(String),
1897    #[error("internal: {0}")]
1898    Internal(String),
1899}
1900
1901/// Typed reason a peer rejected our envelope at its ingress admission gate.
1902///
1903/// This mirrors `meerkat_comms::DropReason` across the core boundary so
1904/// `SendError::AdmissionDropped` can carry the typed cause all the way to
1905/// REST/RPC/MCP error payloads. Callers distinguish transport-level failure
1906/// (`PeerOffline`) from policy-level rejection (`AdmissionDropped { reason }`)
1907/// without collapsing both into "peer unreachable".
1908#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1909#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1910#[serde(rename_all = "snake_case")]
1911#[non_exhaustive]
1912pub enum AdmissionDropReason {
1913    /// `require_peer_auth` is on, the sender is not in the trusted set, and
1914    /// the envelope is not auth-exempt (e.g. supervisor-bridge bootstrap).
1915    UntrustedSender,
1916    /// Classification rejected the item before the admission gate ran.
1917    ClassificationRejected,
1918    /// The receiver's classified inbox is closed (receiver dropped).
1919    SessionClosed,
1920    /// The receiver's classified inbox is at capacity.
1921    InboxFull,
1922}
1923
1924impl AdmissionDropReason {
1925    /// Stable wire code for this drop reason, suitable for REST/RPC/MCP
1926    /// error payloads. Callers-facing discriminant — must stay stable.
1927    pub fn as_code(&self) -> &'static str {
1928        match self {
1929            AdmissionDropReason::UntrustedSender => "untrusted_sender",
1930            AdmissionDropReason::ClassificationRejected => "classification_rejected",
1931            AdmissionDropReason::SessionClosed => "session_closed",
1932            AdmissionDropReason::InboxFull => "inbox_full",
1933        }
1934    }
1935}
1936
1937#[derive(Debug, Clone, thiserror::Error)]
1938#[non_exhaustive]
1939pub enum SendError {
1940    #[error("peer not found: {0}")]
1941    PeerNotFound(String),
1942    #[error("peer offline")]
1943    PeerOffline,
1944    #[error("peer not sendable")]
1945    PeerNotSendable(String),
1946    #[error("input stream closed")]
1947    InputClosed,
1948    #[error("unsupported command: {0}")]
1949    Unsupported(String),
1950    #[error("validation failed: {0}")]
1951    Validation(String),
1952    #[error("internal: {0}")]
1953    Internal(String),
1954    /// The envelope could not reach the peer because the underlying transport
1955    /// (socket/IO) failed. Semantically distinct from `PeerOffline` (peer
1956    /// reachable but did not ack) and from `Internal` (host-side logic
1957    /// error): this is a connectivity failure surfaced as the
1958    /// `peer_unreachable` / `transport_error` wire class. Carries the
1959    /// transport cause for diagnostics (`details` payloads).
1960    #[error("transport error: {0}")]
1961    Transport(String),
1962    /// Receiver admitted the envelope-transport but rejected it at ingress
1963    /// for a typed policy reason (untrusted sender, full inbox, etc.). This
1964    /// is semantically distinct from `PeerOffline` — transport worked,
1965    /// policy refused.
1966    #[error("peer dropped at admission: {reason:?}")]
1967    AdmissionDropped { reason: AdmissionDropReason },
1968}
1969
1970#[derive(Debug, Clone, thiserror::Error)]
1971pub enum SendAndStreamError {
1972    #[error("send failed: {0}")]
1973    Send(#[from] SendError),
1974    #[error("stream attach failed: receipt={receipt:?}, error={error}")]
1975    StreamAttach {
1976        receipt: SendReceipt,
1977        error: StreamError,
1978    },
1979}
1980
1981#[cfg(test)]
1982#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1983mod tests {
1984    use super::*;
1985
1986    #[test]
1987    fn peer_id_pubkey_derivation_matches_uuid_v5() {
1988        let pubkey = [42u8; 32];
1989        assert_eq!(
1990            PeerId::from_ed25519_pubkey(&pubkey).as_uuid(),
1991            &Uuid::new_v5(&PEER_ID_ED25519_PUBKEY_NAMESPACE, &pubkey)
1992        );
1993    }
1994
1995    #[test]
1996    fn peer_name_validation() {
1997        assert!(PeerName::new("alice").is_ok());
1998        assert!(PeerName::new("".to_string()).is_err());
1999        assert!(PeerName::new("bad\x00name").is_err());
2000    }
2001
2002    #[test]
2003    fn peer_directory_entry_fields() -> Result<(), String> {
2004        let entry = PeerDirectoryEntry {
2005            peer_id: PeerId::new(),
2006            name: PeerName::new("agent")?,
2007            address: PeerAddress::new(PeerTransport::Inproc, "agent"),
2008            source: PeerDirectorySource::Inproc,
2009            sendable_kinds: vec![PeerSendability::PeerMessage],
2010            capabilities: PeerCapabilitySet::default(),
2011            meta: crate::PeerMeta::default(),
2012        };
2013        assert_eq!(entry.name.as_str(), "agent");
2014        assert_eq!(entry.address.transport(), PeerTransport::Inproc);
2015        assert_eq!(entry.address.endpoint(), "agent");
2016        assert_eq!(entry.source, PeerDirectorySource::Inproc);
2017        Ok(())
2018    }
2019
2020    #[test]
2021    fn peer_directory_listing_serializes_typed_source_sendability_and_capabilities()
2022    -> Result<(), String> {
2023        let entry = PeerDirectoryEntry {
2024            peer_id: PeerId::new(),
2025            name: PeerName::new("agent")?,
2026            address: PeerAddress::new(PeerTransport::Inproc, "agent"),
2027            source: PeerDirectorySource::Inproc,
2028            sendable_kinds: vec![PeerSendability::PeerMessage, PeerSendability::PeerRequest],
2029            capabilities: PeerCapabilitySet::default()
2030                .with_extension("vendor.echo", serde_json::json!({ "enabled": true })),
2031            meta: crate::PeerMeta::default(),
2032        };
2033
2034        let value = serde_json::to_value(PeerDirectoryListing::new(vec![entry]))
2035            .map_err(|err| err.to_string())?;
2036        let peer = &value["peers"][0];
2037
2038        assert_eq!(peer["source"], "inproc");
2039        assert_eq!(
2040            peer["sendable_kinds"],
2041            serde_json::json!(["peer_message", "peer_request"])
2042        );
2043        assert_eq!(peer["capabilities"]["version"], 1);
2044        assert_eq!(
2045            peer["capabilities"]["extensions"]["vendor.echo"]["enabled"],
2046            true
2047        );
2048        Ok(())
2049    }
2050
2051    #[test]
2052    fn generated_trust_authority_rejects_descriptor_peer_mismatch() {
2053        let pubkey = [1u8; 32];
2054        let descriptor_peer_id = PeerId::from_ed25519_pubkey(&pubkey);
2055        let requested_peer_id = PeerId::from_ed25519_pubkey(&[2u8; 32]);
2056        let descriptor = TrustedPeerDescriptor::unsigned_with_pubkey(
2057            "fake",
2058            descriptor_peer_id.to_string(),
2059            pubkey,
2060            "inproc://fake",
2061        )
2062        .expect("valid descriptor");
2063        let err = CommsTrustMutationAuthority::from_generated_parts(
2064            GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
2065            1,
2066            None,
2067            GeneratedCommsTrustAuthoritySourceKind::MeerkatMachinePeerProjection,
2068            GeneratedCommsTrustAuthorityOperation::PublicAdd,
2069            requested_peer_id.to_string(),
2070            Some(requested_peer_id.to_string()),
2071            Some(descriptor),
2072        )
2073        .expect_err("descriptor for another peer must not mint authority");
2074        assert!(
2075            err.contains("does not match requested"),
2076            "unexpected rejection: {err}"
2077        );
2078    }
2079
2080    #[test]
2081    fn peer_id_parse_round_trip() {
2082        let id = PeerId::new();
2083        let parsed = PeerId::parse(&id.as_str()).expect("parse");
2084        assert_eq!(id, parsed);
2085    }
2086
2087    #[test]
2088    fn peer_id_parse_rejects_garbage() {
2089        let err = PeerId::parse("not-a-uuid").expect_err("parse must reject");
2090        match err {
2091            PeerIdError::Invalid { input, .. } => assert_eq!(input, "not-a-uuid"),
2092        }
2093    }
2094
2095    #[test]
2096    fn peer_address_display() {
2097        let addr = PeerAddress::new(PeerTransport::Tcp, "127.0.0.1:4200");
2098        assert_eq!(addr.to_string(), "tcp://127.0.0.1:4200");
2099    }
2100
2101    #[test]
2102    fn peer_address_parse_round_trips_supported_schemes() {
2103        let cases = [
2104            ("inproc://agent-a", PeerTransport::Inproc, "agent-a"),
2105            (
2106                "uds:///tmp/meerkat.sock",
2107                PeerTransport::Uds,
2108                "/tmp/meerkat.sock",
2109            ),
2110            ("tcp://127.0.0.1:4200", PeerTransport::Tcp, "127.0.0.1:4200"),
2111        ];
2112
2113        for (raw, transport, endpoint) in cases {
2114            let parsed = PeerAddress::parse(raw).expect("supported address parses");
2115            assert_eq!(parsed.transport(), transport);
2116            assert_eq!(parsed.endpoint(), endpoint);
2117            assert_eq!(parsed.to_string(), raw);
2118        }
2119    }
2120
2121    #[test]
2122    fn peer_address_parse_rejects_unknown_scheme() {
2123        let err = PeerAddress::parse("http://127.0.0.1:4200")
2124            .expect_err("unknown transport schemes must fail closed");
2125        assert!(
2126            err.to_string().contains("unknown peer address transport"),
2127            "unexpected error: {err}",
2128        );
2129    }
2130
2131    #[test]
2132    fn peer_address_parse_rejects_schemeless_input() {
2133        let err = PeerAddress::parse("127.0.0.1:4200")
2134            .expect_err("strict parser requires an address scheme");
2135        assert!(
2136            err.to_string().contains("missing transport scheme"),
2137            "unexpected error: {err}",
2138        );
2139    }
2140
2141    #[test]
2142    fn input_stream_mode_roundtrip() -> Result<(), serde_json::Error> {
2143        let mode = InputStreamMode::ReserveInteraction;
2144        let serialized = serde_json::to_value(mode)?;
2145        assert_eq!(serialized.as_str(), Some("reserve_interaction"));
2146        assert_eq!(serde_json::from_value::<InputStreamMode>(serialized)?, mode);
2147        Ok(())
2148    }
2149
2150    #[test]
2151    fn deserialize_input_with_typed_source() -> Result<(), serde_json::Error> {
2152        let json = r#"{"kind":"input","body":"hello","source":"webhook","handling_mode":"steer"}"#;
2153        let req: CommsCommandRequest = serde_json::from_str(json)?;
2154        match req {
2155            CommsCommandRequest::Input {
2156                body,
2157                source,
2158                handling_mode,
2159                ..
2160            } => {
2161                assert_eq!(body, "hello");
2162                assert_eq!(source, Some(InputSource::Webhook));
2163                assert_eq!(handling_mode, Some(HandlingMode::Steer));
2164            }
2165            other => panic!("expected input command request, got {other:?}"),
2166        }
2167        Ok(())
2168    }
2169
2170    #[test]
2171    fn deserialize_input_invalid_source_rejects_at_serde_boundary() {
2172        let json = r#"{"kind":"input","body":"hello","source":"webhookd"}"#;
2173        let err = serde_json::from_str::<CommsCommandRequest>(json)
2174            .expect_err("invalid source must fail deserialization");
2175        let msg = err.to_string();
2176        // serde reports "unknown variant `webhookd`, expected one of ...".
2177        assert!(
2178            msg.contains("webhookd"),
2179            "error should name the rejected value, got: {msg}"
2180        );
2181    }
2182
2183    #[test]
2184    fn deserialize_unknown_kind_rejects_at_serde_boundary() {
2185        let json = r#"{"kind":"foobar","body":"hello"}"#;
2186        let err = serde_json::from_str::<CommsCommandRequest>(json)
2187            .expect_err("unknown kind must fail deserialization");
2188        let msg = err.to_string();
2189        assert!(
2190            msg.contains("foobar") || msg.contains("variant"),
2191            "error should mention unknown variant, got: {msg}"
2192        );
2193    }
2194}