1#![allow(clippy::unwrap_used)]
2#![allow(clippy::expect_used)]
3#![allow(missing_docs)]
4
5pub mod error;
57
58pub mod identity;
64
65pub mod storage;
70
71pub mod revocation;
77
78pub mod bootstrap;
83pub mod network;
85
86pub mod streams;
88
89pub mod forward;
91
92pub mod contacts;
94
95pub mod trust;
100
101pub mod connectivity;
106
107pub mod gossip;
109
110pub mod crdt;
112
113pub mod kv;
115
116pub mod groups;
118
119pub mod mls;
121
122pub mod a2a;
124
125pub mod direct;
130
131pub mod dm;
135
136pub mod dm_capability;
140
141pub mod dm_capability_service;
144
145pub mod dm_inbox;
149
150pub mod dm_send;
153
154pub mod peer_relay;
159
160pub mod presence;
162
163pub mod upgrade;
165
166pub mod files;
168
169pub mod connect;
170pub mod exec;
172
173pub mod constitution;
175
176pub mod logging;
178
179pub mod api;
181
182pub mod cli;
184
185pub mod server;
187
188pub use gossip::{
190 GossipConfig, GossipRuntime, PubSubManager, PubSubMessage, PubSubStats, PubSubStatsSnapshot,
191 SigningContext, Subscription,
192};
193
194pub use direct::{DirectMessage, DirectMessageReceiver, DirectMessaging};
196
197use saorsa_gossip_membership::Membership as _;
199
200pub struct Agent {
222 identity: std::sync::Arc<identity::Identity>,
223 #[allow(dead_code)]
225 network: Option<std::sync::Arc<network::NetworkNode>>,
226 gossip_runtime: Option<std::sync::Arc<gossip::GossipRuntime>>,
228 bootstrap_cache: Option<std::sync::Arc<ant_quic::BootstrapCache>>,
230 gossip_cache_adapter: Option<saorsa_gossip_coordinator::GossipCacheAdapter>,
232 identity_discovery_cache: std::sync::Arc<
234 tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
235 >,
236 authenticated_machine_bindings: dm_inbox::AuthenticatedMachineBindings,
244 machine_discovery_cache: std::sync::Arc<
247 tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
248 >,
249 user_discovery_cache: std::sync::Arc<
252 tokio::sync::RwLock<std::collections::HashMap<identity::UserId, DiscoveredUser>>,
253 >,
254 identity_listener_started: std::sync::atomic::AtomicBool,
256 heartbeat_interval_secs: u64,
258 identity_ttl_secs: u64,
260 heartbeat_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
262 discovery_cache_reaper_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
267 rendezvous_advertised: std::sync::atomic::AtomicBool,
269 contact_store: std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>,
271 direct_messaging: std::sync::Arc<direct::DirectMessaging>,
273 network_event_listener_started: std::sync::atomic::AtomicBool,
275 direct_listener_started: std::sync::atomic::AtomicBool,
277 presence: Option<std::sync::Arc<presence::PresenceWrapper>>,
279 user_identity_consented: std::sync::Arc<std::sync::atomic::AtomicBool>,
283 capability_store: std::sync::Arc<dm_capability::CapabilityStore>,
286 dm_capabilities_tx: std::sync::Arc<tokio::sync::watch::Sender<dm::DmCapabilities>>,
291 dm_inflight_acks: std::sync::Arc<dm::InFlightAcks>,
293 recent_delivery_cache: std::sync::Arc<dm::RecentDeliveryCache>,
295 capability_advert_service:
297 tokio::sync::Mutex<Option<dm_capability_service::CapabilityAdvertService>>,
298 dm_inbox_service: tokio::sync::Mutex<Option<dm_inbox::DmInboxService>>,
300 revocation_set: std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>>,
304 identity_dir: Option<std::path::PathBuf>,
307 shutdown_token: tokio_util::sync::CancellationToken,
313 tracked_tasks: std::sync::Arc<std::sync::Mutex<TrackedTasks>>,
319 peer_relay: std::sync::Arc<peer_relay::PeerRelay>,
326 relay_candidates: std::sync::Arc<tokio::sync::RwLock<Vec<identity::AgentId>>>,
331 stream_accept: std::sync::Arc<streams::StreamAccept>,
335}
336
337struct TrackedTasks {
344 closed: bool,
345 handles: Vec<tokio::task::JoinHandle<()>>,
346}
347
348impl std::fmt::Debug for Agent {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 f.debug_struct("Agent")
351 .field("identity", &self.identity)
352 .field("network", &self.network.is_some())
353 .field("gossip_runtime", &self.gossip_runtime.is_some())
354 .field("bootstrap_cache", &self.bootstrap_cache.is_some())
355 .field("gossip_cache_adapter", &self.gossip_cache_adapter.is_some())
356 .finish()
357 }
358}
359
360impl Drop for Agent {
361 fn drop(&mut self) {
362 if let Ok(mut handle_guard) = self.heartbeat_handle.try_lock() {
363 if let Some(handle) = handle_guard.take() {
364 handle.abort();
365 }
366 }
367 if let Ok(mut reaper_guard) = self.discovery_cache_reaper_handle.try_lock() {
368 if let Some(handle) = reaper_guard.take() {
369 handle.abort();
370 }
371 }
372 }
373}
374
375#[derive(Debug, Clone)]
377pub struct Message {
378 pub origin: String,
380 pub payload: Vec<u8>,
382 pub topic: String,
384}
385
386pub const IDENTITY_ANNOUNCE_TOPIC: &str = "x0x.identity.announce.v2";
391
392pub const MACHINE_ANNOUNCE_TOPIC: &str = "x0x.machine.announce.v2";
397
398pub const USER_ANNOUNCE_TOPIC: &str = "x0x.user.announce.v2";
405
406pub const REVOCATION_TOPIC: &str = "x0x.revocation.v1";
413
414#[must_use]
424pub fn shard_topic_for_agent(agent_id: &identity::AgentId) -> String {
425 let shard = saorsa_gossip_rendezvous::calculate_shard(&agent_id.0);
426 format!("x0x.identity.shard.v2.{shard}")
427}
428
429#[must_use]
434pub fn shard_topic_for_machine(machine_id: &identity::MachineId) -> String {
435 let shard = saorsa_gossip_rendezvous::calculate_shard(&machine_id.0);
436 format!("x0x.machine.shard.v2.{shard}")
437}
438
439#[must_use]
445pub fn shard_topic_for_user(user_id: &identity::UserId) -> String {
446 let shard = saorsa_gossip_rendezvous::calculate_shard(&user_id.0);
447 format!("x0x.user.shard.v2.{shard}")
448}
449
450pub const RENDEZVOUS_SHARD_TOPIC_PREFIX: &str = "x0x.rendezvous.shard";
452
453#[must_use]
459pub fn rendezvous_shard_topic_for_agent(agent_id: &identity::AgentId) -> String {
460 let shard = saorsa_gossip_rendezvous::calculate_shard(&agent_id.0);
461 format!("{RENDEZVOUS_SHARD_TOPIC_PREFIX}.{shard}")
462}
463
464fn is_globally_routable(ip: std::net::IpAddr) -> bool {
471 match ip {
472 std::net::IpAddr::V4(v4) => {
473 !v4.is_private() && !v4.is_loopback() && !v4.is_link_local() && !v4.is_unspecified() && !v4.is_broadcast() && !v4.is_documentation() && !(v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64)
481 }
482 std::net::IpAddr::V6(v6) => {
483 let segs = v6.segments();
484 !v6.is_loopback() && !v6.is_unspecified() && (segs[0] & 0xffc0) != 0xfe80 && (segs[0] & 0xfe00) != 0xfc00 && (segs[0] & 0xfff0) != 0xfec0 }
490 }
491}
492
493pub fn is_publicly_advertisable(addr: std::net::SocketAddr) -> bool {
505 addr.port() > 0 && is_globally_routable(addr.ip())
506}
507
508fn filter_publicly_advertisable_addrs<I>(addresses: I) -> Vec<std::net::SocketAddr>
509where
510 I: IntoIterator<Item = std::net::SocketAddr>,
511{
512 addresses
513 .into_iter()
514 .filter(|addr| is_publicly_advertisable(*addr))
515 .collect()
516}
517
518fn is_local_discovery_addr(addr: std::net::SocketAddr) -> bool {
519 if addr.port() == 0 {
520 return false;
521 }
522 match addr.ip() {
523 std::net::IpAddr::V4(v4) => {
524 !v4.is_unspecified()
525 && !v4.is_broadcast()
526 && !v4.is_documentation()
527 && !v4.is_link_local()
528 }
529 std::net::IpAddr::V6(v6) => {
530 let segs = v6.segments();
531 !v6.is_unspecified() && (segs[0] & 0xffc0) != 0xfe80 && (segs[0] & 0xfff0) != 0xfec0
532 }
533 }
534}
535
536fn filter_local_discovery_addrs<I>(addresses: I) -> Vec<std::net::SocketAddr>
537where
538 I: IntoIterator<Item = std::net::SocketAddr>,
539{
540 let mut filtered = Vec::new();
541 for addr in addresses {
542 if is_local_discovery_addr(addr) && !filtered.contains(&addr) {
543 filtered.push(addr);
544 }
545 }
546 filtered
547}
548
549fn filter_discovery_announcement_addrs<I>(
550 addresses: I,
551 allow_local_scope: bool,
552) -> Vec<std::net::SocketAddr>
553where
554 I: IntoIterator<Item = std::net::SocketAddr>,
555{
556 if allow_local_scope {
557 filter_local_discovery_addrs(addresses)
558 } else {
559 filter_publicly_advertisable_addrs(addresses)
560 }
561}
562
563async fn register_announced_machine(
574 contact_store: &std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>,
575 own_agent_id: identity::AgentId,
576 announced_agent_id: identity::AgentId,
577 announced_machine_id: identity::MachineId,
578) -> bool {
579 if announced_agent_id == own_agent_id {
580 return false;
581 }
582 let mut store = contact_store.write().await;
583 let record = contacts::MachineRecord::new(announced_machine_id, None);
584 store.add_machine(&announced_agent_id, record)
585}
586
587fn local_scoped_bootstrap_addr(addr: std::net::SocketAddr) -> bool {
588 if addr.port() == 0 {
589 return false;
590 }
591 match addr.ip() {
592 std::net::IpAddr::V4(v4) => {
593 v4.is_loopback() || v4.is_private() || is_cgnat_v4(v4) || v4.is_link_local()
594 }
595 std::net::IpAddr::V6(v6) => {
596 let segs = v6.segments();
597 v6.is_loopback() || (segs[0] & 0xfe00) == 0xfc00 || (segs[0] & 0xffc0) == 0xfe80
598 }
599 }
600}
601
602fn allow_local_discovery_addresses(config: &network::NetworkConfig) -> bool {
603 config.bootstrap_nodes.is_empty()
604 || config
605 .bootstrap_nodes
606 .iter()
607 .copied()
608 .all(local_scoped_bootstrap_addr)
609}
610
611pub fn collect_local_interface_addrs(port: u16) -> Vec<std::net::SocketAddr> {
612 fn is_cgnat(v4: std::net::Ipv4Addr) -> bool {
613 v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64
614 }
615
616 fn addr_priority(ip: std::net::IpAddr) -> u8 {
617 match ip {
618 std::net::IpAddr::V4(v4) => {
619 if is_globally_routable(std::net::IpAddr::V4(v4)) {
620 0
621 } else if is_cgnat(v4) {
622 1
623 } else {
624 2
625 }
626 }
627 std::net::IpAddr::V6(v6) => {
628 if is_globally_routable(std::net::IpAddr::V6(v6)) {
629 3
630 } else {
631 4
632 }
633 }
634 }
635 }
636
637 let mut ranked = Vec::new();
638
639 let interfaces = match if_addrs::get_if_addrs() {
640 Ok(interfaces) => interfaces,
641 Err(_) => return Vec::new(),
642 };
643
644 for iface in interfaces {
645 let ip = iface.ip();
646 if ip.is_unspecified() || ip.is_loopback() {
647 continue;
648 }
649
650 let addr = match ip {
651 std::net::IpAddr::V4(v4) => {
652 if v4.is_link_local() {
653 continue;
654 }
655 std::net::SocketAddr::new(std::net::IpAddr::V4(v4), port)
656 }
657 std::net::IpAddr::V6(v6) => {
658 let segs = v6.segments();
659 let is_link_local = (segs[0] & 0xffc0) == 0xfe80;
660 if is_link_local {
661 continue;
662 }
663 std::net::SocketAddr::new(std::net::IpAddr::V6(v6), port)
664 }
665 };
666
667 if !ranked.iter().any(|(_, existing)| *existing == addr) {
668 ranked.push((addr_priority(addr.ip()), addr));
669 }
670 }
671
672 ranked.sort_by_key(|(priority, addr)| (*priority, addr.is_ipv6()));
673 ranked.into_iter().map(|(_, addr)| addr).collect()
674}
675
676fn is_cgnat_v4(v4: std::net::Ipv4Addr) -> bool {
677 v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64
678}
679
680fn same_v4_24(a: std::net::Ipv4Addr, b: std::net::Ipv4Addr) -> bool {
681 let a = a.octets();
682 let b = b.octets();
683 a[0] == b[0] && a[1] == b[1] && a[2] == b[2]
684}
685
686fn local_direct_probe_priority(
687 addr: std::net::SocketAddr,
688 local_v4s: &[std::net::Ipv4Addr],
689) -> Option<u8> {
690 let std::net::IpAddr::V4(v4) = addr.ip() else {
691 return None;
692 };
693 if addr.port() == 0 || v4.is_loopback() || v4.is_link_local() || v4.is_unspecified() {
694 return None;
695 }
696 if local_v4s.iter().any(|local| same_v4_24(*local, v4)) {
697 return Some(0);
698 }
699 if v4.is_private() {
700 return Some(1);
701 }
702 if is_cgnat_v4(v4) {
703 return Some(2);
704 }
705 None
706}
707
708fn local_direct_probe_addrs_with_local_v4s(
709 addresses: &[std::net::SocketAddr],
710 local_v4s: &[std::net::Ipv4Addr],
711) -> Vec<std::net::SocketAddr> {
712 let mut ranked = addresses
713 .iter()
714 .copied()
715 .filter_map(|addr| local_direct_probe_priority(addr, local_v4s).map(|rank| (rank, addr)))
716 .collect::<Vec<_>>();
717 ranked.sort_by_key(|(rank, addr)| (*rank, *addr));
718 ranked.dedup_by_key(|(_, addr)| *addr);
719 ranked.into_iter().map(|(_, addr)| addr).collect()
720}
721
722fn local_direct_probe_addrs(addresses: &[std::net::SocketAddr]) -> Vec<std::net::SocketAddr> {
723 let local_v4s = collect_local_interface_addrs(0)
724 .into_iter()
725 .filter_map(|addr| match addr.ip() {
726 std::net::IpAddr::V4(v4) => Some(v4),
727 std::net::IpAddr::V6(_) => None,
728 })
729 .collect::<Vec<_>>();
730 local_direct_probe_addrs_with_local_v4s(addresses, &local_v4s)
731}
732
733pub const IDENTITY_HEARTBEAT_INTERVAL_SECS: u64 = 300;
741
742pub const IDENTITY_TTL_SECS: u64 = 900;
747
748const DISCOVERY_REBROADCAST_STATE_CAP: usize = 1024;
749const DISCOVERY_REBROADCAST_STATE_TTL: std::time::Duration = std::time::Duration::from_secs(3600);
750
751const DISCOVERY_CACHE_REAPER_INTERVAL_SECS: u64 = 120;
757
758fn discovery_record_is_live(_announced_at: u64, last_seen: u64, cutoff: u64) -> bool {
759 last_seen >= cutoff
760}
761
762fn should_rebroadcast_discovery_once<K>(
763 state: &mut std::collections::HashMap<K, std::time::Instant>,
764 key: K,
765 now: std::time::Instant,
766) -> bool
767where
768 K: Eq + std::hash::Hash,
769{
770 match state.entry(key) {
771 std::collections::hash_map::Entry::Occupied(_) => false,
772 std::collections::hash_map::Entry::Vacant(entry) => {
773 entry.insert(now);
774 if state.len() > DISCOVERY_REBROADCAST_STATE_CAP {
775 if let Some(cutoff) = now.checked_sub(DISCOVERY_REBROADCAST_STATE_TTL) {
776 state.retain(|_, seen_at| *seen_at >= cutoff);
777 }
778 }
779 true
780 }
781 }
782}
783
784#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
785struct IdentityAnnouncementUnsigned {
786 agent_id: identity::AgentId,
787 machine_id: identity::MachineId,
788 user_id: Option<identity::UserId>,
789 agent_certificate: Option<identity::AgentCertificate>,
790 machine_public_key: Vec<u8>,
791 addresses: Vec<std::net::SocketAddr>,
792 announced_at: u64,
793 nat_type: Option<String>,
795 can_receive_direct: Option<bool>,
797 is_relay: Option<bool>,
802 is_coordinator: Option<bool>,
807 reachable_via: Vec<identity::MachineId>,
814 relay_candidates: Vec<identity::MachineId>,
818}
819
820#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
825pub struct IdentityAnnouncement {
826 pub agent_id: identity::AgentId,
828 pub machine_id: identity::MachineId,
830 pub user_id: Option<identity::UserId>,
832 pub agent_certificate: Option<identity::AgentCertificate>,
834 pub machine_public_key: Vec<u8>,
836 pub machine_signature: Vec<u8>,
838 pub addresses: Vec<std::net::SocketAddr>,
840 pub announced_at: u64,
842 pub nat_type: Option<String>,
845 pub can_receive_direct: Option<bool>,
848 pub is_relay: Option<bool>,
854 pub is_coordinator: Option<bool>,
860 pub reachable_via: Vec<identity::MachineId>,
868 pub relay_candidates: Vec<identity::MachineId>,
873 pub agent_public_key: Vec<u8>,
883}
884
885impl IdentityAnnouncement {
886 fn to_unsigned(&self) -> IdentityAnnouncementUnsigned {
887 IdentityAnnouncementUnsigned {
888 agent_id: self.agent_id,
889 machine_id: self.machine_id,
890 user_id: self.user_id,
891 agent_certificate: self.agent_certificate.clone(),
892 machine_public_key: self.machine_public_key.clone(),
893 addresses: self.addresses.clone(),
894 announced_at: self.announced_at,
895 nat_type: self.nat_type.clone(),
896 can_receive_direct: self.can_receive_direct,
897 is_relay: self.is_relay,
898 is_coordinator: self.is_coordinator,
899 reachable_via: self.reachable_via.clone(),
900 relay_candidates: self.relay_candidates.clone(),
901 }
902 }
903
904 pub fn verify(&self) -> error::Result<()> {
906 let machine_pub =
907 ant_quic::MlDsaPublicKey::from_bytes(&self.machine_public_key).map_err(|_| {
908 error::IdentityError::CertificateVerification(
909 "invalid machine public key in announcement".to_string(),
910 )
911 })?;
912 let derived_machine_id = identity::MachineId::from_public_key(&machine_pub);
913 if derived_machine_id != self.machine_id {
914 return Err(error::IdentityError::CertificateVerification(
915 "machine_id does not match machine public key".to_string(),
916 ));
917 }
918
919 let unsigned_bytes = bincode::serialize(&self.to_unsigned()).map_err(|e| {
920 error::IdentityError::Serialization(format!(
921 "failed to serialize announcement for verification: {e}"
922 ))
923 })?;
924 let signature = ant_quic::crypto::raw_public_keys::pqc::MlDsaSignature::from_bytes(
925 &self.machine_signature,
926 )
927 .map_err(|e| {
928 error::IdentityError::CertificateVerification(format!(
929 "invalid machine signature in announcement: {:?}",
930 e
931 ))
932 })?;
933 ant_quic::crypto::raw_public_keys::pqc::verify_with_ml_dsa(
934 &machine_pub,
935 &unsigned_bytes,
936 &signature,
937 )
938 .map_err(|e| {
939 error::IdentityError::CertificateVerification(format!(
940 "machine signature verification failed: {:?}",
941 e
942 ))
943 })?;
944
945 match (self.user_id, self.agent_certificate.as_ref()) {
946 (Some(user_id), Some(cert)) => {
947 cert.verify()?;
948 let cert_agent_id = cert.agent_id()?;
949 if cert_agent_id != self.agent_id {
950 return Err(error::IdentityError::CertificateVerification(
951 "agent certificate agent_id mismatch".to_string(),
952 ));
953 }
954 let cert_user_id = cert.user_id()?;
955 if cert_user_id != user_id {
956 return Err(error::IdentityError::CertificateVerification(
957 "agent certificate user_id mismatch".to_string(),
958 ));
959 }
960 Ok(())
961 }
962 (None, None) => Ok(()),
963 _ => Err(error::IdentityError::CertificateVerification(
964 "user identity disclosure requires matching certificate".to_string(),
965 )),
966 }
967 }
968}
969
970const IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS: u64 = dm::CLOCK_SKEW_TOLERANCE_MS / 1_000;
973
974fn identity_announcement_timestamp_is_acceptable(announced_at: u64, now: u64) -> bool {
975 announced_at <= now.saturating_add(IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS)
976}
977
978fn identity_announcement_has_direct_agent_origin(
979 msg: &gossip::PubSubMessage,
980 announcement: &IdentityAnnouncement,
981) -> bool {
982 if !msg.verified || msg.sender != Some(announcement.agent_id) {
983 return false;
984 }
985 let Some(sender_public_key) = msg.sender_public_key.as_deref() else {
986 return false;
987 };
988 let Ok(sender_public_key) = ant_quic::MlDsaPublicKey::from_bytes(sender_public_key) else {
989 return false;
990 };
991 identity::AgentId::from_public_key(&sender_public_key) == announcement.agent_id
992}
993
994async fn record_authenticated_machine_binding_from_message(
998 bindings: &dm_inbox::AuthenticatedMachineBindings,
999 msg: &gossip::PubSubMessage,
1000 announcement: &IdentityAnnouncement,
1001 now: u64,
1002) -> bool {
1003 if !identity_announcement_timestamp_is_acceptable(announcement.announced_at, now) {
1004 tracing::warn!(
1005 agent = %hex::encode(announcement.agent_id.as_bytes()),
1006 announced_at = announcement.announced_at,
1007 now,
1008 max_future_skew_secs = IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS,
1009 "ignoring far-future identity announcement for authenticated machine binding"
1010 );
1011 return false;
1012 }
1013 if !identity_announcement_has_direct_agent_origin(msg, announcement) {
1014 tracing::debug!(
1015 agent = %hex::encode(announcement.agent_id.as_bytes()),
1016 sender = ?msg.sender.map(|id| hex::encode(id.as_bytes())),
1017 "identity announcement is not direct-origin authenticated; retained binding unchanged"
1018 );
1019 return false;
1020 }
1021 dm_inbox::record_authenticated_machine_binding(
1022 bindings,
1023 announcement.agent_id,
1024 announcement.machine_id,
1025 announcement.announced_at,
1026 )
1027 .await;
1028 true
1029}
1030
1031#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1032struct MachineAnnouncementUnsigned {
1033 machine_id: identity::MachineId,
1034 machine_public_key: Vec<u8>,
1035 addresses: Vec<std::net::SocketAddr>,
1036 announced_at: u64,
1037 nat_type: Option<String>,
1039 can_receive_direct: Option<bool>,
1041 is_relay: Option<bool>,
1043 is_coordinator: Option<bool>,
1045 reachable_via: Vec<identity::MachineId>,
1047 relay_candidates: Vec<identity::MachineId>,
1049}
1050
1051#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1058pub struct MachineAnnouncement {
1059 pub machine_id: identity::MachineId,
1061 pub machine_public_key: Vec<u8>,
1063 pub machine_signature: Vec<u8>,
1065 pub addresses: Vec<std::net::SocketAddr>,
1067 pub announced_at: u64,
1069 pub nat_type: Option<String>,
1072 pub can_receive_direct: Option<bool>,
1075 pub is_relay: Option<bool>,
1077 pub is_coordinator: Option<bool>,
1079 pub reachable_via: Vec<identity::MachineId>,
1085 pub relay_candidates: Vec<identity::MachineId>,
1087}
1088
1089impl MachineAnnouncement {
1090 fn to_unsigned(&self) -> MachineAnnouncementUnsigned {
1091 MachineAnnouncementUnsigned {
1092 machine_id: self.machine_id,
1093 machine_public_key: self.machine_public_key.clone(),
1094 addresses: self.addresses.clone(),
1095 announced_at: self.announced_at,
1096 nat_type: self.nat_type.clone(),
1097 can_receive_direct: self.can_receive_direct,
1098 is_relay: self.is_relay,
1099 is_coordinator: self.is_coordinator,
1100 reachable_via: self.reachable_via.clone(),
1101 relay_candidates: self.relay_candidates.clone(),
1102 }
1103 }
1104
1105 pub fn verify(&self) -> error::Result<()> {
1107 let machine_pub =
1108 ant_quic::MlDsaPublicKey::from_bytes(&self.machine_public_key).map_err(|_| {
1109 error::IdentityError::CertificateVerification(
1110 "invalid machine public key in machine announcement".to_string(),
1111 )
1112 })?;
1113 let derived_machine_id = identity::MachineId::from_public_key(&machine_pub);
1114 if derived_machine_id != self.machine_id {
1115 return Err(error::IdentityError::CertificateVerification(
1116 "machine_id does not match machine public key".to_string(),
1117 ));
1118 }
1119
1120 let unsigned_bytes = bincode::serialize(&self.to_unsigned()).map_err(|e| {
1121 error::IdentityError::Serialization(format!(
1122 "failed to serialize machine announcement for verification: {e}"
1123 ))
1124 })?;
1125 let signature = ant_quic::crypto::raw_public_keys::pqc::MlDsaSignature::from_bytes(
1126 &self.machine_signature,
1127 )
1128 .map_err(|e| {
1129 error::IdentityError::CertificateVerification(format!(
1130 "invalid machine signature in machine announcement: {:?}",
1131 e
1132 ))
1133 })?;
1134 ant_quic::crypto::raw_public_keys::pqc::verify_with_ml_dsa(
1135 &machine_pub,
1136 &unsigned_bytes,
1137 &signature,
1138 )
1139 .map_err(|e| {
1140 error::IdentityError::CertificateVerification(format!(
1141 "machine announcement signature verification failed: {:?}",
1142 e
1143 ))
1144 })
1145 }
1146}
1147
1148#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1149struct UserAnnouncementUnsigned {
1150 user_id: identity::UserId,
1151 user_public_key: Vec<u8>,
1152 agent_certificates: Vec<identity::AgentCertificate>,
1157 announced_at: u64,
1158}
1159
1160#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1169pub struct UserAnnouncement {
1170 pub user_id: identity::UserId,
1172 pub user_public_key: Vec<u8>,
1174 pub user_signature: Vec<u8>,
1176 pub agent_certificates: Vec<identity::AgentCertificate>,
1178 pub announced_at: u64,
1180}
1181
1182impl UserAnnouncement {
1183 fn to_unsigned(&self) -> UserAnnouncementUnsigned {
1184 UserAnnouncementUnsigned {
1185 user_id: self.user_id,
1186 user_public_key: self.user_public_key.clone(),
1187 agent_certificates: self.agent_certificates.clone(),
1188 announced_at: self.announced_at,
1189 }
1190 }
1191
1192 pub fn sign(
1201 user_kp: &identity::UserKeypair,
1202 agent_certificates: Vec<identity::AgentCertificate>,
1203 announced_at: u64,
1204 ) -> error::Result<Self> {
1205 let user_id = user_kp.user_id();
1206 for cert in &agent_certificates {
1209 let cert_user = cert.user_id()?;
1210 if cert_user != user_id {
1211 return Err(error::IdentityError::CertificateVerification(
1212 "user announcement contains certificate issued by a different user".to_string(),
1213 ));
1214 }
1215 }
1216
1217 let user_public_key = user_kp.public_key().as_bytes().to_vec();
1218 let unsigned = UserAnnouncementUnsigned {
1219 user_id,
1220 user_public_key: user_public_key.clone(),
1221 agent_certificates: agent_certificates.clone(),
1222 announced_at,
1223 };
1224 let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
1225 error::IdentityError::Serialization(format!(
1226 "failed to serialize unsigned user announcement: {e}"
1227 ))
1228 })?;
1229 let user_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
1230 user_kp.secret_key(),
1231 &unsigned_bytes,
1232 )
1233 .map_err(|e| {
1234 error::IdentityError::Storage(std::io::Error::other(format!(
1235 "failed to sign user announcement with user key: {e:?}"
1236 )))
1237 })?
1238 .as_bytes()
1239 .to_vec();
1240
1241 Ok(Self {
1242 user_id,
1243 user_public_key,
1244 user_signature,
1245 agent_certificates,
1246 announced_at,
1247 })
1248 }
1249
1250 pub fn verify(&self) -> error::Result<()> {
1262 let user_pub =
1263 ant_quic::MlDsaPublicKey::from_bytes(&self.user_public_key).map_err(|_| {
1264 error::IdentityError::CertificateVerification(
1265 "invalid user public key in user announcement".to_string(),
1266 )
1267 })?;
1268 let derived_user_id = identity::UserId::from_public_key(&user_pub);
1269 if derived_user_id != self.user_id {
1270 return Err(error::IdentityError::CertificateVerification(
1271 "user_id does not match user public key in user announcement".to_string(),
1272 ));
1273 }
1274
1275 let unsigned_bytes = bincode::serialize(&self.to_unsigned()).map_err(|e| {
1276 error::IdentityError::Serialization(format!(
1277 "failed to serialize user announcement for verification: {e}"
1278 ))
1279 })?;
1280 let signature = ant_quic::crypto::raw_public_keys::pqc::MlDsaSignature::from_bytes(
1281 &self.user_signature,
1282 )
1283 .map_err(|e| {
1284 error::IdentityError::CertificateVerification(format!(
1285 "invalid user signature in user announcement: {e:?}"
1286 ))
1287 })?;
1288 ant_quic::crypto::raw_public_keys::pqc::verify_with_ml_dsa(
1289 &user_pub,
1290 &unsigned_bytes,
1291 &signature,
1292 )
1293 .map_err(|e| {
1294 error::IdentityError::CertificateVerification(format!(
1295 "user announcement signature verification failed: {e:?}"
1296 ))
1297 })?;
1298
1299 for cert in &self.agent_certificates {
1300 cert.verify()?;
1301 let cert_user = cert.user_id()?;
1302 if cert_user != self.user_id {
1303 return Err(error::IdentityError::CertificateVerification(
1304 "user announcement certificate user_id mismatch".to_string(),
1305 ));
1306 }
1307 }
1308 Ok(())
1309 }
1310}
1311
1312#[derive(Debug, Clone)]
1314pub struct DiscoveredUser {
1315 pub user_id: identity::UserId,
1317 pub user_public_key: Vec<u8>,
1319 pub agent_certificates: Vec<identity::AgentCertificate>,
1321 pub agent_ids: Vec<identity::AgentId>,
1323 pub announced_at: u64,
1325 pub last_seen: u64,
1327}
1328
1329impl DiscoveredUser {
1330 fn from_announcement(announcement: &UserAnnouncement, last_seen: u64) -> Self {
1331 let agent_ids: Vec<identity::AgentId> = announcement
1332 .agent_certificates
1333 .iter()
1334 .filter_map(|c| c.agent_id().ok())
1335 .collect();
1336 Self {
1337 user_id: announcement.user_id,
1338 user_public_key: announcement.user_public_key.clone(),
1339 agent_certificates: announcement.agent_certificates.clone(),
1340 agent_ids,
1341 announced_at: announcement.announced_at,
1342 last_seen,
1343 }
1344 }
1345}
1346
1347#[derive(Debug, Clone)]
1349pub struct DiscoveredAgent {
1350 pub agent_id: identity::AgentId,
1352 pub machine_id: identity::MachineId,
1354 pub user_id: Option<identity::UserId>,
1356 pub addresses: Vec<std::net::SocketAddr>,
1358 pub announced_at: u64,
1360 pub last_seen: u64,
1362 #[doc(hidden)]
1367 pub machine_public_key: Vec<u8>,
1368 pub nat_type: Option<String>,
1371 pub can_receive_direct: Option<bool>,
1374 pub is_relay: Option<bool>,
1377 pub is_coordinator: Option<bool>,
1380 pub reachable_via: Vec<identity::MachineId>,
1383 pub relay_candidates: Vec<identity::MachineId>,
1385 pub cert_not_after: Option<u64>,
1389 pub agent_certificate: Option<identity::AgentCertificate>,
1397 pub agent_public_key: Vec<u8>,
1408}
1409
1410#[derive(Debug, Clone)]
1412pub struct DiscoveredMachine {
1413 pub machine_id: identity::MachineId,
1415 pub addresses: Vec<std::net::SocketAddr>,
1417 pub announced_at: u64,
1419 pub last_seen: u64,
1421 pub machine_public_key: Vec<u8>,
1423 pub nat_type: Option<String>,
1425 pub can_receive_direct: Option<bool>,
1427 pub is_relay: Option<bool>,
1429 pub is_coordinator: Option<bool>,
1431 pub reachable_via: Vec<identity::MachineId>,
1434 pub relay_candidates: Vec<identity::MachineId>,
1436 pub agent_ids: Vec<identity::AgentId>,
1438 pub user_ids: Vec<identity::UserId>,
1441}
1442
1443fn collect_subject_certs(
1454 cache: &std::collections::HashMap<identity::AgentId, DiscoveredAgent>,
1455) -> std::collections::HashMap<identity::AgentId, identity::AgentCertificate> {
1456 cache
1457 .values()
1458 .filter_map(|a| {
1459 a.agent_certificate
1460 .as_ref()
1461 .map(|c| (a.agent_id, c.clone()))
1462 })
1463 .collect()
1464}
1465
1466impl DiscoveredMachine {
1467 fn from_machine_announcement(
1468 announcement: &MachineAnnouncement,
1469 addresses: Vec<std::net::SocketAddr>,
1470 last_seen: u64,
1471 ) -> Self {
1472 Self {
1473 machine_id: announcement.machine_id,
1474 addresses,
1475 announced_at: announcement.announced_at,
1476 last_seen,
1477 machine_public_key: announcement.machine_public_key.clone(),
1478 nat_type: announcement.nat_type.clone(),
1479 can_receive_direct: announcement.can_receive_direct,
1480 is_relay: announcement.is_relay,
1481 is_coordinator: announcement.is_coordinator,
1482 reachable_via: announcement.reachable_via.clone(),
1483 relay_candidates: announcement.relay_candidates.clone(),
1484 agent_ids: Vec::new(),
1485 user_ids: Vec::new(),
1486 }
1487 }
1488
1489 fn from_discovered_agent(agent: &DiscoveredAgent) -> Self {
1490 Self {
1491 machine_id: agent.machine_id,
1492 addresses: agent.addresses.clone(),
1493 announced_at: agent.announced_at,
1494 last_seen: agent.last_seen,
1495 machine_public_key: agent.machine_public_key.clone(),
1496 nat_type: agent.nat_type.clone(),
1497 can_receive_direct: agent.can_receive_direct,
1498 is_relay: agent.is_relay,
1499 is_coordinator: agent.is_coordinator,
1500 reachable_via: agent.reachable_via.clone(),
1501 relay_candidates: agent.relay_candidates.clone(),
1502 agent_ids: vec![agent.agent_id],
1503 user_ids: agent.user_id.into_iter().collect(),
1504 }
1505 }
1506}
1507
1508#[derive(Debug, Clone, Default, PartialEq, Eq)]
1509struct AnnouncementAssistSnapshot {
1510 nat_type: Option<String>,
1511 can_receive_direct: Option<bool>,
1512 relay_capable: Option<bool>,
1513 coordinator_capable: Option<bool>,
1514 relay_active: Option<bool>,
1515 coordinator_active: Option<bool>,
1516}
1517
1518impl AnnouncementAssistSnapshot {
1519 fn from_node_status(status: &ant_quic::NodeStatus) -> Self {
1520 Self {
1521 nat_type: Some(status.nat_type.to_string()),
1522 can_receive_direct: Some(status.can_receive_direct),
1523 relay_capable: Some(status.relay_service_enabled),
1524 coordinator_capable: Some(status.coordinator_service_enabled),
1525 relay_active: Some(status.is_relaying),
1526 coordinator_active: Some(status.is_coordinating),
1527 }
1528 }
1529}
1530
1531struct IdentityAnnouncementBuildOptions<'a> {
1532 include_user_identity: bool,
1533 human_consent: bool,
1534 addresses: Vec<std::net::SocketAddr>,
1535 assist_snapshot: Option<&'a AnnouncementAssistSnapshot>,
1536 reachable_via: Vec<identity::MachineId>,
1537 relay_candidates: Vec<identity::MachineId>,
1538 allow_local_scope: bool,
1539}
1540
1541fn push_unique<T: Copy + PartialEq>(items: &mut Vec<T>, item: T) {
1542 if !items.contains(&item) {
1543 items.push(item);
1544 }
1545}
1546
1547fn prioritize_discovery_addresses(addresses: &mut [std::net::SocketAddr]) {
1548 addresses.sort_by_key(|addr| is_publicly_advertisable(*addr));
1549}
1550
1551async fn upsert_discovered_agent(
1574 cache: &std::sync::Arc<
1575 tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
1576 >,
1577 mut incoming: DiscoveredAgent,
1578) {
1579 prioritize_discovery_addresses(&mut incoming.addresses);
1580 let mut cache = cache.write().await;
1581 match cache.get_mut(&incoming.agent_id) {
1582 Some(existing) => {
1583 if incoming.announced_at >= existing.announced_at {
1584 existing.announced_at = incoming.announced_at;
1585 existing.addresses = incoming.addresses;
1588 prioritize_discovery_addresses(&mut existing.addresses);
1589 if incoming.machine_id.0 != [0u8; 32] {
1590 existing.machine_id = incoming.machine_id;
1591 }
1592 if incoming.user_id.is_some() || existing.user_id.is_none() {
1593 existing.user_id = incoming.user_id;
1594 }
1595 if !incoming.machine_public_key.is_empty() {
1596 existing.machine_public_key = incoming.machine_public_key;
1597 }
1598 if incoming.nat_type.is_some() {
1599 existing.nat_type = incoming.nat_type;
1600 }
1601 if incoming.can_receive_direct.is_some() {
1602 existing.can_receive_direct = incoming.can_receive_direct;
1603 }
1604 if incoming.is_relay.is_some() {
1605 existing.is_relay = incoming.is_relay;
1606 }
1607 if incoming.is_coordinator.is_some() {
1608 existing.is_coordinator = incoming.is_coordinator;
1609 }
1610 existing.reachable_via = incoming.reachable_via;
1611 existing.relay_candidates = incoming.relay_candidates;
1612 if !incoming.agent_public_key.is_empty() {
1615 existing.agent_public_key = incoming.agent_public_key;
1616 }
1617 }
1618 existing.last_seen = incoming.last_seen;
1619 }
1620 None => {
1621 cache.insert(incoming.agent_id, incoming);
1622 }
1623 }
1624}
1625
1626fn sort_discovered_machine(machine: &mut DiscoveredMachine) {
1627 machine.addresses.sort_by_key(|addr| addr.to_string());
1628 machine.agent_ids.sort_by_key(|id| id.0);
1629 machine.user_ids.sort_by_key(|id| id.0);
1630 machine.reachable_via.sort_by_key(|id| id.0);
1631 machine.relay_candidates.sort_by_key(|id| id.0);
1632}
1633
1634async fn upsert_discovered_machine(
1635 cache: &std::sync::Arc<
1636 tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
1637 >,
1638 mut incoming: DiscoveredMachine,
1639) {
1640 if incoming.machine_id.0 == [0u8; 32] {
1641 return;
1642 }
1643
1644 sort_discovered_machine(&mut incoming);
1645 let mut cache = cache.write().await;
1646 match cache.get_mut(&incoming.machine_id) {
1647 Some(existing) => {
1648 for addr in incoming.addresses {
1649 if !existing.addresses.contains(&addr) {
1650 existing.addresses.push(addr);
1651 }
1652 }
1653 if incoming.announced_at >= existing.announced_at {
1654 existing.announced_at = incoming.announced_at;
1655 if !incoming.machine_public_key.is_empty() {
1656 existing.machine_public_key = incoming.machine_public_key;
1657 }
1658 if incoming.nat_type.is_some() {
1659 existing.nat_type = incoming.nat_type;
1660 }
1661 if incoming.can_receive_direct.is_some() {
1662 existing.can_receive_direct = incoming.can_receive_direct;
1663 }
1664 if incoming.is_relay.is_some() {
1665 existing.is_relay = incoming.is_relay;
1666 }
1667 if incoming.is_coordinator.is_some() {
1668 existing.is_coordinator = incoming.is_coordinator;
1669 }
1670 existing.reachable_via = incoming.reachable_via;
1674 existing.relay_candidates = incoming.relay_candidates;
1675 }
1676 existing.last_seen = existing.last_seen.max(incoming.last_seen);
1677 for agent_id in incoming.agent_ids {
1678 push_unique(&mut existing.agent_ids, agent_id);
1679 }
1680 for user_id in incoming.user_ids {
1681 push_unique(&mut existing.user_ids, user_id);
1682 }
1683 sort_discovered_machine(existing);
1684 }
1685 None => {
1686 cache.insert(incoming.machine_id, incoming);
1687 }
1688 }
1689}
1690
1691async fn upsert_discovered_machine_from_agent(
1692 cache: &std::sync::Arc<
1693 tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
1694 >,
1695 agent: &DiscoveredAgent,
1696) {
1697 if agent.machine_id.0 != [0u8; 32] {
1698 upsert_discovered_machine(cache, DiscoveredMachine::from_discovered_agent(agent)).await;
1699 }
1700}
1701
1702const MAX_MACHINE_ANNOUNCEMENT_DECODE_BYTES: u64 = 64 * 1024;
1703
1704const IDENTITY_ANNOUNCEMENT_V2_MAGIC: &[u8; 4] = b"X0A2";
1710
1711fn serialize_identity_announcement(
1715 announcement: &IdentityAnnouncement,
1716) -> Result<Vec<u8>, Box<bincode::ErrorKind>> {
1717 use bincode::Options;
1718 let body = bincode::DefaultOptions::new()
1719 .with_fixint_encoding()
1720 .serialize(announcement)?;
1721 let mut out = Vec::with_capacity(IDENTITY_ANNOUNCEMENT_V2_MAGIC.len() + body.len());
1722 out.extend_from_slice(IDENTITY_ANNOUNCEMENT_V2_MAGIC);
1723 out.extend_from_slice(&body);
1724 Ok(out)
1725}
1726
1727#[derive(serde::Serialize, serde::Deserialize)]
1733struct IdentityAnnouncementLegacy {
1734 agent_id: identity::AgentId,
1735 machine_id: identity::MachineId,
1736 user_id: Option<identity::UserId>,
1737 agent_certificate: Option<identity::AgentCertificate>,
1738 machine_public_key: Vec<u8>,
1739 machine_signature: Vec<u8>,
1740 addresses: Vec<std::net::SocketAddr>,
1741 announced_at: u64,
1742 nat_type: Option<String>,
1743 can_receive_direct: Option<bool>,
1744 is_relay: Option<bool>,
1745 is_coordinator: Option<bool>,
1746 reachable_via: Vec<identity::MachineId>,
1747 relay_candidates: Vec<identity::MachineId>,
1748}
1749
1750impl IdentityAnnouncementLegacy {
1751 fn into_announcement(self) -> IdentityAnnouncement {
1756 let agent_public_key = self
1757 .agent_certificate
1758 .as_ref()
1759 .map(|c| c.agent_public_key().to_vec())
1760 .unwrap_or_default();
1761 IdentityAnnouncement {
1762 agent_id: self.agent_id,
1763 machine_id: self.machine_id,
1764 user_id: self.user_id,
1765 agent_certificate: self.agent_certificate,
1766 machine_public_key: self.machine_public_key,
1767 machine_signature: self.machine_signature,
1768 addresses: self.addresses,
1769 announced_at: self.announced_at,
1770 nat_type: self.nat_type,
1771 can_receive_direct: self.can_receive_direct,
1772 is_relay: self.is_relay,
1773 is_coordinator: self.is_coordinator,
1774 reachable_via: self.reachable_via,
1775 relay_candidates: self.relay_candidates,
1776 agent_public_key,
1777 }
1778 }
1779}
1780
1781fn deserialize_identity_announcement(
1782 payload: &[u8],
1783) -> std::result::Result<IdentityAnnouncement, Box<bincode::ErrorKind>> {
1784 use bincode::Options;
1785 let opts = || {
1786 bincode::DefaultOptions::new()
1787 .with_fixint_encoding()
1788 .with_limit(crate::network::MAX_MESSAGE_DESERIALIZE_SIZE)
1789 .reject_trailing_bytes()
1790 };
1791 if payload.len() >= IDENTITY_ANNOUNCEMENT_V2_MAGIC.len()
1793 && &payload[..IDENTITY_ANNOUNCEMENT_V2_MAGIC.len()] == IDENTITY_ANNOUNCEMENT_V2_MAGIC
1794 {
1795 return opts().deserialize(&payload[IDENTITY_ANNOUNCEMENT_V2_MAGIC.len()..]);
1796 }
1797 let legacy: IdentityAnnouncementLegacy = opts().deserialize(payload)?;
1800 Ok(legacy.into_announcement())
1801}
1802
1803fn deserialize_user_announcement(
1804 payload: &[u8],
1805) -> std::result::Result<UserAnnouncement, Box<bincode::ErrorKind>> {
1806 use bincode::Options;
1807 bincode::DefaultOptions::new()
1808 .with_fixint_encoding()
1809 .with_limit(crate::network::MAX_MESSAGE_DESERIALIZE_SIZE)
1810 .reject_trailing_bytes()
1811 .deserialize(payload)
1812}
1813
1814fn deserialize_machine_announcement(
1815 payload: &[u8],
1816) -> std::result::Result<MachineAnnouncement, Box<bincode::ErrorKind>> {
1817 use bincode::Options;
1818 bincode::DefaultOptions::new()
1819 .with_fixint_encoding()
1820 .with_limit(MAX_MACHINE_ANNOUNCEMENT_DECODE_BYTES)
1821 .reject_trailing_bytes()
1822 .deserialize(payload)
1823}
1824
1825async fn collect_coordinator_hints(
1835 network: &network::NetworkNode,
1836 machine_cache: &std::sync::Arc<
1837 tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
1838 >,
1839 own_machine_id: identity::MachineId,
1840) -> (Vec<identity::MachineId>, Vec<identity::MachineId>) {
1841 const MAX_COORDINATOR_HINTS: usize = 8;
1844
1845 let connected = network.connected_peers().await;
1846 if connected.is_empty() {
1847 return (Vec::new(), Vec::new());
1848 }
1849
1850 let cache = machine_cache.read().await;
1851 let mut reachable_via: Vec<identity::MachineId> = Vec::new();
1852 let mut relay_candidates: Vec<identity::MachineId> = Vec::new();
1853
1854 for peer_id in connected {
1855 let mid = identity::MachineId(peer_id.0);
1856 if mid == own_machine_id {
1857 continue;
1858 }
1859 let Some(entry) = cache.get(&mid) else {
1860 continue;
1861 };
1862 if entry.is_coordinator == Some(true)
1863 && !reachable_via.contains(&mid)
1864 && reachable_via.len() < MAX_COORDINATOR_HINTS
1865 {
1866 reachable_via.push(mid);
1867 }
1868 if entry.is_relay == Some(true)
1869 && !relay_candidates.contains(&mid)
1870 && relay_candidates.len() < MAX_COORDINATOR_HINTS
1871 {
1872 relay_candidates.push(mid);
1873 }
1874 }
1875
1876 reachable_via.sort_by_key(|id| id.0);
1877 relay_candidates.sort_by_key(|id| id.0);
1878 (reachable_via, relay_candidates)
1879}
1880
1881fn build_machine_announcement_for_identity(
1882 identity: &identity::Identity,
1883 addresses: Vec<std::net::SocketAddr>,
1884 announced_at: u64,
1885 assist_snapshot: Option<&AnnouncementAssistSnapshot>,
1886 reachable_via: Vec<identity::MachineId>,
1887 relay_candidates: Vec<identity::MachineId>,
1888 allow_local_scope: bool,
1889) -> error::Result<MachineAnnouncement> {
1890 let addresses = filter_discovery_announcement_addrs(addresses, allow_local_scope);
1891 let machine_public_key = identity.machine_keypair().public_key().as_bytes().to_vec();
1892 let unsigned = MachineAnnouncementUnsigned {
1893 machine_id: identity.machine_id(),
1894 machine_public_key: machine_public_key.clone(),
1895 addresses,
1896 announced_at,
1897 nat_type: assist_snapshot.and_then(|snapshot| snapshot.nat_type.clone()),
1898 can_receive_direct: assist_snapshot.and_then(|snapshot| snapshot.can_receive_direct),
1899 is_relay: assist_snapshot.and_then(|snapshot| snapshot.relay_capable),
1900 is_coordinator: assist_snapshot.and_then(|snapshot| snapshot.coordinator_capable),
1901 reachable_via,
1902 relay_candidates,
1903 };
1904 let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
1905 error::IdentityError::Serialization(format!(
1906 "failed to serialize unsigned machine announcement: {e}"
1907 ))
1908 })?;
1909 let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
1910 identity.machine_keypair().secret_key(),
1911 &unsigned_bytes,
1912 )
1913 .map_err(|e| {
1914 error::IdentityError::Storage(std::io::Error::other(format!(
1915 "failed to sign machine announcement with machine key: {:?}",
1916 e
1917 )))
1918 })?
1919 .as_bytes()
1920 .to_vec();
1921
1922 Ok(MachineAnnouncement {
1923 machine_id: unsigned.machine_id,
1924 machine_public_key,
1925 machine_signature,
1926 addresses: unsigned.addresses,
1927 announced_at: unsigned.announced_at,
1928 nat_type: unsigned.nat_type,
1929 can_receive_direct: unsigned.can_receive_direct,
1930 is_relay: unsigned.is_relay,
1931 is_coordinator: unsigned.is_coordinator,
1932 reachable_via: unsigned.reachable_via,
1933 relay_candidates: unsigned.relay_candidates,
1934 })
1935}
1936
1937#[derive(Debug)]
1974pub struct AgentBuilder {
1975 machine_key_path: Option<std::path::PathBuf>,
1976 agent_keypair: Option<identity::AgentKeypair>,
1977 agent_key_path: Option<std::path::PathBuf>,
1978 agent_cert_path: Option<std::path::PathBuf>,
1985 user_keypair: Option<identity::UserKeypair>,
1986 user_key_path: Option<std::path::PathBuf>,
1987 #[allow(dead_code)]
1988 network_config: Option<network::NetworkConfig>,
1989 gossip_config: Option<gossip::GossipConfig>,
1990 peer_cache_dir: Option<std::path::PathBuf>,
1991 disable_peer_cache: bool,
1994 heartbeat_interval_secs: Option<u64>,
1995 identity_ttl_secs: Option<u64>,
1996 presence_beacon_interval_secs: Option<u64>,
1997 presence_event_poll_interval_secs: Option<u64>,
1998 presence_offline_timeout_secs: Option<u64>,
1999 contact_store_path: Option<std::path::PathBuf>,
2001 identity_dir: Option<std::path::PathBuf>,
2005}
2006
2007struct HeartbeatContext {
2009 identity: std::sync::Arc<identity::Identity>,
2010 runtime: std::sync::Arc<gossip::GossipRuntime>,
2011 network: std::sync::Arc<network::NetworkNode>,
2012 interval_secs: u64,
2013 cache: std::sync::Arc<
2014 tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
2015 >,
2016 machine_cache: std::sync::Arc<
2017 tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
2018 >,
2019 user_identity_consented: std::sync::Arc<std::sync::atomic::AtomicBool>,
2023 allow_local_discovery_addrs: bool,
2024 revocation_set: std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>>,
2027}
2028
2029impl HeartbeatContext {
2030 async fn announce(&self) -> error::Result<()> {
2031 let machine_public_key = self
2032 .identity
2033 .machine_keypair()
2034 .public_key()
2035 .as_bytes()
2036 .to_vec();
2037 let announced_at = Agent::unix_timestamp_secs();
2038
2039 let mut addresses = match self.network.node_status().await {
2042 Some(status) if !status.external_addrs.is_empty() => status.external_addrs,
2043 _ => match self.network.routable_addr().await {
2044 Some(addr) => vec![addr],
2045 None => Vec::new(),
2046 },
2047 };
2048
2049 let bind_port = self
2058 .network
2059 .bound_addr()
2060 .await
2061 .map(|a| a.port())
2062 .unwrap_or(5483);
2063 if let Ok(sock) = std::net::UdpSocket::bind("[::]:0") {
2064 if sock.connect("[2001:4860:4860::8888]:80").is_ok() {
2065 if let Ok(local) = sock.local_addr() {
2066 if let std::net::IpAddr::V6(v6) = local.ip() {
2067 let segs = v6.segments();
2068 let is_global = (segs[0] & 0xffc0) != 0xfe80
2069 && (segs[0] & 0xff00) != 0xfd00
2070 && !v6.is_loopback();
2071 if is_global {
2072 let v6_addr =
2073 std::net::SocketAddr::new(std::net::IpAddr::V6(v6), bind_port);
2074 if !addresses.contains(&v6_addr) {
2075 addresses.push(v6_addr);
2076 }
2077 }
2078 }
2079 }
2080 }
2081 }
2082
2083 for addr in collect_local_interface_addrs(bind_port) {
2084 if !addresses.contains(&addr) {
2085 addresses.push(addr);
2086 }
2087 }
2088
2089 addresses =
2095 filter_discovery_announcement_addrs(addresses, self.allow_local_discovery_addrs);
2096
2097 let assist_snapshot = self
2101 .network
2102 .node_status()
2103 .await
2104 .map(|status| AnnouncementAssistSnapshot::from_node_status(&status))
2105 .unwrap_or_default();
2106 let nat_type = assist_snapshot.nat_type.clone();
2107 let can_receive_direct = assist_snapshot.can_receive_direct;
2108 let relay_capable = assist_snapshot.relay_capable;
2109 let coordinator_capable = assist_snapshot.coordinator_capable;
2110
2111 let (reachable_via, relay_candidates) = if can_receive_direct == Some(true) {
2115 (Vec::new(), Vec::new())
2116 } else {
2117 collect_coordinator_hints(
2118 self.network.as_ref(),
2119 &self.machine_cache,
2120 self.identity.machine_id(),
2121 )
2122 .await
2123 };
2124
2125 let include_user = self
2129 .user_identity_consented
2130 .load(std::sync::atomic::Ordering::Acquire);
2131 let (user_id, agent_certificate) = if include_user {
2132 (
2133 self.identity
2134 .user_keypair()
2135 .map(identity::UserKeypair::user_id),
2136 self.identity.agent_certificate().cloned(),
2137 )
2138 } else {
2139 (None, None)
2140 };
2141
2142 let unsigned = IdentityAnnouncementUnsigned {
2143 agent_id: self.identity.agent_id(),
2144 machine_id: self.identity.machine_id(),
2145 user_id,
2146 agent_certificate,
2147 machine_public_key: machine_public_key.clone(),
2148 addresses,
2149 announced_at,
2150 nat_type: nat_type.clone(),
2151 can_receive_direct,
2152 is_relay: relay_capable,
2153 is_coordinator: coordinator_capable,
2154 reachable_via: reachable_via.clone(),
2155 relay_candidates: relay_candidates.clone(),
2156 };
2157 let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
2158 error::IdentityError::Serialization(format!(
2159 "heartbeat: failed to serialize announcement: {e}"
2160 ))
2161 })?;
2162 let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
2163 self.identity.machine_keypair().secret_key(),
2164 &unsigned_bytes,
2165 )
2166 .map_err(|e| {
2167 error::IdentityError::Storage(std::io::Error::other(format!(
2168 "heartbeat: failed to sign announcement: {:?}",
2169 e
2170 )))
2171 })?
2172 .as_bytes()
2173 .to_vec();
2174
2175 let agent_public_key = self
2176 .identity
2177 .agent_keypair()
2178 .public_key()
2179 .as_bytes()
2180 .to_vec();
2181 let announcement = IdentityAnnouncement {
2182 agent_id: unsigned.agent_id,
2183 machine_id: unsigned.machine_id,
2184 user_id: unsigned.user_id,
2185 agent_certificate: unsigned.agent_certificate,
2186 machine_public_key: machine_public_key.clone(),
2187 machine_signature,
2188 addresses: unsigned.addresses,
2189 announced_at,
2190 nat_type,
2191 can_receive_direct,
2192 is_relay: relay_capable,
2193 is_coordinator: coordinator_capable,
2194 reachable_via: reachable_via.clone(),
2195 relay_candidates: relay_candidates.clone(),
2196 agent_public_key,
2197 };
2198 tracing::debug!(
2199 target: "x0x::discovery",
2200 announcement_kind = "heartbeat",
2201 machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
2202 addr_total = announcement.addresses.len(),
2203 nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
2204 can_receive_direct = ?announcement.can_receive_direct,
2205 relay_capable = ?announcement.is_relay,
2206 coordinator_capable = ?announcement.is_coordinator,
2207 relay_active = ?assist_snapshot.relay_active,
2208 coordinator_active = ?assist_snapshot.coordinator_active,
2209 reachable_via_count = announcement.reachable_via.len(),
2210 relay_candidate_count = announcement.relay_candidates.len(),
2211 "publishing identity announcement"
2212 );
2213
2214 let machine_announcement = build_machine_announcement_for_identity(
2215 &self.identity,
2216 announcement.addresses.clone(),
2217 announced_at,
2218 Some(&assist_snapshot),
2219 reachable_via.clone(),
2220 relay_candidates.clone(),
2221 self.allow_local_discovery_addrs,
2222 )?;
2223 tracing::debug!(
2224 target: "x0x::discovery",
2225 announcement_kind = "machine_heartbeat",
2226 machine_prefix = %network::hex_prefix(&machine_announcement.machine_id.0, 4),
2227 addr_total = machine_announcement.addresses.len(),
2228 nat_type = machine_announcement.nat_type.as_deref().unwrap_or("unknown"),
2229 can_receive_direct = ?machine_announcement.can_receive_direct,
2230 relay_capable = ?machine_announcement.is_relay,
2231 coordinator_capable = ?machine_announcement.is_coordinator,
2232 "publishing machine announcement"
2233 );
2234 let machine_encoded = bincode::serialize(&machine_announcement).map_err(|e| {
2235 error::IdentityError::Serialization(format!(
2236 "heartbeat: failed to serialize machine announcement: {e}"
2237 ))
2238 })?;
2239 let machine_payload = bytes::Bytes::from(machine_encoded);
2240 self.runtime
2241 .pubsub()
2242 .publish(
2243 shard_topic_for_machine(&machine_announcement.machine_id),
2244 machine_payload.clone(),
2245 )
2246 .await
2247 .map_err(|e| {
2248 error::IdentityError::Storage(std::io::Error::other(format!(
2249 "heartbeat: machine shard publish failed: {e}"
2250 )))
2251 })?;
2252 self.runtime
2253 .pubsub()
2254 .publish(MACHINE_ANNOUNCE_TOPIC.to_string(), machine_payload)
2255 .await
2256 .map_err(|e| {
2257 error::IdentityError::Storage(std::io::Error::other(format!(
2258 "heartbeat: machine publish failed: {e}"
2259 )))
2260 })?;
2261
2262 let encoded = serialize_identity_announcement(&announcement).map_err(|e| {
2263 error::IdentityError::Serialization(format!(
2264 "heartbeat: failed to serialize announcement: {e}"
2265 ))
2266 })?;
2267 self.runtime
2268 .pubsub()
2269 .publish(
2270 IDENTITY_ANNOUNCE_TOPIC.to_string(),
2271 bytes::Bytes::from(encoded),
2272 )
2273 .await
2274 .map_err(|e| {
2275 error::IdentityError::Storage(std::io::Error::other(format!(
2276 "heartbeat: publish failed: {e}"
2277 )))
2278 })?;
2279 let now = Agent::unix_timestamp_secs();
2280 upsert_discovered_machine(
2281 &self.machine_cache,
2282 DiscoveredMachine::from_machine_announcement(
2283 &machine_announcement,
2284 machine_announcement.addresses.clone(),
2285 now,
2286 ),
2287 )
2288 .await;
2289 let discovered_agent = DiscoveredAgent {
2290 agent_id: announcement.agent_id,
2291 machine_id: announcement.machine_id,
2292 user_id: announcement.user_id,
2293 addresses: announcement.addresses,
2294 announced_at: announcement.announced_at,
2295 last_seen: now,
2296 machine_public_key: machine_public_key.clone(),
2297 nat_type: announcement.nat_type.clone(),
2298 can_receive_direct: announcement.can_receive_direct,
2299 is_relay: announcement.is_relay,
2300 is_coordinator: announcement.is_coordinator,
2301 reachable_via: announcement.reachable_via.clone(),
2302 relay_candidates: announcement.relay_candidates.clone(),
2303 cert_not_after: None,
2304 agent_certificate: None,
2305 agent_public_key: announcement.agent_public_key.clone(),
2306 };
2307 upsert_discovered_machine_from_agent(&self.machine_cache, &discovered_agent).await;
2308 upsert_discovered_agent(&self.cache, discovered_agent).await;
2309
2310 let records = self.revocation_set.read().await.all_records();
2315 if !records.is_empty() {
2316 match bincode::serialize(&records) {
2317 Ok(bytes) => {
2318 if let Err(e) = self
2319 .runtime
2320 .pubsub()
2321 .publish(REVOCATION_TOPIC.to_string(), bytes::Bytes::from(bytes))
2322 .await
2323 {
2324 tracing::debug!("heartbeat: revocation re-broadcast failed: {e}");
2325 }
2326 }
2327 Err(e) => {
2328 tracing::debug!("heartbeat: failed to serialize revocation set: {e}");
2329 }
2330 }
2331 }
2332
2333 Ok(())
2334 }
2335}
2336
2337impl Agent {
2338 pub async fn new() -> error::Result<Self> {
2347 Agent::builder().build().await
2348 }
2349
2350 pub fn builder() -> AgentBuilder {
2358 AgentBuilder {
2359 machine_key_path: None,
2360 agent_keypair: None,
2361 agent_key_path: None,
2362 agent_cert_path: None,
2363 user_keypair: None,
2364 user_key_path: None,
2365 network_config: None,
2366 gossip_config: None,
2367 peer_cache_dir: None,
2368 disable_peer_cache: false,
2369 heartbeat_interval_secs: None,
2370 identity_ttl_secs: None,
2371 presence_beacon_interval_secs: None,
2372 presence_event_poll_interval_secs: None,
2373 presence_offline_timeout_secs: None,
2374 contact_store_path: None,
2375 identity_dir: None,
2376 }
2377 }
2378
2379 #[inline]
2385 #[must_use]
2386 pub fn identity(&self) -> &identity::Identity {
2387 &self.identity
2388 }
2389
2390 #[inline]
2399 #[must_use]
2400 pub fn machine_id(&self) -> identity::MachineId {
2401 self.identity.machine_id()
2402 }
2403
2404 #[inline]
2414 #[must_use]
2415 pub fn agent_id(&self) -> identity::AgentId {
2416 self.identity.agent_id()
2417 }
2418
2419 #[inline]
2424 #[must_use]
2425 pub fn user_id(&self) -> Option<identity::UserId> {
2426 self.identity.user_id()
2427 }
2428
2429 #[inline]
2433 #[must_use]
2434 pub fn agent_certificate(&self) -> Option<&identity::AgentCertificate> {
2435 self.identity.agent_certificate()
2436 }
2437
2438 #[must_use]
2440 pub fn network(&self) -> Option<&std::sync::Arc<network::NetworkNode>> {
2441 self.network.as_ref()
2442 }
2443
2444 pub fn gossip_cache_adapter(&self) -> Option<&saorsa_gossip_coordinator::GossipCacheAdapter> {
2449 self.gossip_cache_adapter.as_ref()
2450 }
2451
2452 #[must_use]
2459 pub fn gossip_stats(&self) -> Option<gossip::PubSubStatsSnapshot> {
2460 self.gossip_runtime.as_ref().map(|rt| rt.pubsub().stats())
2461 }
2462
2463 #[must_use]
2469 pub fn gossip_dispatch_stats(&self) -> Option<gossip::GossipDispatchStatsSnapshot> {
2470 self.gossip_runtime.as_ref().map(|rt| rt.dispatch_stats())
2471 }
2472
2473 #[must_use]
2479 pub fn gossip_pubsub_stage_stats(&self) -> Option<gossip::PubSubStageStatsSnapshot> {
2480 self.gossip_runtime
2481 .as_ref()
2482 .map(|rt| rt.pubsub().stage_stats())
2483 }
2484
2485 #[must_use]
2491 pub fn recv_pump_diagnostics(&self) -> Option<network::RecvPumpDiagnosticsSnapshot> {
2492 self.network.as_ref().map(|net| net.recv_pump_diagnostics())
2493 }
2494
2495 #[must_use]
2501 pub fn presence_system(&self) -> Option<&std::sync::Arc<presence::PresenceWrapper>> {
2502 self.presence.as_ref()
2503 }
2504
2505 #[must_use]
2513 pub fn contacts(&self) -> &std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>> {
2514 &self.contact_store
2515 }
2516
2517 pub async fn reachability(
2523 &self,
2524 agent_id: &identity::AgentId,
2525 ) -> Option<connectivity::ReachabilityInfo> {
2526 let cache = self.identity_discovery_cache.read().await;
2527 cache
2528 .get(agent_id)
2529 .map(connectivity::ReachabilityInfo::from_discovered)
2530 }
2531
2532 async fn seed_transport_peer_hints_for_target(
2533 &self,
2534 network: &network::NetworkNode,
2535 target: &DiscoveredAgent,
2536 ) -> error::Result<()> {
2537 #[derive(Default)]
2538 struct HelperHintEntry {
2539 addrs: Vec<std::net::SocketAddr>,
2540 caps: ant_quic::bootstrap_cache::PeerCapabilities,
2541 sources: std::collections::BTreeSet<&'static str>,
2542 }
2543
2544 fn merge_helper_hint(
2545 hints: &mut std::collections::HashMap<ant_quic::PeerId, HelperHintEntry>,
2546 peer_id: ant_quic::PeerId,
2547 source: &'static str,
2548 addrs: impl IntoIterator<Item = std::net::SocketAddr>,
2549 supports_coordination: bool,
2550 supports_relay: bool,
2551 ) {
2552 let entry = hints.entry(peer_id).or_default();
2553 entry.sources.insert(source);
2554 for addr in addrs {
2555 if !entry.addrs.contains(&addr) {
2556 entry.addrs.push(addr);
2557 }
2558 }
2559 if supports_coordination {
2560 entry.caps.supports_coordination = true;
2561 }
2562 if supports_relay {
2563 entry.caps.supports_relay = true;
2564 }
2565 }
2566
2567 let target_agent_prefix = network::hex_prefix(&target.agent_id.0, 4);
2568 let target_machine_prefix = network::hex_prefix(&target.machine_id.0, 4);
2569 let target_peer_id = ant_quic::PeerId(target.machine_id.0);
2570 if target.machine_id.0 != [0u8; 32] {
2571 network
2572 .upsert_peer_hints(target_peer_id, target.addresses.clone(), None)
2573 .await
2574 .map_err(|e| {
2575 error::IdentityError::Storage(std::io::Error::other(format!(
2576 "failed to upsert target peer hints: {e}"
2577 )))
2578 })?;
2579 tracing::debug!(
2580 target: "x0x::connect",
2581 stage = "seed_target_hints",
2582 %target_agent_prefix,
2583 %target_machine_prefix,
2584 target_addr_count = target.addresses.len(),
2585 "upserted direct target hints"
2586 );
2587 }
2588
2589 let mut helper_hints: std::collections::HashMap<ant_quic::PeerId, HelperHintEntry> =
2590 std::collections::HashMap::new();
2591
2592 if let Some(ref cache) = self.bootstrap_cache {
2593 for peer in cache.select_coordinators(6).await {
2594 merge_helper_hint(
2595 &mut helper_hints,
2596 peer.peer_id,
2597 "bootstrap_cache:coordinator",
2598 peer.preferred_addresses(),
2599 true,
2600 false,
2601 );
2602 }
2603 for peer in cache.select_relay_peers(6).await {
2604 merge_helper_hint(
2605 &mut helper_hints,
2606 peer.peer_id,
2607 "bootstrap_cache:relay",
2608 peer.preferred_addresses(),
2609 false,
2610 true,
2611 );
2612 }
2613 }
2614
2615 if let Some(ref adapter) = self.gossip_cache_adapter {
2616 let mut adverts = adapter.get_all_adverts();
2617 adverts.sort_by_key(|a| std::cmp::Reverse(a.score));
2618 for advert in adverts.into_iter().take(12) {
2619 let advert_peer_id = ant_quic::PeerId(*advert.peer.as_bytes());
2620 if advert_peer_id == target_peer_id {
2621 continue;
2622 }
2623 merge_helper_hint(
2624 &mut helper_hints,
2625 advert_peer_id,
2626 "gossip_cache_advert",
2627 advert
2628 .addr_hints
2629 .into_iter()
2630 .map(|hint| hint.addr)
2631 .filter(|addr| is_publicly_advertisable(*addr)),
2632 advert.roles.coordinator || advert.roles.rendezvous,
2633 advert.roles.relay,
2634 );
2635 }
2636 }
2637
2638 let discovered: Vec<DiscoveredMachine> = {
2639 let cache = self.machine_discovery_cache.read().await;
2640 cache.values().cloned().collect()
2641 };
2642 for candidate in discovered {
2643 if candidate.machine_id == target.machine_id || candidate.machine_id.0 == [0u8; 32] {
2644 continue;
2645 }
2646 merge_helper_hint(
2647 &mut helper_hints,
2648 ant_quic::PeerId(candidate.machine_id.0),
2649 "machine_discovery_cache",
2650 candidate.addresses.iter().copied(),
2651 candidate.is_coordinator == Some(true),
2652 candidate.is_relay == Some(true),
2653 );
2654 }
2655
2656 let helper_candidate_count = helper_hints.len();
2657 let helper_addr_total: usize = helper_hints.values().map(|entry| entry.addrs.len()).sum();
2658 tracing::info!(
2659 target: "x0x::connect",
2660 stage = "seed_target_hints",
2661 %target_agent_prefix,
2662 %target_machine_prefix,
2663 target_addr_count = target.addresses.len(),
2664 helper_candidate_count,
2665 helper_addr_total,
2666 "prepared helper hints for peer-authenticated dial"
2667 );
2668
2669 for (peer_id, entry) in &helper_hints {
2670 tracing::debug!(
2671 target: "x0x::connect",
2672 stage = "seed_target_hints",
2673 helper_peer_prefix = %network::hex_prefix(&peer_id.0, 4),
2674 helper_addr_count = entry.addrs.len(),
2675 supports_coordination = entry.caps.supports_coordination,
2676 supports_relay = entry.caps.supports_relay,
2677 sources = %entry.sources.iter().copied().collect::<Vec<_>>().join(","),
2678 "helper candidate discovered"
2679 );
2680 }
2681
2682 for (peer_id, entry) in helper_hints {
2683 let HelperHintEntry {
2684 mut addrs,
2685 caps,
2686 sources,
2687 } = entry;
2688 addrs.retain(|addr| !target.addresses.contains(addr));
2689 if addrs.is_empty() && !caps.supports_coordination && !caps.supports_relay {
2690 tracing::debug!(
2691 target: "x0x::connect",
2692 stage = "seed_target_hints",
2693 helper_peer_prefix = %network::hex_prefix(&peer_id.0, 4),
2694 sources = %sources.iter().copied().collect::<Vec<_>>().join(","),
2695 "skipping helper with no remaining addresses or assist capability"
2696 );
2697 continue;
2698 }
2699 tracing::debug!(
2700 target: "x0x::connect",
2701 stage = "seed_target_hints",
2702 helper_peer_prefix = %network::hex_prefix(&peer_id.0, 4),
2703 helper_addr_count = addrs.len(),
2704 supports_coordination = caps.supports_coordination,
2705 supports_relay = caps.supports_relay,
2706 sources = %sources.iter().copied().collect::<Vec<_>>().join(","),
2707 "upserting helper peer hints"
2708 );
2709 network
2710 .upsert_peer_hints(peer_id, addrs, Some(caps))
2711 .await
2712 .map_err(|e| {
2713 error::IdentityError::Storage(std::io::Error::other(format!(
2714 "failed to upsert helper peer hints: {e}"
2715 )))
2716 })?;
2717 }
2718
2719 Ok(())
2720 }
2721
2722 pub async fn connect_to_agent(
2740 &self,
2741 agent_id: &identity::AgentId,
2742 ) -> error::Result<connectivity::ConnectOutcome> {
2743 let call_start = std::time::Instant::now();
2744 let agent_prefix = network::hex_prefix(&agent_id.0, 4);
2745 tracing::debug!(
2746 target: "x0x::connect",
2747 stage = "connect_to_agent",
2748 %agent_prefix,
2749 "begin"
2750 );
2751 let discovered = {
2753 let cache = self.identity_discovery_cache.read().await;
2754 cache.get(agent_id).cloned()
2755 };
2756
2757 let agent = match discovered {
2758 Some(a) => a,
2759 None => {
2760 tracing::info!(
2761 target: "x0x::connect",
2762 stage = "connect_to_agent",
2763 %agent_prefix,
2764 outcome = "not_found",
2765 dur_ms = call_start.elapsed().as_millis() as u64,
2766 "agent not in discovery cache"
2767 );
2768 return Ok(connectivity::ConnectOutcome::NotFound);
2769 }
2770 };
2771
2772 {
2777 let revoked = self.revocation_set.read().await;
2778 if revoked.is_agent_revoked(agent_id) || revoked.is_machine_revoked(&agent.machine_id) {
2779 tracing::info!(
2780 target: "x0x::connect",
2781 stage = "connect_to_agent",
2782 %agent_prefix,
2783 outcome = "revoked",
2784 dur_ms = call_start.elapsed().as_millis() as u64,
2785 "connect target is revoked — refusing before any dial"
2786 );
2787 return Ok(connectivity::ConnectOutcome::NotFound);
2788 }
2789 }
2790
2791 let info = connectivity::ReachabilityInfo::from_discovered(&agent);
2792 let v4_addrs = info.addresses.iter().filter(|a| a.is_ipv4()).count();
2793 let v6_addrs = info.addresses.len() - v4_addrs;
2794 tracing::info!(
2795 target: "x0x::connect",
2796 stage = "connect_to_agent",
2797 %agent_prefix,
2798 machine_prefix = %network::hex_prefix(&agent.machine_id.0, 4),
2799 addr_total = info.addresses.len(),
2800 v4_addrs,
2801 v6_addrs,
2802 can_receive_direct = ?info.can_receive_direct,
2803 should_attempt_direct = info.should_attempt_direct(),
2804 needs_coordination = info.needs_coordination(),
2805 "reachability classified"
2806 );
2807
2808 let Some(ref network) = self.network else {
2809 tracing::warn!(
2810 target: "x0x::connect",
2811 stage = "connect_to_agent",
2812 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
2813 outcome = "unreachable_no_network",
2814 "network layer not initialised"
2815 );
2816 return Ok(connectivity::ConnectOutcome::Unreachable);
2817 };
2818
2819 let connected_machine_id = if agent.machine_id.0 != [0u8; 32]
2825 && network
2826 .is_connected(&ant_quic::PeerId(agent.machine_id.0))
2827 .await
2828 {
2829 Some(agent.machine_id)
2830 } else {
2831 match self.direct_messaging.get_machine_id(agent_id).await {
2832 Some(machine_id) if network.is_connected(&ant_quic::PeerId(machine_id.0)).await => {
2833 Some(machine_id)
2834 }
2835 _ => None,
2836 }
2837 };
2838 if let Some(machine_id) = connected_machine_id {
2839 if machine_id != agent.machine_id {
2840 let mut cache = self.identity_discovery_cache.write().await;
2841 if let Some(entry) = cache.get_mut(agent_id) {
2842 entry.machine_id = machine_id;
2843 }
2844 }
2845 self.direct_messaging
2846 .mark_connected(agent.agent_id, machine_id)
2847 .await;
2848 let dur_ms = call_start.elapsed().as_millis() as u64;
2849 return if let Some(addr) = info.addresses.first() {
2850 let family = if addr.is_ipv4() { "v4" } else { "v6" };
2851 tracing::info!(
2852 target: "x0x::connect",
2853 stage = "connect_to_agent",
2854 %agent_prefix,
2855 strategy = "already_connected",
2856 outcome = "direct",
2857 selected_addr = %addr,
2858 family,
2859 dur_ms,
2860 "reusing existing connection"
2861 );
2862 Ok(connectivity::ConnectOutcome::Direct(*addr))
2863 } else {
2864 tracing::info!(
2865 target: "x0x::connect",
2866 stage = "connect_to_agent",
2867 %agent_prefix,
2868 strategy = "already_connected",
2869 outcome = "already_connected",
2870 dur_ms,
2871 "reusing existing connection without known addr"
2872 );
2873 Ok(connectivity::ConnectOutcome::AlreadyConnected)
2874 };
2875 }
2876
2877 if info.addresses.is_empty() {
2878 tracing::info!(
2879 target: "x0x::connect",
2880 stage = "connect_to_agent",
2881 %agent_prefix,
2882 outcome = "unreachable",
2883 reason = "no_addresses",
2884 dur_ms = call_start.elapsed().as_millis() as u64,
2885 "no known addresses for agent"
2886 );
2887 return Ok(connectivity::ConnectOutcome::Unreachable);
2888 }
2889
2890 let dial_timeout = std::time::Duration::from_secs(8);
2891 let local_probe_timeout = std::time::Duration::from_secs(3);
2892 let direct_probe_addrs = local_direct_probe_addrs(&info.addresses);
2893
2894 let peer_id_hint =
2900 (agent.machine_id.0 != [0u8; 32]).then_some(ant_quic::PeerId(agent.machine_id.0));
2901 for addr in &direct_probe_addrs {
2902 if let Some(peer_id_hint) = peer_id_hint {
2903 match tokio::time::timeout(
2904 local_probe_timeout,
2905 network.connect_peer_with_addrs(peer_id_hint, vec![*addr]),
2906 )
2907 .await
2908 {
2909 Ok(Ok((selected_addr, connected_peer_id)))
2910 if connected_peer_id == peer_id_hint =>
2911 {
2912 let real_machine_id = identity::MachineId(connected_peer_id.0);
2913 if let Some(ref bc) = self.bootstrap_cache {
2914 bc.add_from_connection(connected_peer_id, vec![selected_addr], None)
2915 .await;
2916 }
2917 {
2918 let mut cache = self.identity_discovery_cache.write().await;
2919 if let Some(entry) = cache.get_mut(agent_id) {
2920 entry.machine_id = real_machine_id;
2921 }
2922 }
2923 self.direct_messaging
2924 .mark_connected(agent.agent_id, real_machine_id)
2925 .await;
2926 tracing::info!(
2927 target: "x0x::connect",
2928 stage = "connect_to_agent",
2929 %agent_prefix,
2930 strategy = "local_direct_first",
2931 outcome = "direct",
2932 selected_addr = %selected_addr,
2933 family = "v4",
2934 dur_ms = call_start.elapsed().as_millis() as u64,
2935 "local peer-authenticated dial succeeded"
2936 );
2937 return Ok(connectivity::ConnectOutcome::Direct(selected_addr));
2938 }
2939 Ok(Ok((selected_addr, connected_peer_id))) => {
2940 tracing::warn!(
2941 target: "x0x::connect",
2942 stage = "connect_to_agent",
2943 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
2944 strategy = "local_direct_first",
2945 requested_addr = %crate::logging::LogHexId::addr(&addr.to_string()),
2946 selected_addr = %crate::logging::LogHexId::addr(&selected_addr.to_string()),
2947 connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
2948 "local peer-authenticated dial reached unexpected peer"
2949 );
2950 }
2951 Ok(Err(e)) => {
2952 tracing::debug!(
2953 target: "x0x::connect",
2954 %agent_prefix,
2955 strategy = "local_direct_first",
2956 %addr,
2957 error = %e,
2958 "local peer-authenticated dial failed; trying verified raw-address fallback"
2959 );
2960 }
2961 Err(_) => {
2962 tracing::debug!(
2963 target: "x0x::connect",
2964 %agent_prefix,
2965 strategy = "local_direct_first",
2966 %addr,
2967 timeout_s = local_probe_timeout.as_secs(),
2968 "local peer-authenticated dial timed out; trying verified raw-address fallback"
2969 );
2970 }
2971 }
2972
2973 match tokio::time::timeout(local_probe_timeout, network.connect_addr(*addr)).await {
2974 Ok(Ok(connected_peer_id)) if connected_peer_id == peer_id_hint => {
2975 let real_machine_id = identity::MachineId(connected_peer_id.0);
2976 if let Some(ref bc) = self.bootstrap_cache {
2977 bc.add_from_connection(connected_peer_id, vec![*addr], None)
2978 .await;
2979 }
2980 {
2981 let mut cache = self.identity_discovery_cache.write().await;
2982 if let Some(entry) = cache.get_mut(agent_id) {
2983 entry.machine_id = real_machine_id;
2984 }
2985 }
2986 self.direct_messaging
2987 .mark_connected(agent.agent_id, real_machine_id)
2988 .await;
2989 tracing::info!(
2990 target: "x0x::connect",
2991 stage = "connect_to_agent",
2992 %agent_prefix,
2993 strategy = "local_direct_raw_fallback",
2994 outcome = "direct",
2995 selected_addr = %addr,
2996 family = "v4",
2997 dur_ms = call_start.elapsed().as_millis() as u64,
2998 "verified local raw-address fallback succeeded"
2999 );
3000 return Ok(connectivity::ConnectOutcome::Direct(*addr));
3001 }
3002 Ok(Ok(connected_peer_id)) => {
3003 tracing::warn!(
3004 target: "x0x::connect",
3005 stage = "connect_to_agent",
3006 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
3007 strategy = "local_direct_raw_fallback",
3008 addr = %crate::logging::LogHexId::addr(&addr.to_string()),
3009 connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
3010 "verified local raw-address fallback reached unexpected peer"
3011 );
3012 }
3013 Ok(Err(e)) => {
3014 tracing::debug!(
3015 target: "x0x::connect",
3016 %agent_prefix,
3017 strategy = "local_direct_raw_fallback",
3018 %addr,
3019 error = %e,
3020 "verified local raw-address fallback failed"
3021 );
3022 }
3023 Err(_) => {
3024 tracing::debug!(
3025 target: "x0x::connect",
3026 %agent_prefix,
3027 strategy = "local_direct_raw_fallback",
3028 %addr,
3029 timeout_s = local_probe_timeout.as_secs(),
3030 "verified local raw-address fallback timed out"
3031 );
3032 }
3033 }
3034 } else {
3035 match tokio::time::timeout(local_probe_timeout, network.connect_addr(*addr)).await {
3036 Ok(Ok(connected_peer_id)) => {
3037 let real_machine_id = identity::MachineId(connected_peer_id.0);
3038 if let Some(ref bc) = self.bootstrap_cache {
3039 bc.add_from_connection(connected_peer_id, vec![*addr], None)
3040 .await;
3041 }
3042 {
3043 let mut cache = self.identity_discovery_cache.write().await;
3044 if let Some(entry) = cache.get_mut(agent_id) {
3045 entry.machine_id = real_machine_id;
3046 }
3047 }
3048 self.direct_messaging
3049 .mark_connected(agent.agent_id, real_machine_id)
3050 .await;
3051 tracing::info!(
3052 target: "x0x::connect",
3053 stage = "connect_to_agent",
3054 %agent_prefix,
3055 strategy = "local_direct_first",
3056 outcome = "direct",
3057 selected_addr = %addr,
3058 family = "v4",
3059 dur_ms = call_start.elapsed().as_millis() as u64,
3060 "local direct dial succeeded"
3061 );
3062 return Ok(connectivity::ConnectOutcome::Direct(*addr));
3063 }
3064 Ok(Err(e)) => {
3065 tracing::debug!(
3066 target: "x0x::connect",
3067 %agent_prefix,
3068 strategy = "local_direct_first",
3069 %addr,
3070 error = %e,
3071 "local direct dial failed"
3072 );
3073 }
3074 Err(_) => {
3075 tracing::debug!(
3076 target: "x0x::connect",
3077 %agent_prefix,
3078 strategy = "local_direct_first",
3079 %addr,
3080 timeout_s = local_probe_timeout.as_secs(),
3081 "local direct dial timed out"
3082 );
3083 }
3084 }
3085 }
3086 }
3087
3088 if agent.machine_id.0 != [0u8; 32] {
3092 let peer_id_hint = ant_quic::PeerId(agent.machine_id.0);
3093 self.seed_transport_peer_hints_for_target(network, &agent)
3094 .await
3095 .map_err(|e| {
3096 error::IdentityError::Storage(std::io::Error::other(format!(
3097 "failed to seed transport peer hints: {e}"
3098 )))
3099 })?;
3100
3101 match tokio::time::timeout(
3102 dial_timeout,
3103 network.connect_peer_with_addrs(peer_id_hint, info.addresses.clone()),
3104 )
3105 .await
3106 {
3107 Ok(Ok((addr, verified_peer_id))) => {
3108 let verified_machine_id = identity::MachineId(verified_peer_id.0);
3109 if let Some(ref bc) = self.bootstrap_cache {
3110 bc.add_from_connection(verified_peer_id, vec![addr], None)
3111 .await;
3112 bc.record_success(&verified_peer_id, 0).await;
3113 }
3114 {
3115 let mut cache = self.identity_discovery_cache.write().await;
3116 if let Some(entry) = cache.get_mut(agent_id) {
3117 entry.machine_id = verified_machine_id;
3118 }
3119 }
3120 self.direct_messaging
3121 .mark_connected(agent.agent_id, verified_machine_id)
3122 .await;
3123 let family = if addr.is_ipv4() { "v4" } else { "v6" };
3124 tracing::info!(
3125 target: "x0x::connect",
3126 stage = "connect_to_agent",
3127 %agent_prefix,
3128 strategy = "hinted_peer",
3129 outcome = "coordinated",
3130 selected_addr = %addr,
3131 family,
3132 dur_ms = call_start.elapsed().as_millis() as u64,
3133 "hinted peer dial succeeded"
3134 );
3135 return Ok(connectivity::ConnectOutcome::Coordinated(addr));
3136 }
3137 Ok(Err(e)) => {
3138 tracing::debug!(
3139 target: "x0x::connect",
3140 %agent_prefix,
3141 strategy = "hinted_peer",
3142 error = %e,
3143 "hinted peer dial failed"
3144 );
3145 }
3146 Err(_) => {
3147 tracing::debug!(
3148 target: "x0x::connect",
3149 %agent_prefix,
3150 strategy = "hinted_peer",
3151 timeout_s = dial_timeout.as_secs(),
3152 "hinted peer dial timed out"
3153 );
3154 }
3155 }
3156 }
3157
3158 if info.should_attempt_direct() {
3162 for addr in &info.addresses {
3163 if direct_probe_addrs.contains(addr) {
3164 continue;
3165 }
3166 match tokio::time::timeout(dial_timeout, network.connect_addr(*addr)).await {
3167 Ok(Ok(connected_peer_id)) => {
3168 let real_machine_id = identity::MachineId(connected_peer_id.0);
3171 if let Some(ref bc) = self.bootstrap_cache {
3173 bc.add_from_connection(connected_peer_id, vec![*addr], None)
3174 .await;
3175 }
3176 {
3178 let mut cache = self.identity_discovery_cache.write().await;
3179 if let Some(entry) = cache.get_mut(agent_id) {
3180 entry.machine_id = real_machine_id;
3181 }
3182 }
3183 self.direct_messaging
3185 .mark_connected(agent.agent_id, real_machine_id)
3186 .await;
3187 let family = if addr.is_ipv4() { "v4" } else { "v6" };
3188 tracing::info!(
3189 target: "x0x::connect",
3190 stage = "connect_to_agent",
3191 %agent_prefix,
3192 strategy = "direct_per_addr",
3193 outcome = "direct",
3194 selected_addr = %addr,
3195 family,
3196 dur_ms = call_start.elapsed().as_millis() as u64,
3197 "direct dial succeeded"
3198 );
3199 return Ok(connectivity::ConnectOutcome::Direct(*addr));
3200 }
3201 Ok(Err(e)) => {
3202 tracing::debug!(
3203 target: "x0x::connect",
3204 %agent_prefix,
3205 strategy = "direct_per_addr",
3206 %addr,
3207 error = %e,
3208 "direct dial failed"
3209 );
3210 }
3211 Err(_) => {
3212 tracing::debug!(
3213 target: "x0x::connect",
3214 %agent_prefix,
3215 strategy = "direct_per_addr",
3216 %addr,
3217 timeout_s = dial_timeout.as_secs(),
3218 "direct dial timed out"
3219 );
3220 }
3221 }
3222 }
3223 }
3224
3225 if info.needs_coordination() || !info.should_attempt_direct() {
3230 for coord in &info.reachable_via {
3235 if let Some(coord_machine) = self
3236 .machine_discovery_cache
3237 .read()
3238 .await
3239 .get(coord)
3240 .cloned()
3241 {
3242 let coord_peer = ant_quic::PeerId(coord_machine.machine_id.0);
3243 let coord_addrs = coord_machine.addresses.clone();
3244 if !coord_addrs.is_empty() {
3245 if let Err(e) = network
3246 .upsert_peer_hints(coord_peer, coord_addrs, None)
3247 .await
3248 {
3249 tracing::debug!(
3250 target: "x0x::connect",
3251 %agent_prefix,
3252 coord_prefix = %network::hex_prefix(&coord.0, 4),
3253 error = %e,
3254 "failed to seed coordinator hints from reachable_via"
3255 );
3256 }
3257 }
3258 }
3259 }
3260
3261 let peer_id_hint = ant_quic::PeerId(agent.machine_id.0);
3265 let hint_was_zeroed = agent.machine_id.0 == [0u8; 32];
3266 self.seed_transport_peer_hints_for_target(network, &agent)
3267 .await
3268 .map_err(|e| {
3269 error::IdentityError::Storage(std::io::Error::other(format!(
3270 "failed to seed transport peer hints: {e}"
3271 )))
3272 })?;
3273 let coordinated_result = tokio::time::timeout(
3274 dial_timeout,
3275 network.connect_peer_with_addrs(peer_id_hint, info.addresses.clone()),
3276 )
3277 .await;
3278 match coordinated_result {
3279 Ok(Ok((addr, verified_peer_id))) => {
3280 let verified_machine_id = identity::MachineId(verified_peer_id.0);
3281
3282 if !hint_was_zeroed {
3288 if let Some(ref bc) = self.bootstrap_cache {
3289 bc.add_from_connection(verified_peer_id, vec![addr], None)
3290 .await;
3291 bc.record_success(&verified_peer_id, 0).await;
3292 }
3293 {
3294 let mut cache = self.identity_discovery_cache.write().await;
3295 if let Some(entry) = cache.get_mut(agent_id) {
3296 entry.machine_id = verified_machine_id;
3297 }
3298 }
3299 }
3300
3301 if !hint_was_zeroed {
3308 self.direct_messaging
3309 .mark_connected(agent.agent_id, verified_machine_id)
3310 .await;
3311 }
3312 let family = if addr.is_ipv4() { "v4" } else { "v6" };
3313 tracing::info!(
3314 target: "x0x::connect",
3315 stage = "connect_to_agent",
3316 %agent_prefix,
3317 strategy = "coordinated_fallback",
3318 outcome = "coordinated",
3319 selected_addr = %addr,
3320 family,
3321 hint_was_zeroed,
3322 dur_ms = call_start.elapsed().as_millis() as u64,
3323 "coordinated dial succeeded"
3324 );
3325 return Ok(connectivity::ConnectOutcome::Coordinated(addr));
3326 }
3327 Ok(Err(e)) => {
3328 tracing::debug!(
3329 target: "x0x::connect",
3330 %agent_prefix,
3331 strategy = "coordinated_fallback",
3332 error = %e,
3333 "coordinated dial failed"
3334 );
3335 }
3336 Err(_) => {
3337 tracing::debug!(
3338 target: "x0x::connect",
3339 %agent_prefix,
3340 strategy = "coordinated_fallback",
3341 timeout_s = dial_timeout.as_secs(),
3342 "coordinated dial timed out"
3343 );
3344 }
3345 }
3346 }
3347
3348 tracing::warn!(
3349 target: "x0x::connect",
3350 stage = "connect_to_agent",
3351 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
3352 outcome = "unreachable",
3353 reason = "all_strategies_exhausted",
3354 dur_ms = call_start.elapsed().as_millis() as u64,
3355 v4_addrs,
3356 v6_addrs,
3357 "all connection strategies exhausted"
3358 );
3359 Ok(connectivity::ConnectOutcome::Unreachable)
3360 }
3361
3362 pub async fn connect_to_machine(
3374 &self,
3375 machine_id: &identity::MachineId,
3376 ) -> error::Result<connectivity::ConnectOutcome> {
3377 let call_start = std::time::Instant::now();
3378 let machine_prefix = network::hex_prefix(&machine_id.0, 4);
3379 tracing::debug!(
3380 target: "x0x::connect",
3381 stage = "connect_to_machine",
3382 %machine_prefix,
3383 "begin"
3384 );
3385
3386 let machine = {
3387 let cache = self.machine_discovery_cache.read().await;
3388 cache.get(machine_id).cloned()
3389 };
3390 let Some(machine) = machine else {
3391 tracing::info!(
3392 target: "x0x::connect",
3393 stage = "connect_to_machine",
3394 %machine_prefix,
3395 outcome = "not_found",
3396 dur_ms = call_start.elapsed().as_millis() as u64,
3397 "machine not in discovery cache"
3398 );
3399 return Ok(connectivity::ConnectOutcome::NotFound);
3400 };
3401
3402 let Some(ref network) = self.network else {
3403 tracing::warn!(
3404 target: "x0x::connect",
3405 stage = "connect_to_machine",
3406 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
3407 outcome = "unreachable_no_network",
3408 "network layer not initialised"
3409 );
3410 return Ok(connectivity::ConnectOutcome::Unreachable);
3411 };
3412
3413 let peer_id = ant_quic::PeerId(machine.machine_id.0);
3414 if network.is_connected(&peer_id).await {
3415 for agent_id in &machine.agent_ids {
3416 self.direct_messaging
3417 .mark_connected(*agent_id, machine.machine_id)
3418 .await;
3419 }
3420 return if let Some(addr) = machine.addresses.first() {
3421 Ok(connectivity::ConnectOutcome::Direct(*addr))
3422 } else {
3423 Ok(connectivity::ConnectOutcome::AlreadyConnected)
3424 };
3425 }
3426
3427 let info = connectivity::ReachabilityInfo::from_discovered_machine(&machine);
3428 let v4_addrs = info.addresses.iter().filter(|a| a.is_ipv4()).count();
3429 let v6_addrs = info.addresses.len() - v4_addrs;
3430 tracing::info!(
3431 target: "x0x::connect",
3432 stage = "connect_to_machine",
3433 %machine_prefix,
3434 addr_total = info.addresses.len(),
3435 v4_addrs,
3436 v6_addrs,
3437 can_receive_direct = ?info.can_receive_direct,
3438 should_attempt_direct = info.should_attempt_direct(),
3439 needs_coordination = info.needs_coordination(),
3440 "machine reachability classified"
3441 );
3442
3443 if info.addresses.is_empty() {
3444 return Ok(connectivity::ConnectOutcome::Unreachable);
3445 }
3446
3447 let dial_timeout = std::time::Duration::from_secs(8);
3448 let local_probe_timeout = std::time::Duration::from_secs(3);
3449 let direct_probe_addrs = local_direct_probe_addrs(&info.addresses);
3450
3451 for addr in &direct_probe_addrs {
3452 match tokio::time::timeout(
3453 local_probe_timeout,
3454 network.connect_peer_with_addrs(peer_id, vec![*addr]),
3455 )
3456 .await
3457 {
3458 Ok(Ok((selected_addr, connected_peer_id))) if connected_peer_id == peer_id => {
3459 if let Some(ref bc) = self.bootstrap_cache {
3460 bc.add_from_connection(connected_peer_id, vec![selected_addr], None)
3461 .await;
3462 }
3463 for agent_id in &machine.agent_ids {
3464 self.direct_messaging
3465 .mark_connected(*agent_id, machine.machine_id)
3466 .await;
3467 }
3468 tracing::info!(
3469 target: "x0x::connect",
3470 stage = "connect_to_machine",
3471 %machine_prefix,
3472 strategy = "local_direct_first",
3473 outcome = "direct",
3474 selected_addr = %selected_addr,
3475 dur_ms = call_start.elapsed().as_millis() as u64,
3476 "machine local direct dial succeeded"
3477 );
3478 return Ok(connectivity::ConnectOutcome::Direct(selected_addr));
3479 }
3480 Ok(Ok((selected_addr, connected_peer_id))) => {
3481 tracing::warn!(
3482 target: "x0x::connect",
3483 stage = "connect_to_machine",
3484 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
3485 requested_addr = %crate::logging::LogHexId::addr(&addr.to_string()),
3486 selected_addr = %crate::logging::LogHexId::addr(&selected_addr.to_string()),
3487 connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
3488 "machine local direct dial reached unexpected peer"
3489 );
3490 }
3491 Ok(Err(e)) => {
3492 tracing::debug!(
3493 target: "x0x::connect",
3494 stage = "connect_to_machine",
3495 %machine_prefix,
3496 strategy = "local_direct_first",
3497 %addr,
3498 error = %e,
3499 "machine local direct dial failed"
3500 );
3501 }
3502 Err(_) => {
3503 tracing::debug!(
3504 target: "x0x::connect",
3505 stage = "connect_to_machine",
3506 %machine_prefix,
3507 strategy = "local_direct_first",
3508 %addr,
3509 timeout_s = local_probe_timeout.as_secs(),
3510 "machine local direct dial timed out"
3511 );
3512 }
3513 }
3514
3515 match tokio::time::timeout(local_probe_timeout, network.connect_addr(*addr)).await {
3516 Ok(Ok(connected_peer_id)) if connected_peer_id == peer_id => {
3517 if let Some(ref bc) = self.bootstrap_cache {
3518 bc.add_from_connection(connected_peer_id, vec![*addr], None)
3519 .await;
3520 }
3521 for agent_id in &machine.agent_ids {
3522 self.direct_messaging
3523 .mark_connected(*agent_id, machine.machine_id)
3524 .await;
3525 }
3526 tracing::info!(
3527 target: "x0x::connect",
3528 stage = "connect_to_machine",
3529 %machine_prefix,
3530 strategy = "local_direct_raw_fallback",
3531 outcome = "direct",
3532 selected_addr = %addr,
3533 dur_ms = call_start.elapsed().as_millis() as u64,
3534 "verified machine local raw-address fallback succeeded"
3535 );
3536 return Ok(connectivity::ConnectOutcome::Direct(*addr));
3537 }
3538 Ok(Ok(connected_peer_id)) => {
3539 tracing::warn!(
3540 target: "x0x::connect",
3541 stage = "connect_to_machine",
3542 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
3543 strategy = "local_direct_raw_fallback",
3544 addr = %crate::logging::LogHexId::addr(&addr.to_string()),
3545 connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
3546 "verified machine local raw-address fallback reached unexpected peer"
3547 );
3548 }
3549 Ok(Err(e)) => {
3550 tracing::debug!(
3551 target: "x0x::connect",
3552 stage = "connect_to_machine",
3553 %machine_prefix,
3554 strategy = "local_direct_raw_fallback",
3555 %addr,
3556 error = %e,
3557 "verified machine local raw-address fallback failed"
3558 );
3559 }
3560 Err(_) => {
3561 tracing::debug!(
3562 target: "x0x::connect",
3563 stage = "connect_to_machine",
3564 %machine_prefix,
3565 strategy = "local_direct_raw_fallback",
3566 %addr,
3567 timeout_s = local_probe_timeout.as_secs(),
3568 "verified machine local raw-address fallback timed out"
3569 );
3570 }
3571 }
3572 }
3573
3574 network
3575 .upsert_peer_hints(peer_id, info.addresses.clone(), None)
3576 .await
3577 .map_err(|e| {
3578 error::IdentityError::Storage(std::io::Error::other(format!(
3579 "failed to upsert machine peer hints: {e}"
3580 )))
3581 })?;
3582
3583 match tokio::time::timeout(
3584 dial_timeout,
3585 network.connect_peer_with_addrs(peer_id, info.addresses.clone()),
3586 )
3587 .await
3588 {
3589 Ok(Ok((addr, verified_peer_id))) if verified_peer_id == peer_id => {
3590 if let Some(ref bc) = self.bootstrap_cache {
3591 bc.add_from_connection(verified_peer_id, vec![addr], None)
3592 .await;
3593 bc.record_success(&verified_peer_id, 0).await;
3594 }
3595 for agent_id in &machine.agent_ids {
3596 self.direct_messaging
3597 .mark_connected(*agent_id, machine.machine_id)
3598 .await;
3599 }
3600 tracing::info!(
3601 target: "x0x::connect",
3602 stage = "connect_to_machine",
3603 %machine_prefix,
3604 strategy = "hinted_peer",
3605 outcome = "coordinated",
3606 selected_addr = %addr,
3607 dur_ms = call_start.elapsed().as_millis() as u64,
3608 "machine peer-authenticated dial succeeded"
3609 );
3610 return Ok(connectivity::ConnectOutcome::Coordinated(addr));
3611 }
3612 Ok(Ok((addr, verified_peer_id))) => {
3613 tracing::warn!(
3614 target: "x0x::connect",
3615 stage = "connect_to_machine",
3616 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
3617 selected_addr = %crate::logging::LogHexId::addr(&addr.to_string()),
3618 verified_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&verified_peer_id.0, 4)),
3619 "machine dial reached unexpected peer"
3620 );
3621 }
3622 Ok(Err(e)) => {
3623 tracing::debug!(
3624 target: "x0x::connect",
3625 stage = "connect_to_machine",
3626 %machine_prefix,
3627 error = %e,
3628 "machine peer-authenticated dial failed"
3629 );
3630 }
3631 Err(_) => {
3632 tracing::debug!(
3633 target: "x0x::connect",
3634 stage = "connect_to_machine",
3635 %machine_prefix,
3636 timeout_s = dial_timeout.as_secs(),
3637 "machine peer-authenticated dial timed out"
3638 );
3639 }
3640 }
3641
3642 if info.should_attempt_direct() {
3643 for addr in &info.addresses {
3644 if direct_probe_addrs.contains(addr) {
3645 continue;
3646 }
3647 match tokio::time::timeout(dial_timeout, network.connect_addr(*addr)).await {
3648 Ok(Ok(connected_peer_id)) if connected_peer_id == peer_id => {
3649 if let Some(ref bc) = self.bootstrap_cache {
3650 bc.add_from_connection(connected_peer_id, vec![*addr], None)
3651 .await;
3652 }
3653 for agent_id in &machine.agent_ids {
3654 self.direct_messaging
3655 .mark_connected(*agent_id, machine.machine_id)
3656 .await;
3657 }
3658 tracing::info!(
3659 target: "x0x::connect",
3660 stage = "connect_to_machine",
3661 %machine_prefix,
3662 strategy = "direct_per_addr",
3663 outcome = "direct",
3664 selected_addr = %addr,
3665 dur_ms = call_start.elapsed().as_millis() as u64,
3666 "machine direct dial succeeded"
3667 );
3668 return Ok(connectivity::ConnectOutcome::Direct(*addr));
3669 }
3670 Ok(Ok(connected_peer_id)) => {
3671 tracing::warn!(
3672 target: "x0x::connect",
3673 stage = "connect_to_machine",
3674 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
3675 addr = %crate::logging::LogHexId::addr(&addr.to_string()),
3676 connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
3677 "machine direct dial reached unexpected peer"
3678 );
3679 }
3680 Ok(Err(e)) => {
3681 tracing::debug!(
3682 target: "x0x::connect",
3683 stage = "connect_to_machine",
3684 %machine_prefix,
3685 %addr,
3686 error = %e,
3687 "machine direct dial failed"
3688 );
3689 }
3690 Err(_) => {
3691 tracing::debug!(
3692 target: "x0x::connect",
3693 stage = "connect_to_machine",
3694 %machine_prefix,
3695 %addr,
3696 timeout_s = dial_timeout.as_secs(),
3697 "machine direct dial timed out"
3698 );
3699 }
3700 }
3701 }
3702 }
3703
3704 tracing::warn!(
3705 target: "x0x::connect",
3706 stage = "connect_to_machine",
3707 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
3708 outcome = "unreachable",
3709 reason = "all_strategies_exhausted",
3710 dur_ms = call_start.elapsed().as_millis() as u64,
3711 v4_addrs,
3712 v6_addrs,
3713 "all machine connection strategies exhausted"
3714 );
3715 Ok(connectivity::ConnectOutcome::Unreachable)
3716 }
3717
3718 fn spawn_tracked<F>(&self, fut: F)
3723 where
3724 F: std::future::Future<Output = ()> + Send + 'static,
3725 {
3726 let mut guard = match self.tracked_tasks.lock() {
3729 Ok(guard) => guard,
3730 Err(poisoned) => poisoned.into_inner(),
3731 };
3732 if guard.closed {
3733 return;
3734 }
3735 guard.handles.push(tokio::spawn(fut));
3736 }
3737
3738 pub fn begin_shutdown(&self) {
3750 self.shutdown_token.cancel();
3751 let mut guard = match self.tracked_tasks.lock() {
3752 Ok(guard) => guard,
3753 Err(poisoned) => poisoned.into_inner(),
3754 };
3755 guard.closed = true;
3756 }
3757
3758 pub async fn shutdown(&self) {
3772 self.shutdown_token.cancel();
3775
3776 self.stop_identity_heartbeat().await;
3778 self.stop_discovery_cache_reaper().await;
3779
3780 self.stop_dm_inbox().await;
3784 {
3785 let service = {
3786 let mut guard = self.capability_advert_service.lock().await;
3787 guard.take()
3788 };
3789 if let Some(service) = service {
3790 service.abort();
3791 }
3792 }
3793
3794 let handles = {
3800 let mut guard = match self.tracked_tasks.lock() {
3801 Ok(guard) => guard,
3802 Err(poisoned) => poisoned.into_inner(),
3803 };
3804 guard.closed = true;
3805 std::mem::take(&mut guard.handles)
3806 };
3807 if !handles.is_empty() {
3808 let abort_handles: Vec<tokio::task::AbortHandle> =
3816 handles.iter().map(|h| h.abort_handle()).collect();
3817 let mut join = futures::future::join_all(handles);
3818 tokio::select! {
3819 _results = &mut join => {}
3820 _ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {
3821 tracing::warn!(
3822 "Agent background tasks did not stop within grace; aborting stragglers"
3823 );
3824 for handle in &abort_handles {
3825 handle.abort();
3826 }
3827 let _results: Vec<Result<(), tokio::task::JoinError>> = join.await;
3828 }
3829 }
3830 }
3831
3832 if let Some(ref pw) = self.presence {
3834 pw.shutdown().await;
3835 tracing::info!("Presence system shut down");
3836 }
3837
3838 if let Some(ref cache) = self.bootstrap_cache {
3839 if let Err(e) = cache.save().await {
3840 tracing::warn!("Failed to save bootstrap cache on shutdown: {e}");
3841 } else {
3842 tracing::info!("Bootstrap cache saved on shutdown");
3843 }
3844 }
3845
3846 if let Some(ref runtime) = self.gossip_runtime {
3853 if let Err(e) = runtime.shutdown().await {
3854 tracing::warn!("Gossip runtime shutdown error: {e}");
3855 } else {
3856 tracing::info!("Gossip runtime shut down");
3857 }
3858 }
3859 if let Some(ref network) = self.network {
3860 network.shutdown().await;
3861 tracing::info!("Network node shut down");
3862 }
3863 }
3864
3865 async fn stop_identity_heartbeat(&self) {
3866 let handle = {
3867 let mut handle_guard = self.heartbeat_handle.lock().await;
3868 handle_guard.take()
3869 };
3870
3871 if let Some(handle) = handle {
3872 handle.abort();
3873 match handle.await {
3874 Ok(()) => tracing::debug!("Identity heartbeat task stopped"),
3875 Err(e) if e.is_cancelled() => {
3876 tracing::debug!("Identity heartbeat task aborted")
3877 }
3878 Err(e) => tracing::warn!("Identity heartbeat task failed during shutdown: {e}"),
3879 }
3880 }
3881 }
3882
3883 async fn stop_discovery_cache_reaper(&self) {
3884 let handle = {
3885 let mut handle_guard = self.discovery_cache_reaper_handle.lock().await;
3886 handle_guard.take()
3887 };
3888
3889 if let Some(handle) = handle {
3890 handle.abort();
3891 match handle.await {
3892 Ok(()) => tracing::debug!("Discovery cache reaper stopped"),
3893 Err(e) if e.is_cancelled() => {
3894 tracing::debug!("Discovery cache reaper aborted")
3895 }
3896 Err(e) => tracing::warn!("Discovery cache reaper failed during shutdown: {e}"),
3897 }
3898 }
3899 }
3900
3901 async fn discovery_cache_reaper_loop(
3906 identity_cache: std::sync::Arc<
3907 tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
3908 >,
3909 machine_cache: std::sync::Arc<
3910 tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
3911 >,
3912 user_cache: std::sync::Arc<
3913 tokio::sync::RwLock<std::collections::HashMap<identity::UserId, DiscoveredUser>>,
3914 >,
3915 ttl_secs: u64,
3916 interval: std::time::Duration,
3917 ) {
3918 loop {
3919 tokio::time::sleep(interval).await;
3920 let cutoff = Self::unix_timestamp_secs().saturating_sub(ttl_secs);
3921 {
3923 let mut c = identity_cache.write().await;
3924 c.retain(|_, a| a.last_seen >= cutoff);
3925 }
3926 {
3928 let mut c = machine_cache.write().await;
3929 c.retain(|_, m| m.last_seen >= cutoff);
3930 }
3931 {
3933 let mut c = user_cache.write().await;
3934 c.retain(|_, u| u.last_seen >= cutoff);
3935 }
3936 }
3937 }
3938
3939 async fn start_discovery_cache_reaper(&self) -> error::Result<()> {
3940 let mut guard = self.discovery_cache_reaper_handle.lock().await;
3941 if self.shutdown_token.is_cancelled() {
3944 return Ok(());
3945 }
3946 if guard.is_some() {
3947 return Ok(());
3948 }
3949
3950 let identity = std::sync::Arc::clone(&self.identity_discovery_cache);
3951 let machine = std::sync::Arc::clone(&self.machine_discovery_cache);
3952 let user = std::sync::Arc::clone(&self.user_discovery_cache);
3953 let ttl = self.identity_ttl_secs;
3954 let interval = std::time::Duration::from_secs(DISCOVERY_CACHE_REAPER_INTERVAL_SECS);
3955
3956 let handle = tokio::spawn(Self::discovery_cache_reaper_loop(
3957 identity, machine, user, ttl, interval,
3958 ));
3959 *guard = Some(handle);
3960 Ok(())
3961 }
3962
3963 pub async fn send_direct(
4009 &self,
4010 to: &identity::AgentId,
4011 payload: Vec<u8>,
4012 ) -> Result<dm::DmReceipt, dm::DmError> {
4013 self.send_direct_with_config(to, payload, dm::DmSendConfig::default())
4014 .await
4015 }
4016
4017 async fn dm_peer_rtt_ms(&self, agent_id: &identity::AgentId) -> Option<u32> {
4018 let registry_machine_id = self.direct_messaging.get_machine_id(agent_id).await;
4019 let cached_machine_id = {
4020 let cache = self.identity_discovery_cache.read().await;
4021 cache
4022 .get(agent_id)
4023 .map(|entry| entry.machine_id)
4024 .filter(|machine_id| machine_id.0 != [0_u8; 32])
4025 };
4026 let machine_id = registry_machine_id.or(cached_machine_id)?;
4027 let peer = self
4028 .bootstrap_cache
4029 .as_ref()?
4030 .get(&ant_quic::PeerId(machine_id.0))
4031 .await?;
4032 (peer.stats.avg_rtt_ms > 0).then_some(peer.stats.avg_rtt_ms)
4033 }
4034
4035 async fn dm_lifecycle_hint(
4040 &self,
4041 agent_id: &identity::AgentId,
4042 ) -> Option<dm_send::DmLifecycleHint> {
4043 let registry_machine_id = self.direct_messaging.get_machine_id(agent_id).await;
4044 let cached_machine_id = {
4045 let cache = self.identity_discovery_cache.read().await;
4046 cache
4047 .get(agent_id)
4048 .map(|entry| entry.machine_id)
4049 .filter(|machine_id| machine_id.0 != [0_u8; 32])
4050 };
4051 let machine_id = registry_machine_id.or(cached_machine_id)?;
4052 Some(dm_send::DmLifecycleHint {
4053 recipient_machine_id: machine_id,
4054 replaced_rx: self.direct_messaging.subscribe_lifecycle_replaced(),
4055 })
4056 }
4057
4058 async fn dm_peer_likely_offline(
4059 &self,
4060 agent_id: &identity::AgentId,
4061 ) -> Option<(f64, Option<u64>)> {
4062 if self.is_agent_connected(agent_id).await {
4063 return None;
4064 }
4065 let last_seen = {
4066 let cache = self.identity_discovery_cache.read().await;
4067 cache.get(agent_id).map(|entry| entry.last_seen)
4068 }?;
4069 let now_secs = Self::unix_timestamp_secs();
4070 let age_secs = now_secs.saturating_sub(last_seen);
4071 let heartbeat = self.heartbeat_interval_secs.max(1);
4072 let phi = age_secs as f64 / heartbeat as f64;
4073 (phi > 8.0).then_some((phi, Some(age_secs.saturating_mul(1000))))
4074 }
4075
4076 pub async fn send_direct_with_config(
4082 &self,
4083 to: &identity::AgentId,
4084 payload: Vec<u8>,
4085 config: dm::DmSendConfig,
4086 ) -> Result<dm::DmReceipt, dm::DmError> {
4087 if *to == self.identity.agent_id() {
4088 self.direct_messaging.record_outgoing_started(*to, None);
4089 if payload.len() > direct::MAX_DIRECT_PAYLOAD_SIZE {
4090 self.direct_messaging.record_outgoing_failed(*to);
4091 return Err(dm::DmError::PayloadTooLarge {
4092 len: payload.len(),
4093 max: direct::MAX_DIRECT_PAYLOAD_SIZE,
4094 });
4095 }
4096
4097 let delivered = self
4098 .direct_messaging
4099 .handle_loopback(
4100 self.identity.machine_id(),
4101 self.identity.agent_id(),
4102 payload,
4103 )
4104 .await;
4105 let receipt = dm_send::loopback_receipt();
4106 self.direct_messaging
4107 .record_outgoing_succeeded(*to, receipt.path);
4108 tracing::debug!(
4109 target: "dm.trace",
4110 stage = "outbound_send_returned_ok",
4111 request_id = %hex::encode(receipt.request_id),
4112 sender = %hex::encode(self.identity.agent_id().as_bytes()),
4113 recipient = %hex::encode(to.as_bytes()),
4114 path = "loopback",
4115 delivered_subscribers = delivered,
4116 );
4117 return Ok(receipt);
4118 }
4119
4120 let advert_cap = self.capability_store.lookup(to);
4121 let advert_gossip_ready = advert_cap
4122 .as_ref()
4123 .is_some_and(|caps| caps.gossip_inbox && !caps.kem_public_key.is_empty());
4124 let (cap, cap_source) = if advert_gossip_ready {
4125 (advert_cap, "advert_cache")
4126 } else {
4127 let contact_cap = {
4128 let contacts = self.contact_store.read().await;
4129 contacts.get(to).and_then(|contact| {
4130 contact
4131 .dm_capabilities
4132 .as_ref()
4133 .filter(|caps| caps.gossip_inbox && !caps.kem_public_key.is_empty())
4134 .cloned()
4135 })
4136 };
4137 match contact_cap {
4138 Some(cap) if advert_cap.is_some() => {
4139 (Some(cap), "contact_card_after_unusable_advert")
4140 }
4141 Some(cap) => (Some(cap), "contact_card"),
4142 None if advert_cap.is_some() => (advert_cap, "advert_cache_unusable"),
4143 None => (None, "none"),
4144 }
4145 };
4146 let gossip_ok = cap
4147 .as_ref()
4148 .map(|c| c.gossip_inbox && !c.kem_public_key.is_empty())
4149 .unwrap_or(false);
4150 tracing::debug!(
4151 target: "dm.trace",
4152 stage = "capability_lookup",
4153 recipient = %hex::encode(to.as_bytes()),
4154 hit = cap.is_some(),
4155 gossip_ok,
4156 source = cap_source,
4157 capability_store_entries = self.capability_store.len(),
4158 );
4159
4160 let relay_seed: Option<(Vec<u8>, Vec<u8>)> = if self.peer_relay.policy().enabled {
4165 cap.as_ref()
4166 .filter(|c| !c.kem_public_key.is_empty())
4167 .map(|c| (payload.clone(), c.kem_public_key.clone()))
4168 } else {
4169 None
4170 };
4171
4172 let rtt_hint_ms = self.dm_peer_rtt_ms(to).await;
4173 let mut config = config;
4174 if !gossip_ok && config.timeout_per_attempt == dm::dm_attempt_timeout(None) {
4179 config.timeout_per_attempt = dm::dm_attempt_timeout(rtt_hint_ms);
4180 }
4181 self.direct_messaging
4182 .record_outgoing_started(*to, rtt_hint_ms);
4183 if let Some((phi, last_seen_ms_ago)) = self.dm_peer_likely_offline(to).await {
4184 self.direct_messaging.record_outgoing_failed(*to);
4185 return Err(dm::DmError::PeerLikelyOffline {
4186 phi,
4187 last_seen_ms_ago,
4188 });
4189 }
4190
4191 let mut preferred_raw_err = None;
4192 let prefer_newest_grace = std::time::Duration::from_millis(config.prefer_newest_grace_ms);
4193 let preferred_raw_receipt = if config.prefer_raw_quic_if_connected && !config.require_gossip
4194 {
4195 match self
4196 .send_direct_raw_quic(
4197 to,
4198 &payload,
4199 config.raw_quic_receive_ack_timeout,
4200 prefer_newest_grace,
4201 )
4202 .await
4203 {
4204 Ok(path) => Some(dm_send::raw_quic_receipt_for_path(path)),
4205 Err(e) => {
4206 tracing::debug!(
4207 target: "x0x::direct",
4208 recipient = %hex::encode(to.as_bytes()),
4209 error = %e,
4210 "preferred raw-QUIC path unavailable; falling back to capability-aware send"
4211 );
4212 preferred_raw_err = Some(e);
4213 None
4214 }
4215 }
4216 } else {
4217 None
4218 };
4219
4220 let result = if let Some(receipt) = preferred_raw_receipt {
4221 Ok(receipt)
4222 } else if preferred_raw_err.as_ref().is_some_and(|err| {
4223 config.stop_fallback_on_raw_error
4224 || Self::raw_quic_error_should_stop_fallback(err, gossip_ok)
4225 }) {
4226 match preferred_raw_err.take() {
4227 Some(e) => Err(Self::map_raw_quic_dm_error(e)),
4228 None => Err(dm::DmError::NoConnectivity(
4229 "raw-QUIC send failed before gossip fallback".to_string(),
4230 )),
4231 }
4232 } else if gossip_ok {
4233 match self.gossip_runtime.as_ref() {
4234 Some(runtime) => {
4235 let signing =
4236 gossip::SigningContext::from_keypair(self.identity.agent_keypair());
4237 let kem_pub = cap
4238 .as_ref()
4239 .map(|c| c.kem_public_key.clone())
4240 .unwrap_or_default();
4241 let lifecycle_hint = self.dm_lifecycle_hint(to).await;
4246 dm_send::send_via_gossip(
4247 dm_send::DmSendContext {
4248 pubsub: std::sync::Arc::clone(runtime.pubsub()),
4249 signing: &signing,
4250 self_agent_id: self.identity.agent_id(),
4251 self_machine_id: self.identity.machine_id(),
4252 inflight: std::sync::Arc::clone(&self.dm_inflight_acks),
4253 },
4254 *to,
4255 &kem_pub,
4256 payload,
4257 &config,
4258 lifecycle_hint,
4259 )
4260 .await
4261 }
4262 None => Err(dm::DmError::LocalGossipUnavailable(
4263 "send_direct: no gossip runtime configured".to_string(),
4264 )),
4265 }
4266 } else if config.require_gossip {
4267 Err(dm::DmError::RecipientKeyUnavailable(format!(
4268 "recipient {} has no gossip DM capability advert",
4269 hex::encode(to.as_bytes())
4270 )))
4271 } else {
4272 match preferred_raw_err {
4273 Some(e) => Err(Self::map_raw_quic_dm_error(e)),
4274 None => self
4275 .send_direct_raw_quic(
4276 to,
4277 &payload,
4278 config.raw_quic_receive_ack_timeout,
4279 prefer_newest_grace,
4280 )
4281 .await
4282 .map(dm_send::raw_quic_receipt_for_path)
4283 .map_err(Self::map_raw_quic_dm_error),
4284 }
4285 };
4286
4287 match result {
4288 Ok(receipt) => {
4289 self.direct_messaging
4290 .record_outgoing_succeeded(*to, receipt.path);
4291 self.peer_relay.record_direct_success(to);
4297 Ok(receipt)
4298 }
4299 Err(direct_err) => {
4300 self.direct_messaging.record_outgoing_failed(*to);
4301 self.peer_relay.record_direct_failure(to);
4306 if let Some((saved_payload, kem_pub)) = relay_seed {
4314 if self.peer_relay.needs_relay(to) {
4315 match self.try_relay_fallback(to, saved_payload, &kem_pub).await {
4316 Ok(relay_receipt) => {
4317 self.direct_messaging
4318 .record_outgoing_succeeded(*to, relay_receipt.path);
4319 return Ok(relay_receipt);
4320 }
4321 Err(relay_err) => {
4322 tracing::debug!(
4323 target: "x0x::relay",
4324 recipient = %hex::encode(to.as_bytes()),
4325 direct_err = %direct_err,
4326 relay_err = %relay_err,
4327 "X0X-0070b relay fallback failed; surfacing original direct error"
4328 );
4329 }
4330 }
4331 }
4332 }
4333 Err(direct_err)
4334 }
4335 }
4336 }
4337
4338 async fn try_relay_fallback(
4362 &self,
4363 to: &identity::AgentId,
4364 payload: Vec<u8>,
4365 recipient_kem_public_key: &[u8],
4366 ) -> Result<dm::DmReceipt, dm::DmError> {
4367 let sender = self.identity.agent_id();
4368 let candidates = self.relay_candidates.read().await.clone();
4369 let Some(relay_agent) = self.peer_relay.select_relay(&candidates, to, &sender) else {
4370 return Err(dm::DmError::NoRelayCandidate);
4371 };
4372
4373 let relay_machine_id = {
4374 let cache = self.identity_discovery_cache.read().await;
4375 cache.get(&relay_agent).map(|e| e.machine_id)
4376 };
4377 let Some(relay_machine_id) = relay_machine_id else {
4378 return Err(dm::DmError::NoRelayCandidate);
4382 };
4383
4384 let now = dm::now_unix_ms();
4385 let expires = now.saturating_add(dm_send::DEFAULT_ENVELOPE_LIFETIME_MS);
4386 let request_id = dm_send::fresh_request_id();
4387 let signing = gossip::SigningContext::from_keypair(self.identity.agent_keypair());
4388 let envelope = dm::EnvelopeBuilder::build_payload_envelope(
4389 request_id,
4390 &sender,
4391 &self.identity.machine_id(),
4392 to,
4393 recipient_kem_public_key,
4394 now,
4395 expires,
4396 payload,
4397 |bytes| signing.sign(bytes).map_err(|e| e.to_string()),
4398 )?;
4399
4400 let (sender_pub_bytes, sender_sec_bytes) = self.identity.agent_keypair().to_bytes();
4401 let sender_secret = ant_quic::MlDsaSecretKey::from_bytes(&sender_sec_bytes)
4402 .map_err(|e| dm::DmError::RelayBuildFailed(format!("agent secret key: {e:?}")))?;
4403 let relayed = self
4404 .peer_relay
4405 .build_relayed_dm(to, &sender, sender_pub_bytes, now, envelope, |bytes| {
4406 ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(&sender_secret, bytes)
4407 .map(|s| s.as_bytes().to_vec())
4408 .map_err(|e| format!("{e:?}"))
4409 })
4410 .map_err(dm::DmError::RelayBuildFailed)?;
4411
4412 let wire = postcard::to_allocvec(&relayed).map_err(|e| {
4413 dm::DmError::EnvelopeConstruction(format!("relayed envelope postcard: {e}"))
4414 })?;
4415 let network = self
4416 .network
4417 .as_ref()
4418 .ok_or_else(|| dm::DmError::NoConnectivity("no network for relay send".to_string()))?;
4419 let relay_peer_id = ant_quic::PeerId(relay_machine_id.0);
4420 network
4421 .send_direct_typed(
4422 &relay_peer_id,
4423 sender.as_bytes(),
4424 network::RELAYED_DM_STREAM_TYPE,
4425 &wire,
4426 )
4427 .await
4428 .map_err(|e| dm::DmError::PublishFailed(format!("relay send: {e}")))?;
4429
4430 Ok(dm::DmReceipt {
4431 request_id,
4432 accepted_at: std::time::Instant::now(),
4433 retries_used: 0,
4434 path: dm::DmPath::Relayed { via: relay_agent },
4435 })
4436 }
4437
4438 #[must_use]
4442 pub fn peer_relay(&self) -> &peer_relay::PeerRelay {
4443 &self.peer_relay
4444 }
4445
4446 pub async fn relay_candidates(&self) -> Vec<identity::AgentId> {
4452 self.relay_candidates.read().await.clone()
4453 }
4454
4455 async fn send_direct_raw_quic(
4473 &self,
4474 agent_id: &identity::AgentId,
4475 payload: &[u8],
4476 receive_ack_timeout: Option<std::time::Duration>,
4477 prefer_newest_grace: std::time::Duration,
4478 ) -> error::NetworkResult<dm::DmPath> {
4479 let send_start = std::time::Instant::now();
4480 let agent_prefix = network::hex_prefix(&agent_id.0, 4);
4481 let self_prefix = network::hex_prefix(&self.identity.agent_id().0, 4);
4482 let bytes = payload.len();
4483 let digest = direct::dm_payload_digest_hex(payload);
4484 let target_path_label = if receive_ack_timeout.is_some() {
4485 "raw_quic_acked"
4486 } else {
4487 "raw_quic"
4488 };
4489
4490 let network = self.network.as_ref().ok_or_else(|| {
4491 tracing::warn!(
4492 target: "x0x::direct",
4493 stage = "send",
4494 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
4495 outcome = "err_no_network",
4496 "network not initialised"
4497 );
4498 error::NetworkError::NodeCreation("network not initialized".to_string())
4499 })?;
4500
4501 let cached_machine_id = {
4506 let cache = self.identity_discovery_cache.read().await;
4507 cache
4508 .get(agent_id)
4509 .map(|d| d.machine_id)
4510 .filter(|m| m.0 != [0u8; 32]) };
4512 let registry_machine_id = self.direct_messaging.get_machine_id(agent_id).await;
4513
4514 let (machine_id, resolution) = match (cached_machine_id, registry_machine_id) {
4515 (Some(id), _) if network.is_connected(&ant_quic::PeerId(id.0)).await => {
4516 (id, "cached_connected")
4517 }
4518 (_, Some(id)) if network.is_connected(&ant_quic::PeerId(id.0)).await => {
4519 if cached_machine_id != Some(id) {
4520 let mut cache = self.identity_discovery_cache.write().await;
4521 if let Some(entry) = cache.get_mut(agent_id) {
4522 entry.machine_id = id;
4523 }
4524 }
4525 (id, "registry_connected")
4526 }
4527 (Some(id), None) => (id, "cached_not_connected"),
4528 (Some(id), Some(_)) => (id, "cached_both_disconnected"),
4529 (None, Some(id)) => (id, "registry_not_connected"),
4530 (None, None) => {
4531 tracing::debug!(
4532 target: "x0x::direct",
4533 stage = "send",
4534 %agent_prefix,
4535 resolution = "last_resort_connect",
4536 "no machine_id known; triggering connect_to_agent"
4537 );
4538 let _ = self.connect_to_agent(agent_id).await;
4539 let id = self
4540 .direct_messaging
4541 .get_machine_id(agent_id)
4542 .await
4543 .ok_or_else(|| {
4544 tracing::warn!(
4545 target: "x0x::direct",
4546 stage = "send",
4547 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
4548 outcome = "err_agent_not_found",
4549 dur_ms = send_start.elapsed().as_millis() as u64,
4550 "no machine_id after connect_to_agent"
4551 );
4552 error::NetworkError::AgentNotFound(agent_id.0)
4553 })?;
4554 (id, "post_connect")
4555 }
4556 };
4557
4558 let ant_peer_id = ant_quic::PeerId(machine_id.0);
4565 let machine_prefix = network::hex_prefix(&machine_id.0, 4);
4566 let mut connected = network.is_connected(&ant_peer_id).await;
4567
4568 let mut repair_outcome: Option<&'static str> = None;
4577 if !connected && resolution != "post_connect" {
4578 const REPAIR_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
4579 let outcome = match tokio::time::timeout(
4580 REPAIR_TIMEOUT,
4581 network.ensure_peer_send_ready(&ant_peer_id),
4582 )
4583 .await
4584 {
4585 Ok(Ok(())) => "repaired",
4586 Ok(Err(_)) => "repair_failed",
4587 Err(_) => "repair_timeout",
4588 };
4589 repair_outcome = Some(outcome);
4590 tracing::debug!(
4591 target: "x0x::direct",
4592 stage = "send",
4593 %agent_prefix,
4594 %machine_prefix,
4595 resolution,
4596 outcome,
4597 "send-readiness repair on disconnected peer"
4598 );
4599 connected = network.is_connected(&ant_peer_id).await;
4600 }
4601
4602 if connected {
4612 if let Some(reason) = self.direct_messaging.lifecycle_block_reason(&machine_id) {
4613 tracing::warn!(
4614 target: "x0x::direct",
4615 stage = "send",
4616 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
4617 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
4618 resolution,
4619 ?repair_outcome,
4620 reason = %reason,
4621 "ignoring stale lifecycle block because ant-quic reports a live connection"
4622 );
4623 self.direct_messaging
4624 .record_lifecycle_established(machine_id, None);
4625 }
4626 } else {
4627 if let Some(reason) = self.direct_messaging.lifecycle_block_reason(&machine_id) {
4628 tracing::warn!(
4629 target: "x0x::direct",
4630 stage = "send",
4631 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
4632 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
4633 resolution,
4634 ?repair_outcome,
4635 outcome = "err_peer_disconnected",
4636 reason = %reason,
4637 dur_ms = send_start.elapsed().as_millis() as u64,
4638 "lifecycle watcher says peer is disconnected"
4639 );
4640 return Err(error::NetworkError::ConnectionFailed(format!(
4641 "peer disconnected: {reason}"
4642 )));
4643 }
4644 tracing::warn!(
4645 target: "x0x::direct",
4646 stage = "send",
4647 agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
4648 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
4649 resolution,
4650 ?repair_outcome,
4651 outcome = "err_not_connected",
4652 bytes,
4653 dur_ms = send_start.elapsed().as_millis() as u64,
4654 "machine_id resolved but peer not currently connected after repair attempt"
4655 );
4656 return Err(error::NetworkError::AgentNotConnected(agent_id.0));
4657 }
4658
4659 tracing::debug!(
4660 target: "dm.trace",
4661 stage = "path_chosen",
4662 sender = %hex::encode(self.identity.agent_id().as_bytes()),
4663 recipient = %hex::encode(agent_id.as_bytes()),
4664 machine_id = %hex::encode(machine_id.as_bytes()),
4665 path = target_path_label,
4666 bytes,
4667 digest = %digest,
4668 );
4669
4670 let send_result = if let Some(timeout) = receive_ack_timeout {
4674 let wire = direct::DirectMessaging::encode_message(&self.identity.agent_id(), payload)?;
4675 tracing::debug!(
4676 target: "dm.trace",
4677 stage = "wire_encoded",
4678 sender = %hex::encode(self.identity.agent_id().as_bytes()),
4679 recipient = %hex::encode(agent_id.as_bytes()),
4680 path = "raw_quic_acked",
4681 bytes = wire.len(),
4682 payload_bytes = bytes,
4683 digest = %digest,
4684 );
4685 self.send_ack_racing_replaced(
4692 network.as_ref(),
4693 ant_peer_id,
4694 machine_id,
4695 &wire,
4696 timeout,
4697 prefer_newest_grace,
4698 agent_id,
4699 )
4700 .await
4701 } else {
4702 tracing::debug!(
4703 target: "dm.trace",
4704 stage = "wire_encoded",
4705 sender = %hex::encode(self.identity.agent_id().as_bytes()),
4706 recipient = %hex::encode(agent_id.as_bytes()),
4707 path = "raw_quic",
4708 bytes,
4709 digest = %digest,
4710 );
4711 network
4712 .send_direct(&ant_peer_id, &self.identity.agent_id().0, payload)
4713 .await
4714 .map(|()| dm::DmPath::RawQuic)
4715 };
4716
4717 match send_result {
4718 Ok(path) => {
4719 let path_label = match path {
4720 dm::DmPath::Loopback => "loopback",
4721 dm::DmPath::RawQuic => "raw_quic",
4722 dm::DmPath::RawQuicAcked => "raw_quic_acked",
4723 dm::DmPath::GossipInbox => "gossip_inbox",
4724 dm::DmPath::Relayed { .. } => "relayed",
4725 };
4726 tracing::debug!(
4727 target: "dm.trace",
4728 stage = "outbound_send_returned_ok",
4729 sender = %hex::encode(self.identity.agent_id().as_bytes()),
4730 recipient = %hex::encode(agent_id.as_bytes()),
4731 machine_id = %hex::encode(machine_id.as_bytes()),
4732 path = path_label,
4733 bytes,
4734 digest = %digest,
4735 dur_ms = send_start.elapsed().as_millis() as u64,
4736 );
4737 tracing::info!(
4738 target: "x0x::direct",
4739 stage = "send",
4740 from = %self_prefix,
4741 to = %agent_prefix,
4742 %machine_prefix,
4743 resolution,
4744 bytes,
4745 dur_ms = send_start.elapsed().as_millis() as u64,
4746 outcome = "ok",
4747 path = ?path,
4748 "direct message sent"
4749 );
4750 Ok(path)
4751 }
4752 Err(e) => {
4753 tracing::warn!(
4754 target: "x0x::direct",
4755 stage = "send",
4756 from = %self_prefix,
4757 to = %agent_prefix,
4758 machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
4759 resolution,
4760 bytes,
4761 dur_ms = send_start.elapsed().as_millis() as u64,
4762 outcome = "err_transport",
4763 error = %e,
4764 "transport send_direct failed"
4765 );
4766 if receive_ack_timeout.is_some() && network.is_connected(&ant_peer_id).await {
4775 match network.disconnect(&ant_peer_id).await {
4776 Ok(()) => tracing::info!(
4777 target: "x0x::direct",
4778 stage = "send",
4779 to = %agent_prefix,
4780 %machine_prefix,
4781 "tore down zombie connection after acked send failure; retry will redial"
4782 ),
4783 Err(de) => tracing::debug!(
4784 target: "x0x::direct",
4785 stage = "send",
4786 to = %agent_prefix,
4787 %machine_prefix,
4788 error = %de,
4789 "failed to tear down zombie connection after acked send failure"
4790 ),
4791 }
4792 }
4793 Err(e)
4794 }
4795 }
4796 }
4797
4798 #[allow(clippy::too_many_arguments)]
4807 async fn send_ack_racing_replaced(
4808 &self,
4809 network: &network::NetworkNode,
4810 ant_peer_id: ant_quic::PeerId,
4811 machine_id: identity::MachineId,
4812 wire: &[u8],
4813 timeout: std::time::Duration,
4814 prefer_newest_grace: std::time::Duration,
4815 agent_id: &identity::AgentId,
4816 ) -> error::NetworkResult<dm::DmPath> {
4817 use tokio::sync::broadcast::error::RecvError;
4818 use tokio::sync::broadcast::error::TryRecvError as BroadcastTryRecvError;
4819
4820 let mut replaced_rx = self.direct_messaging.subscribe_lifecycle_replaced();
4823 let pre_send_generation = self.direct_messaging.current_generation(&machine_id);
4824
4825 let agent_prefix = network::hex_prefix(&agent_id.0, 4);
4826 let machine_prefix = network::hex_prefix(&machine_id.0, 4);
4827 let self_prefix = network::hex_prefix(&self.identity.agent_id().0, 4);
4828
4829 let ack_race_test_hook = self.direct_messaging.raw_quic_ack_race_test_hook();
4830
4831 let send_fut = async {
4833 if let Some(hook) = ack_race_test_hook.as_ref() {
4834 hook.notify_first_attempt_started();
4835 }
4836 let result = network
4837 .send_with_receive_ack(ant_peer_id, wire, timeout)
4838 .await;
4839 if let Some(hook) = ack_race_test_hook.as_ref() {
4840 hook.hold_first_attempt_result().await;
4841 }
4842 result
4843 };
4844 tokio::pin!(send_fut);
4845
4846 let superseded_to: u64;
4847
4848 loop {
4849 tokio::select! {
4850 biased;
4851 send_result = &mut send_fut => {
4852 match send_result {
4861 Some(Ok(())) => return Ok(dm::DmPath::RawQuicAcked),
4862 Some(Err(e)) => {
4863 let mut queued_supersede: Option<u64> = None;
4865 loop {
4866 match replaced_rx.try_recv() {
4867 Ok((m, gen)) if m == machine_id => {
4868 queued_supersede = Some(gen);
4869 }
4870 Ok(_) => continue,
4871 Err(BroadcastTryRecvError::Empty)
4872 | Err(BroadcastTryRecvError::Closed)
4873 | Err(BroadcastTryRecvError::Lagged(_)) => break,
4874 }
4875 }
4876 if let Some(gen) = queued_supersede {
4877 superseded_to = gen;
4878 break;
4879 }
4880 let reason = format!("send_with_receive_ack failed: {e}");
4881 return if Self::raw_quic_ack_receive_backpressured(&reason) {
4882 Err(error::NetworkError::RemoteReceiveBackpressured(reason))
4883 } else {
4884 Err(error::NetworkError::ConnectionFailed(reason))
4885 };
4886 }
4887 None => return Err(error::NetworkError::NodeCreation(
4888 "network node not initialized".to_string(),
4889 )),
4890 }
4891 }
4892 replaced = replaced_rx.recv() => {
4893 match replaced {
4894 Ok((m, gen)) if m == machine_id => {
4895 superseded_to = gen;
4896 break;
4897 }
4898 Ok(_) => continue,
4899 Err(RecvError::Lagged(_)) => {
4900 let mut found: Option<u64> = None;
4903 loop {
4904 match replaced_rx.try_recv() {
4905 Ok((m, gen)) if m == machine_id => {
4906 found = Some(gen);
4907 break;
4908 }
4909 Ok(_) => continue,
4910 Err(BroadcastTryRecvError::Empty)
4911 | Err(BroadcastTryRecvError::Closed)
4912 | Err(BroadcastTryRecvError::Lagged(_)) => break,
4913 }
4914 }
4915 if let Some(gen) = found {
4916 superseded_to = gen;
4917 break;
4918 }
4919 replaced_rx = self
4922 .direct_messaging
4923 .subscribe_lifecycle_replaced();
4924 continue;
4925 }
4926 Err(RecvError::Closed) => {
4927 let result = (&mut send_fut).await;
4931 return match result {
4932 Some(Ok(())) => Ok(dm::DmPath::RawQuicAcked),
4933 Some(Err(e)) => {
4934 let reason =
4935 format!("send_with_receive_ack failed: {e}");
4936 if Self::raw_quic_ack_receive_backpressured(&reason) {
4937 Err(error::NetworkError::RemoteReceiveBackpressured(reason))
4938 } else {
4939 Err(error::NetworkError::ConnectionFailed(reason))
4940 }
4941 }
4942 None => Err(error::NetworkError::NodeCreation(
4943 "network node not initialized".to_string(),
4944 )),
4945 };
4946 }
4947 }
4948 }
4949 }
4950 }
4951
4952 let new_generation = superseded_to;
4959 if let Some(hook) = ack_race_test_hook.as_ref() {
4960 hook.notify_replaced_short_circuit();
4961 }
4962 tracing::debug!(
4963 target: "dm.trace",
4964 stage = "raw_quic_ack_replaced_short_circuit",
4965 from = %self_prefix,
4966 to = %agent_prefix,
4967 %machine_prefix,
4968 pre_send_generation = ?pre_send_generation,
4969 new_generation,
4970 grace_ms = prefer_newest_grace.as_millis() as u64,
4971 "X0X-0053: same-peer Replaced fired during in-flight send_with_receive_ack; abandoning and reissuing",
4972 );
4973
4974 if !prefer_newest_grace.is_zero() {
4978 let grace_deadline = tokio::time::Instant::now() + prefer_newest_grace;
4979 const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(20);
4980 while tokio::time::Instant::now() < grace_deadline {
4981 if network.is_connected(&ant_peer_id).await {
4982 break;
4983 }
4984 tokio::time::sleep(POLL_INTERVAL).await;
4985 }
4986 }
4987
4988 if !network.is_connected(&ant_peer_id).await {
4989 return Err(error::NetworkError::AgentNotConnected(agent_id.0));
4990 }
4991
4992 match network
4995 .send_with_receive_ack(ant_peer_id, wire, timeout)
4996 .await
4997 {
4998 Some(Ok(())) => Ok(dm::DmPath::RawQuicAcked),
4999 Some(Err(e)) => {
5000 let reason = format!("send_with_receive_ack failed: {e}");
5001 if Self::raw_quic_ack_receive_backpressured(&reason) {
5002 Err(error::NetworkError::RemoteReceiveBackpressured(reason))
5003 } else {
5004 Err(error::NetworkError::ConnectionFailed(reason))
5005 }
5006 }
5007 None => Err(error::NetworkError::NodeCreation(
5008 "network node not initialized".to_string(),
5009 )),
5010 }
5011 }
5012
5013 fn raw_quic_error_should_stop_fallback(
5014 err: &error::NetworkError,
5015 gossip_available: bool,
5016 ) -> bool {
5017 match err {
5018 error::NetworkError::PayloadTooLarge { .. } | error::NetworkError::NodeCreation(_) => {
5021 true
5022 }
5023 error::NetworkError::ConnectionFailed(reason)
5024 if reason.starts_with("peer disconnected:")
5025 || reason.starts_with("send_with_receive_ack failed:") =>
5026 {
5027 !gossip_available
5028 }
5029 error::NetworkError::RemoteReceiveBackpressured(_) => !gossip_available,
5030 error::NetworkError::ConnectionClosed(_)
5031 | error::NetworkError::ConnectionReset(_)
5032 | error::NetworkError::NotConnected(_) => !gossip_available,
5033 _ => false,
5034 }
5035 }
5036
5037 fn raw_quic_ack_receive_backpressured(reason: &str) -> bool {
5038 reason.contains("Remote receive pipeline rejected payload: Backpressured")
5039 }
5040
5041 fn map_raw_quic_dm_error(err: error::NetworkError) -> dm::DmError {
5042 match err {
5043 error::NetworkError::AgentNotFound(_) => {
5044 dm::DmError::RecipientKeyUnavailable(err.to_string())
5045 }
5046 error::NetworkError::AgentNotConnected(_)
5047 | error::NetworkError::NotConnected(_)
5048 | error::NetworkError::ConnectionClosed(_)
5049 | error::NetworkError::ConnectionReset(_) => dm::DmError::PeerDisconnected {
5050 reason: err.to_string(),
5051 },
5052 error::NetworkError::ConnectionFailed(reason)
5053 if reason.starts_with("peer disconnected:")
5054 || reason.starts_with("send_with_receive_ack failed:") =>
5055 {
5056 dm::DmError::PeerDisconnected { reason }
5057 }
5058 error::NetworkError::RemoteReceiveBackpressured(reason) => {
5059 dm::DmError::ReceiverBackpressured { reason }
5060 }
5061 error::NetworkError::PayloadTooLarge { size, max } => {
5062 dm::DmError::PayloadTooLarge { len: size, max }
5063 }
5064 error::NetworkError::NodeCreation(reason) => dm::DmError::NoConnectivity(reason),
5065 other => dm::DmError::PublishFailed(other.to_string()),
5066 }
5067 }
5068
5069 pub async fn recv_direct(&self) -> Option<direct::DirectMessage> {
5094 self.recv_direct_inner().await
5095 }
5096
5097 pub async fn recv_direct_annotated(&self) -> Option<direct::DirectMessage> {
5123 self.recv_direct_inner().await
5124 }
5125
5126 async fn recv_direct_inner(&self) -> Option<direct::DirectMessage> {
5133 self.direct_messaging.recv().await
5134 }
5135
5136 pub fn subscribe_direct(&self) -> direct::DirectMessageReceiver {
5152 self.direct_messaging.subscribe()
5153 }
5154
5155 pub fn direct_messaging(&self) -> &std::sync::Arc<direct::DirectMessaging> {
5159 &self.direct_messaging
5160 }
5161
5162 pub async fn is_agent_connected(&self, agent_id: &identity::AgentId) -> bool {
5172 let Some(network) = &self.network else {
5173 return false;
5174 };
5175
5176 let machine_id = {
5178 let cache = self.identity_discovery_cache.read().await;
5179 cache.get(agent_id).map(|d| d.machine_id)
5180 };
5181
5182 match machine_id {
5183 Some(mid) => {
5184 let ant_peer_id = ant_quic::PeerId(mid.0);
5185 network.is_connected(&ant_peer_id).await
5186 }
5187 None => false,
5188 }
5189 }
5190
5191 pub async fn connected_agents(&self) -> Vec<identity::AgentId> {
5196 let Some(network) = &self.network else {
5197 return Vec::new();
5198 };
5199
5200 let connected_peers = network.connected_peers().await;
5201 let cache = self.identity_discovery_cache.read().await;
5202
5203 cache
5205 .values()
5206 .filter(|agent| {
5207 let ant_peer_id = ant_quic::PeerId(agent.machine_id.0);
5208 connected_peers.contains(&ant_peer_id)
5209 })
5210 .map(|agent| agent.agent_id)
5211 .collect()
5212 }
5213
5214 pub fn set_contacts(&self, store: std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>) {
5222 if let Some(runtime) = &self.gossip_runtime {
5223 let pubsub = runtime.pubsub();
5224 pubsub.set_contacts(store);
5225 pubsub.set_revocation_set(self.revocation_set());
5230 }
5231 }
5232
5233 pub async fn announce_identity(
5251 &self,
5252 include_user_identity: bool,
5253 human_consent: bool,
5254 ) -> error::Result<()> {
5255 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
5256 error::IdentityError::Storage(std::io::Error::other(
5257 "gossip runtime not initialized - configure agent with network first",
5258 ))
5259 })?;
5260
5261 self.start_identity_listener().await?;
5262
5263 let network_status = if let Some(network) = self.network.as_ref() {
5264 network.node_status().await
5265 } else {
5266 None
5267 };
5268 let assist_snapshot = network_status
5269 .as_ref()
5270 .map(AnnouncementAssistSnapshot::from_node_status)
5271 .unwrap_or_default();
5272
5273 let mut addresses = if let Some(network) = self.network.as_ref() {
5275 match network_status.as_ref() {
5276 Some(status) if !status.external_addrs.is_empty() => status.external_addrs.clone(),
5277 _ => match network.routable_addr().await {
5278 Some(addr) => vec![addr],
5279 None => self.announcement_addresses(),
5280 },
5281 }
5282 } else {
5283 self.announcement_addresses()
5284 };
5285 let bind_port = if let Some(network) = self.network.as_ref() {
5294 network.bound_addr().await.map(|a| a.port()).unwrap_or(5483)
5295 } else {
5296 5483
5297 };
5298
5299 if let Ok(sock) = std::net::UdpSocket::bind("[::]:0") {
5301 if sock.connect("[2001:4860:4860::8888]:80").is_ok() {
5302 if let Ok(local) = sock.local_addr() {
5303 if let std::net::IpAddr::V6(v6) = local.ip() {
5304 let segs = v6.segments();
5305 let is_global = (segs[0] & 0xffc0) != 0xfe80
5306 && (segs[0] & 0xff00) != 0xfd00
5307 && !v6.is_loopback();
5308 if is_global {
5309 let v6_addr =
5310 std::net::SocketAddr::new(std::net::IpAddr::V6(v6), bind_port);
5311 if !addresses.contains(&v6_addr) {
5312 addresses.push(v6_addr);
5313 }
5314 }
5315 }
5316 }
5317 }
5318 }
5319
5320 for addr in collect_local_interface_addrs(bind_port) {
5321 if !addresses.contains(&addr) {
5322 addresses.push(addr);
5323 }
5324 }
5325
5326 let allow_local_scope = self
5327 .network
5328 .as_ref()
5329 .is_some_and(|network| allow_local_discovery_addresses(network.config()));
5330 addresses = filter_discovery_announcement_addrs(addresses, allow_local_scope);
5334
5335 let (reachable_via, relay_candidates) = if assist_snapshot.can_receive_direct == Some(true)
5338 {
5339 (Vec::new(), Vec::new())
5340 } else if let Some(network) = self.network.as_ref() {
5341 collect_coordinator_hints(
5342 network.as_ref(),
5343 &self.machine_discovery_cache,
5344 self.machine_id(),
5345 )
5346 .await
5347 } else {
5348 (Vec::new(), Vec::new())
5349 };
5350
5351 let announcement =
5352 self.build_identity_announcement_with_addrs(IdentityAnnouncementBuildOptions {
5353 include_user_identity,
5354 human_consent,
5355 addresses,
5356 assist_snapshot: Some(&assist_snapshot),
5357 reachable_via,
5358 relay_candidates,
5359 allow_local_scope,
5360 })?;
5361 tracing::debug!(
5362 target: "x0x::discovery",
5363 announcement_kind = "explicit",
5364 machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
5365 addr_total = announcement.addresses.len(),
5366 nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
5367 can_receive_direct = ?announcement.can_receive_direct,
5368 relay_capable = ?announcement.is_relay,
5369 coordinator_capable = ?announcement.is_coordinator,
5370 relay_active = ?assist_snapshot.relay_active,
5371 coordinator_active = ?assist_snapshot.coordinator_active,
5372 "publishing identity announcement"
5373 );
5374
5375 let machine_announcement = build_machine_announcement_for_identity(
5376 &self.identity,
5377 announcement.addresses.clone(),
5378 announcement.announced_at,
5379 Some(&assist_snapshot),
5380 announcement.reachable_via.clone(),
5381 announcement.relay_candidates.clone(),
5382 allow_local_scope,
5383 )?;
5384 tracing::debug!(
5385 target: "x0x::discovery",
5386 announcement_kind = "machine_explicit",
5387 machine_prefix = %network::hex_prefix(&machine_announcement.machine_id.0, 4),
5388 addr_total = machine_announcement.addresses.len(),
5389 nat_type = machine_announcement.nat_type.as_deref().unwrap_or("unknown"),
5390 can_receive_direct = ?machine_announcement.can_receive_direct,
5391 relay_capable = ?machine_announcement.is_relay,
5392 coordinator_capable = ?machine_announcement.is_coordinator,
5393 reachable_via_count = machine_announcement.reachable_via.len(),
5394 relay_candidate_count = machine_announcement.relay_candidates.len(),
5395 "publishing machine announcement"
5396 );
5397 let machine_payload =
5398 bytes::Bytes::from(bincode::serialize(&machine_announcement).map_err(|e| {
5399 error::IdentityError::Serialization(format!(
5400 "failed to serialize machine announcement: {e}"
5401 ))
5402 })?);
5403 runtime
5404 .pubsub()
5405 .publish(
5406 shard_topic_for_machine(&machine_announcement.machine_id),
5407 machine_payload.clone(),
5408 )
5409 .await
5410 .map_err(|e| {
5411 error::IdentityError::Storage(std::io::Error::other(format!(
5412 "failed to publish machine announcement to shard topic: {e}"
5413 )))
5414 })?;
5415 runtime
5416 .pubsub()
5417 .publish(MACHINE_ANNOUNCE_TOPIC.to_string(), machine_payload)
5418 .await
5419 .map_err(|e| {
5420 error::IdentityError::Storage(std::io::Error::other(format!(
5421 "failed to publish machine announcement: {e}"
5422 )))
5423 })?;
5424
5425 let encoded = serialize_identity_announcement(&announcement).map_err(|e| {
5426 error::IdentityError::Serialization(format!(
5427 "failed to serialize identity announcement: {e}"
5428 ))
5429 })?;
5430
5431 let payload = bytes::Bytes::from(encoded);
5432
5433 let shard_topic = shard_topic_for_agent(&announcement.agent_id);
5435 runtime
5436 .pubsub()
5437 .publish(shard_topic, payload.clone())
5438 .await
5439 .map_err(|e| {
5440 error::IdentityError::Storage(std::io::Error::other(format!(
5441 "failed to publish identity announcement to shard topic: {e}"
5442 )))
5443 })?;
5444
5445 runtime
5447 .pubsub()
5448 .publish(IDENTITY_ANNOUNCE_TOPIC.to_string(), payload)
5449 .await
5450 .map_err(|e| {
5451 error::IdentityError::Storage(std::io::Error::other(format!(
5452 "failed to publish identity announcement: {e}"
5453 )))
5454 })?;
5455
5456 let now = Self::unix_timestamp_secs();
5457 upsert_discovered_machine(
5458 &self.machine_discovery_cache,
5459 DiscoveredMachine::from_machine_announcement(
5460 &machine_announcement,
5461 machine_announcement.addresses.clone(),
5462 now,
5463 ),
5464 )
5465 .await;
5466 let discovered_agent = DiscoveredAgent {
5467 agent_id: announcement.agent_id,
5468 machine_id: announcement.machine_id,
5469 user_id: announcement.user_id,
5470 addresses: announcement.addresses.clone(),
5471 announced_at: announcement.announced_at,
5472 last_seen: now,
5473 machine_public_key: announcement.machine_public_key.clone(),
5474 nat_type: announcement.nat_type.clone(),
5475 can_receive_direct: announcement.can_receive_direct,
5476 is_relay: announcement.is_relay,
5477 is_coordinator: announcement.is_coordinator,
5478 reachable_via: announcement.reachable_via.clone(),
5479 relay_candidates: announcement.relay_candidates.clone(),
5480 cert_not_after: None,
5481 agent_certificate: None,
5482 agent_public_key: announcement.agent_public_key.clone(),
5483 };
5484 upsert_discovered_machine_from_agent(&self.machine_discovery_cache, &discovered_agent)
5485 .await;
5486 upsert_discovered_agent(&self.identity_discovery_cache, discovered_agent).await;
5487
5488 if include_user_identity && human_consent {
5491 self.user_identity_consented
5492 .store(true, std::sync::atomic::Ordering::Release);
5493 }
5494
5495 Ok(())
5496 }
5497
5498 pub async fn discovered_agents(&self) -> error::Result<Vec<DiscoveredAgent>> {
5504 self.start_identity_listener().await?;
5505 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
5506 let mut agents: Vec<_> = self
5507 .identity_discovery_cache
5508 .read()
5509 .await
5510 .values()
5511 .filter(|a| discovery_record_is_live(a.announced_at, a.last_seen, cutoff))
5512 .cloned()
5513 .collect();
5514 agents.sort_by_key(|a| a.agent_id.0);
5515 Ok(agents)
5516 }
5517
5518 pub async fn online_agents(&self) -> error::Result<Vec<DiscoveredAgent>> {
5529 self.start_identity_listener().await?;
5530 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
5531 let cache = self.identity_discovery_cache.read().await;
5532 let mut seen = std::collections::HashSet::new();
5533 let mut agents = Vec::new();
5534
5535 for agent in cache
5536 .values()
5537 .filter(|agent| discovery_record_is_live(agent.announced_at, agent.last_seen, cutoff))
5538 {
5539 if seen.insert(agent.agent_id) {
5540 agents.push(agent.clone());
5541 }
5542 }
5543
5544 if let Some(ref pw) = self.presence {
5545 let records = pw
5546 .manager()
5547 .get_group_presence(crate::presence::global_presence_topic())
5548 .await;
5549 for (peer_id, record) in records {
5550 if let Some(agent) =
5551 crate::presence::presence_record_to_discovered_agent(peer_id, &record, &cache)
5552 {
5553 if seen.insert(agent.agent_id) {
5554 agents.push(agent);
5555 }
5556 }
5557 }
5558 }
5559
5560 agents.sort_by_key(|a| a.agent_id.0);
5561 Ok(agents)
5562 }
5563
5564 pub async fn discovered_agents_unfiltered(&self) -> error::Result<Vec<DiscoveredAgent>> {
5575 self.start_identity_listener().await?;
5576 let mut agents: Vec<_> = self
5577 .identity_discovery_cache
5578 .read()
5579 .await
5580 .values()
5581 .cloned()
5582 .collect();
5583 agents.sort_by_key(|a| a.agent_id.0);
5584 Ok(agents)
5585 }
5586
5587 pub async fn discovered_agent(
5593 &self,
5594 agent_id: identity::AgentId,
5595 ) -> error::Result<Option<DiscoveredAgent>> {
5596 self.start_identity_listener().await?;
5597 Ok(self
5598 .identity_discovery_cache
5599 .read()
5600 .await
5601 .get(&agent_id)
5602 .cloned())
5603 }
5604
5605 pub async fn discovered_machines(&self) -> error::Result<Vec<DiscoveredMachine>> {
5616 self.start_identity_listener().await?;
5617 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
5618 let mut machines: Vec<_> = self
5619 .machine_discovery_cache
5620 .read()
5621 .await
5622 .values()
5623 .filter(|m| discovery_record_is_live(m.announced_at, m.last_seen, cutoff))
5624 .cloned()
5625 .collect();
5626 machines.sort_by_key(|m| m.machine_id.0);
5627 Ok(machines)
5628 }
5629
5630 pub async fn discovered_machines_unfiltered(&self) -> error::Result<Vec<DiscoveredMachine>> {
5641 self.start_identity_listener().await?;
5642 let mut machines: Vec<_> = self
5643 .machine_discovery_cache
5644 .read()
5645 .await
5646 .values()
5647 .cloned()
5648 .collect();
5649 machines.sort_by_key(|m| m.machine_id.0);
5650 Ok(machines)
5651 }
5652
5653 pub async fn discovered_machine(
5659 &self,
5660 machine_id: identity::MachineId,
5661 ) -> error::Result<Option<DiscoveredMachine>> {
5662 self.start_identity_listener().await?;
5663 Ok(self
5664 .machine_discovery_cache
5665 .read()
5666 .await
5667 .get(&machine_id)
5668 .cloned())
5669 }
5670
5671 pub async fn machine_for_agent(
5677 &self,
5678 agent_id: identity::AgentId,
5679 ) -> error::Result<Option<DiscoveredMachine>> {
5680 self.start_identity_listener().await?;
5681 let machine_id = {
5682 let agents = self.identity_discovery_cache.read().await;
5683 agents.get(&agent_id).map(|agent| agent.machine_id)
5684 };
5685 let Some(machine_id) = machine_id else {
5686 return Ok(None);
5687 };
5688 Ok(self
5689 .machine_discovery_cache
5690 .read()
5691 .await
5692 .get(&machine_id)
5693 .cloned())
5694 }
5695
5696 pub async fn find_machines_by_user(
5702 &self,
5703 user_id: identity::UserId,
5704 ) -> error::Result<Vec<DiscoveredMachine>> {
5705 self.start_identity_listener().await?;
5706 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
5707 let mut machines: Vec<_> = self
5708 .machine_discovery_cache
5709 .read()
5710 .await
5711 .values()
5712 .filter(|m| {
5713 discovery_record_is_live(m.announced_at, m.last_seen, cutoff)
5714 && m.user_ids.contains(&user_id)
5715 })
5716 .cloned()
5717 .collect();
5718 machines.sort_by_key(|m| m.machine_id.0);
5719 Ok(machines)
5720 }
5721
5722 pub async fn announce_user_identity(&self, human_consent: bool) -> error::Result<()> {
5735 if !human_consent {
5736 return Err(error::IdentityError::Storage(std::io::Error::other(
5737 "user announcement requires explicit human consent — set human_consent: true",
5738 )));
5739 }
5740 let user_kp = self.identity.user_keypair().ok_or_else(|| {
5741 error::IdentityError::Storage(std::io::Error::other(
5742 "user announcement requested but no user identity is configured",
5743 ))
5744 })?;
5745 let own_cert = self.identity.agent_certificate().cloned().ok_or_else(|| {
5746 error::IdentityError::Storage(std::io::Error::other(
5747 "user announcement requested but agent certificate is missing",
5748 ))
5749 })?;
5750 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
5751 error::IdentityError::Storage(std::io::Error::other(
5752 "gossip runtime not initialized - configure agent with network first",
5753 ))
5754 })?;
5755 self.start_identity_listener().await?;
5756
5757 let announced_at = Self::unix_timestamp_secs();
5758 let announcement = UserAnnouncement::sign(user_kp, vec![own_cert], announced_at)?;
5759 let payload = bytes::Bytes::from(bincode::serialize(&announcement).map_err(|e| {
5760 error::IdentityError::Serialization(format!(
5761 "failed to serialize user announcement: {e}"
5762 ))
5763 })?);
5764 runtime
5765 .pubsub()
5766 .publish(shard_topic_for_user(&announcement.user_id), payload.clone())
5767 .await
5768 .map_err(|e| {
5769 error::IdentityError::Storage(std::io::Error::other(format!(
5770 "failed to publish user announcement to shard topic: {e}"
5771 )))
5772 })?;
5773 runtime
5774 .pubsub()
5775 .publish(USER_ANNOUNCE_TOPIC.to_string(), payload)
5776 .await
5777 .map_err(|e| {
5778 error::IdentityError::Storage(std::io::Error::other(format!(
5779 "failed to publish user announcement: {e}"
5780 )))
5781 })?;
5782
5783 let now = Self::unix_timestamp_secs();
5784 let incoming = DiscoveredUser::from_announcement(&announcement, now);
5785 self.user_discovery_cache
5786 .write()
5787 .await
5788 .insert(incoming.user_id, incoming);
5789 Ok(())
5790 }
5791
5792 pub async fn discovered_user(
5801 &self,
5802 user_id: identity::UserId,
5803 ) -> error::Result<Option<DiscoveredUser>> {
5804 self.start_identity_listener().await?;
5805 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
5806 Ok(self
5807 .user_discovery_cache
5808 .read()
5809 .await
5810 .get(&user_id)
5811 .filter(|u| discovery_record_is_live(u.announced_at, u.last_seen, cutoff))
5812 .cloned())
5813 }
5814
5815 pub async fn discovered_users(&self) -> error::Result<Vec<DiscoveredUser>> {
5821 self.start_identity_listener().await?;
5822 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
5823 let mut users: Vec<_> = self
5824 .user_discovery_cache
5825 .read()
5826 .await
5827 .values()
5828 .filter(|u| discovery_record_is_live(u.announced_at, u.last_seen, cutoff))
5829 .cloned()
5830 .collect();
5831 users.sort_by_key(|u| u.user_id.0);
5832 Ok(users)
5833 }
5834
5835 #[must_use]
5842 pub async fn discovery_cache_entry_counts(&self) -> (usize, usize, usize) {
5843 let id = self.identity_discovery_cache.read().await.len();
5844 let mach = self.machine_discovery_cache.read().await.len();
5845 let usr = self.user_discovery_cache.read().await.len();
5846 (id, mach, usr)
5847 }
5848
5849 async fn start_identity_listener(&self) -> error::Result<()> {
5850 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
5851 error::IdentityError::Storage(std::io::Error::other(
5852 "gossip runtime not initialized - configure agent with network first",
5853 ))
5854 })?;
5855
5856 if self
5857 .identity_listener_started
5858 .swap(true, std::sync::atomic::Ordering::AcqRel)
5859 {
5860 return Ok(());
5861 }
5862
5863 let mut sub_legacy = runtime
5864 .pubsub()
5865 .subscribe(IDENTITY_ANNOUNCE_TOPIC.to_string())
5866 .await;
5867 let own_shard_topic = shard_topic_for_agent(&self.agent_id());
5868 let mut sub_shard = runtime.pubsub().subscribe(own_shard_topic).await;
5869 let mut sub_machine_legacy = runtime
5870 .pubsub()
5871 .subscribe(MACHINE_ANNOUNCE_TOPIC.to_string())
5872 .await;
5873 let own_machine_shard_topic = shard_topic_for_machine(&self.machine_id());
5874 let mut sub_machine_shard = runtime.pubsub().subscribe(own_machine_shard_topic).await;
5875 let mut sub_user_legacy = runtime
5876 .pubsub()
5877 .subscribe(USER_ANNOUNCE_TOPIC.to_string())
5878 .await;
5879 let mut sub_user_shard = match self.user_id() {
5882 Some(uid) => Some(runtime.pubsub().subscribe(shard_topic_for_user(&uid)).await),
5883 None => None,
5884 };
5885 let cache = std::sync::Arc::clone(&self.identity_discovery_cache);
5886 let authenticated_machine_bindings =
5887 std::sync::Arc::clone(&self.authenticated_machine_bindings);
5888 let machine_cache = std::sync::Arc::clone(&self.machine_discovery_cache);
5889 let user_cache = std::sync::Arc::clone(&self.user_discovery_cache);
5890 let bootstrap_cache = self.bootstrap_cache.clone();
5891 let contact_store = std::sync::Arc::clone(&self.contact_store);
5892 let direct_messaging = std::sync::Arc::clone(&self.direct_messaging);
5893 let network = self.network.as_ref().map(std::sync::Arc::clone);
5894 let allow_local_scope = network
5895 .as_ref()
5896 .is_some_and(|network| allow_local_discovery_addresses(network.config()));
5897 let own_agent_id = self.agent_id();
5898 let own_machine_id = self.machine_id();
5899 let own_user_id = self.user_id();
5900 let rebroadcast_pubsub = std::sync::Arc::clone(runtime.pubsub());
5901 let token = self.shutdown_token.clone();
5902 let mut sub_revocation = runtime
5904 .pubsub()
5905 .subscribe(REVOCATION_TOPIC.to_string())
5906 .await;
5907 let revocation_set = std::sync::Arc::clone(&self.revocation_set);
5908 let identity_dir_for_listener = self.identity_dir.clone();
5909 let contact_store_for_evict = std::sync::Arc::clone(&self.contact_store);
5910
5911 self.spawn_tracked(async move {
5912 enum DiscoveryMessage {
5913 Identity(crate::gossip::PubSubMessage),
5914 Machine(crate::gossip::PubSubMessage),
5915 User(crate::gossip::PubSubMessage),
5916 Revocation(crate::gossip::PubSubMessage),
5917 }
5918
5919 let mut auto_connect_attempted = std::collections::HashSet::<identity::AgentId>::new();
5922
5923 let mut rebroadcast_state: std::collections::HashMap<
5929 (identity::AgentId, u64),
5930 std::time::Instant,
5931 > = std::collections::HashMap::new();
5932 let mut machine_rebroadcast_state: std::collections::HashMap<
5933 (identity::MachineId, u64),
5934 std::time::Instant,
5935 > = std::collections::HashMap::new();
5936 let mut seen_identity_payloads: std::collections::HashMap<
5937 blake3::Hash,
5938 std::time::Instant,
5939 > = std::collections::HashMap::new();
5940 let mut seen_machine_payloads: std::collections::HashMap<
5941 blake3::Hash,
5942 std::time::Instant,
5943 > = std::collections::HashMap::new();
5944 let mut user_rebroadcast_state: std::collections::HashMap<
5945 (identity::UserId, u64),
5946 std::time::Instant,
5947 > = std::collections::HashMap::new();
5948 const VERIFIED_PAYLOAD_TTL: std::time::Duration = std::time::Duration::from_secs(60);
5949
5950 let has_recent_verified_payload =
5951 |seen: &std::collections::HashMap<blake3::Hash, std::time::Instant>,
5952 payload: &[u8]| {
5953 let key = blake3::hash(payload);
5954 matches!(seen.get(&key), Some(last) if last.elapsed() < VERIFIED_PAYLOAD_TTL)
5955 };
5956 let remember_verified_payload =
5957 |seen: &mut std::collections::HashMap<blake3::Hash, std::time::Instant>,
5958 payload: &[u8]| {
5959 let now = std::time::Instant::now();
5960 seen.insert(blake3::hash(payload), now);
5961 if seen.len() > 4096 {
5962 let cutoff = now - VERIFIED_PAYLOAD_TTL;
5963 seen.retain(|_, t| *t >= cutoff);
5964 }
5965 };
5966
5967 loop {
5968 let msg = tokio::select! {
5973 Some(m) = sub_legacy.recv() => DiscoveryMessage::Identity(m),
5974 Some(m) = sub_shard.recv() => DiscoveryMessage::Identity(m),
5975 Some(m) = sub_machine_legacy.recv() => DiscoveryMessage::Machine(m),
5976 Some(m) = sub_machine_shard.recv() => DiscoveryMessage::Machine(m),
5977 Some(m) = sub_user_legacy.recv() => DiscoveryMessage::User(m),
5978 Some(m) = async {
5979 match sub_user_shard.as_mut() {
5980 Some(s) => s.recv().await,
5981 None => std::future::pending().await,
5982 }
5983 } => DiscoveryMessage::User(m),
5984 Some(m) = sub_revocation.recv() => DiscoveryMessage::Revocation(m),
5985 _ = token.cancelled() => break,
5995 else => break,
5996 };
5997 let msg = match msg {
5998 DiscoveryMessage::Machine(msg) => {
5999 let raw_payload = msg.payload.clone();
6000 let already_verified =
6001 has_recent_verified_payload(&seen_machine_payloads, &raw_payload);
6002 let announcement = match deserialize_machine_announcement(&raw_payload) {
6003 Ok(a) => a,
6004 Err(e) => {
6005 tracing::debug!(
6006 "Ignoring invalid machine announcement payload: {}",
6007 e
6008 );
6009 continue;
6010 }
6011 };
6012
6013 if !already_verified {
6014 if let Err(e) = announcement.verify() {
6015 tracing::warn!("Ignoring unverifiable machine announcement: {}", e);
6016 continue;
6017 }
6018 remember_verified_payload(&mut seen_machine_payloads, &raw_payload);
6019 }
6020
6021 let now = std::time::SystemTime::now()
6022 .duration_since(std::time::UNIX_EPOCH)
6023 .map_or(0, |d| d.as_secs());
6024
6025 let bootstrap_addresses = filter_publicly_advertisable_addrs(
6026 announcement.addresses.iter().copied(),
6027 );
6028 if !bootstrap_addresses.is_empty() {
6029 if let Some(ref bc) = &bootstrap_cache {
6030 let peer_id = ant_quic::PeerId(announcement.machine_id.0);
6031 bc.add_from_connection(peer_id, bootstrap_addresses.clone(), None)
6032 .await;
6033 }
6034 }
6035
6036 let discovery_addresses = filter_discovery_announcement_addrs(
6037 announcement.addresses.iter().copied(),
6038 allow_local_scope,
6039 );
6040 let filtered_addr_count = discovery_addresses.len();
6041 upsert_discovered_machine(
6042 &machine_cache,
6043 DiscoveredMachine::from_machine_announcement(
6044 &announcement,
6045 discovery_addresses,
6046 now,
6047 ),
6048 )
6049 .await;
6050 tracing::debug!(
6051 target: "x0x::discovery",
6052 announcement_kind = "machine_received",
6053 machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
6054 addr_total = announcement.addresses.len(),
6055 filtered_addr_count,
6056 nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
6057 can_receive_direct = ?announcement.can_receive_direct,
6058 relay_capable = ?announcement.is_relay,
6059 coordinator_capable = ?announcement.is_coordinator,
6060 "cached verified machine announcement"
6061 );
6062
6063 if announcement.machine_id != own_machine_id {
6064 let key = (announcement.machine_id, announcement.announced_at);
6065 if should_rebroadcast_discovery_once(
6066 &mut machine_rebroadcast_state,
6067 key,
6068 std::time::Instant::now(),
6069 ) {
6070 let pubsub = std::sync::Arc::clone(&rebroadcast_pubsub);
6071 tokio::spawn(async move {
6072 if let Err(e) = pubsub
6073 .publish(MACHINE_ANNOUNCE_TOPIC.to_string(), raw_payload)
6074 .await
6075 {
6076 tracing::debug!(
6077 "machine announcement re-broadcast failed: {e}"
6078 );
6079 }
6080 });
6081 }
6082 }
6083 continue;
6084 }
6085 DiscoveryMessage::User(msg) => {
6086 let raw_payload = msg.payload.clone();
6087 let announcement = match deserialize_user_announcement(&raw_payload) {
6088 Ok(a) => a,
6089 Err(e) => {
6090 tracing::debug!(
6091 "Ignoring invalid user announcement payload: {}",
6092 e
6093 );
6094 continue;
6095 }
6096 };
6097 if let Err(e) = announcement.verify() {
6098 tracing::warn!("Ignoring unverifiable user announcement: {}", e);
6099 continue;
6100 }
6101 let now = std::time::SystemTime::now()
6102 .duration_since(std::time::UNIX_EPOCH)
6103 .map_or(0, |d| d.as_secs());
6104 let incoming = DiscoveredUser::from_announcement(&announcement, now);
6105 {
6106 let mut cache = user_cache.write().await;
6107 match cache.get_mut(&incoming.user_id) {
6108 Some(existing) if incoming.announced_at < existing.announced_at => {
6109 }
6111 Some(existing) => {
6112 existing.user_public_key = incoming.user_public_key;
6113 existing.agent_certificates = incoming.agent_certificates;
6114 existing.agent_ids = incoming.agent_ids;
6115 existing.announced_at = incoming.announced_at;
6116 existing.last_seen = now;
6117 }
6118 None => {
6119 cache.insert(incoming.user_id, incoming);
6120 }
6121 }
6122 }
6123 tracing::debug!(
6124 target: "x0x::discovery",
6125 announcement_kind = "user_received",
6126 user_prefix = %network::hex_prefix(&announcement.user_id.0, 4),
6127 agent_count = announcement.agent_certificates.len(),
6128 "cached verified user announcement"
6129 );
6130 if Some(announcement.user_id) != own_user_id {
6133 let key = (announcement.user_id, announcement.announced_at);
6134 if should_rebroadcast_discovery_once(
6135 &mut user_rebroadcast_state,
6136 key,
6137 std::time::Instant::now(),
6138 ) {
6139 let pubsub = std::sync::Arc::clone(&rebroadcast_pubsub);
6140 tokio::spawn(async move {
6141 if let Err(e) = pubsub
6142 .publish(USER_ANNOUNCE_TOPIC.to_string(), raw_payload)
6143 .await
6144 {
6145 tracing::debug!(
6146 "user announcement re-broadcast failed: {e}"
6147 );
6148 }
6149 });
6150 }
6151 }
6152 continue;
6153 }
6154 DiscoveryMessage::Identity(msg) => msg,
6155 DiscoveryMessage::Revocation(msg) => {
6156 const MAX_REVOCATION_PAYLOAD_BYTES: usize = 2 * 1024 * 1024;
6160 if msg.payload.len() > MAX_REVOCATION_PAYLOAD_BYTES {
6161 tracing::debug!(
6162 "ignoring oversized revocation payload ({} bytes)",
6163 msg.payload.len()
6164 );
6165 continue;
6166 }
6167 let records: Vec<revocation::RevocationRecord> =
6168 match bincode::deserialize(&msg.payload) {
6169 Ok(r) => r,
6170 Err(e) => {
6171 tracing::debug!("ignoring invalid revocation payload: {e}");
6172 continue;
6173 }
6174 };
6175 let mut newly_inserted = Vec::new();
6176 let subject_certs = collect_subject_certs(&*cache.read().await);
6187 {
6188 let mut set = revocation_set.write().await;
6189 for record in records {
6190 if set.contains_hash(&record.record_hash()) {
6191 continue; }
6193 let subject_cert = match &record.subject {
6194 revocation::RevokedSubject::Agent(agent_id) => {
6195 subject_certs.get(agent_id)
6196 }
6197 _ => None,
6198 };
6199 match set.verify_and_insert(record.clone(), subject_cert) {
6200 Ok(true) => newly_inserted.push(record),
6201 Ok(false) => {} Err(e) => {
6203 tracing::debug!(
6204 "revocation record rejected: {e}"
6205 );
6206 }
6207 }
6208 }
6209 }
6210 if !newly_inserted.is_empty() {
6211 let persisted_bytes = revocation_set.read().await.to_bytes();
6221 let id_dir = identity_dir_for_listener.clone();
6222 tokio::spawn(async move {
6223 match persisted_bytes {
6224 Ok(bytes) => {
6225 if let Err(e) = storage::save_revocation_set_bytes(
6226 bytes,
6227 id_dir.as_deref(),
6228 )
6229 .await
6230 {
6231 tracing::warn!(
6232 "failed to persist revocation set: {e}"
6233 );
6234 }
6235 }
6236 Err(e) => tracing::warn!(
6237 "failed to encode revocation set for persistence: {e}"
6238 ),
6239 }
6240 });
6241 for record in newly_inserted {
6243 match &record.subject {
6244 revocation::RevokedSubject::Agent(agent_id) => {
6245 if let Some(entry) = cache.write().await.remove(agent_id) {
6246 machine_cache.write().await.remove(&entry.machine_id);
6247 }
6248 let mut cs = contact_store_for_evict.write().await;
6249 cs.set_trust(agent_id, contacts::TrustLevel::Blocked);
6250 tracing::info!(
6251 agent = %hex::encode(agent_id.as_bytes()),
6252 "evicted revoked agent (received via gossip)"
6253 );
6254 }
6255 revocation::RevokedSubject::Machine(machine_id) => {
6256 machine_cache.write().await.remove(machine_id);
6257 cache.write().await.retain(|_, a| a.machine_id != *machine_id);
6258 tracing::info!(
6259 machine = %hex::encode(machine_id.as_bytes()),
6260 "evicted revoked machine (received via gossip)"
6261 );
6262 }
6263 }
6264 }
6265 }
6266 continue;
6267 }
6268 };
6269 let raw_payload = msg.payload.clone();
6270 let already_verified =
6271 has_recent_verified_payload(&seen_identity_payloads, &raw_payload);
6272 let announcement = match deserialize_identity_announcement(&raw_payload) {
6273 Ok(a) => a,
6274 Err(e) => {
6275 tracing::debug!("Ignoring invalid identity announcement payload: {}", e);
6276 continue;
6277 }
6278 };
6279
6280 if !already_verified {
6281 if let Err(e) = announcement.verify() {
6282 tracing::warn!("Ignoring unverifiable identity announcement: {}", e);
6283 continue;
6284 }
6285 remember_verified_payload(&mut seen_identity_payloads, &raw_payload);
6286 }
6287
6288 let now = Agent::unix_timestamp_secs();
6289 if !identity_announcement_timestamp_is_acceptable(announcement.announced_at, now) {
6290 tracing::warn!(
6291 agent = %hex::encode(announcement.agent_id.as_bytes()),
6292 announced_at = announcement.announced_at,
6293 now,
6294 max_future_skew_secs = IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS,
6295 "ignoring far-future identity announcement"
6296 );
6297 continue;
6298 }
6299
6300 {
6303 let store = contact_store.read().await;
6304 let evaluator = trust::TrustEvaluator::new(&store);
6305 let decision = evaluator.evaluate(&trust::TrustContext {
6306 agent_id: &announcement.agent_id,
6307 machine_id: &announcement.machine_id,
6308 });
6309 match decision {
6310 trust::TrustDecision::RejectBlocked => {
6311 tracing::debug!(
6312 "Dropping identity announcement from blocked agent {:?}",
6313 hex::encode(&announcement.agent_id.0[..8]),
6314 );
6315 continue;
6316 }
6317 trust::TrustDecision::RejectMachineMismatch => {
6318 tracing::warn!(
6319 "Dropping identity announcement from agent {}: machine {} not in pinned list",
6320 crate::logging::LogAgentId::from(&announcement.agent_id),
6321 crate::logging::LogMachineId::from(&announcement.machine_id),
6322 );
6323 continue;
6324 }
6325 _ => {}
6326 }
6327 }
6328
6329 {
6335 let revoked = revocation_set.read().await;
6336 if revoked.is_agent_revoked(&announcement.agent_id) {
6337 tracing::debug!(
6338 "Dropping identity announcement from revoked agent {:?}",
6339 hex::encode(&announcement.agent_id.0[..8]),
6340 );
6341 continue;
6342 }
6343 if revoked.is_machine_revoked(&announcement.machine_id) {
6344 tracing::debug!(
6345 "Dropping identity announcement from revoked machine {:?}",
6346 hex::encode(&announcement.machine_id.0[..8]),
6347 );
6348 continue;
6349 }
6350 }
6351
6352 if let Some(cert) = &announcement.agent_certificate {
6357 if identity::is_expired(cert.not_after(), Agent::unix_timestamp_secs()) {
6358 tracing::debug!(
6359 "Dropping identity announcement with expired cert from agent {:?}",
6360 hex::encode(&announcement.agent_id.0[..8]),
6361 );
6362 continue;
6363 }
6364 }
6365
6366 register_announced_machine(
6376 &contact_store,
6377 own_agent_id,
6378 announcement.agent_id,
6379 announcement.machine_id,
6380 )
6381 .await;
6382
6383
6384 let bootstrap_addresses =
6390 filter_publicly_advertisable_addrs(announcement.addresses.iter().copied());
6391 let discovery_addresses = filter_discovery_announcement_addrs(
6392 announcement.addresses.iter().copied(),
6393 allow_local_scope,
6394 );
6395 let filtered_addr_count = discovery_addresses.len();
6396 let auto_connect_addresses = discovery_addresses.clone();
6397 {
6398 if !bootstrap_addresses.is_empty() {
6399 if let Some(ref bc) = &bootstrap_cache {
6400 let peer_id = ant_quic::PeerId(announcement.machine_id.0);
6401 bc.add_from_connection(peer_id, bootstrap_addresses.clone(), None)
6402 .await;
6403 tracing::debug!(
6404 "Added {} public addresses to bootstrap cache for agent {:?} (machine {:?})",
6405 bootstrap_addresses.len(),
6406 announcement.agent_id,
6407 hex::encode(&announcement.machine_id.0[..8]),
6408 );
6409 }
6410 }
6411 }
6412
6413 let cert_not_after = announcement
6419 .agent_certificate
6420 .as_ref()
6421 .and_then(|c| c.not_after());
6422 let discovered_agent = DiscoveredAgent {
6423 agent_id: announcement.agent_id,
6424 machine_id: announcement.machine_id,
6425 user_id: announcement.user_id,
6426 addresses: discovery_addresses,
6427 announced_at: announcement.announced_at,
6428 last_seen: now,
6429 machine_public_key: announcement.machine_public_key.clone(),
6430 nat_type: announcement.nat_type.clone(),
6431 can_receive_direct: announcement.can_receive_direct,
6432 is_relay: announcement.is_relay,
6433 is_coordinator: announcement.is_coordinator,
6434 reachable_via: announcement.reachable_via.clone(),
6435 relay_candidates: announcement.relay_candidates.clone(),
6436 cert_not_after,
6437 agent_certificate: announcement.agent_certificate.clone(),
6438 agent_public_key: announcement.agent_public_key.clone(),
6439 };
6440 record_authenticated_machine_binding_from_message(
6441 &authenticated_machine_bindings,
6442 &msg,
6443 &announcement,
6444 now,
6445 )
6446 .await;
6447 upsert_discovered_machine_from_agent(&machine_cache, &discovered_agent).await;
6448 upsert_discovered_agent(&cache, discovered_agent).await;
6449 tracing::debug!(
6450 target: "x0x::discovery",
6451 announcement_kind = "received",
6452 agent_prefix = %network::hex_prefix(&announcement.agent_id.0, 4),
6453 machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
6454 addr_total = announcement.addresses.len(),
6455 filtered_addr_count,
6456 nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
6457 can_receive_direct = ?announcement.can_receive_direct,
6458 relay_capable = ?announcement.is_relay,
6459 coordinator_capable = ?announcement.is_coordinator,
6460 reachable_via_count = announcement.reachable_via.len(),
6461 relay_candidate_count = announcement.relay_candidates.len(),
6462 "cached verified identity announcement"
6463 );
6464
6465 direct_messaging
6469 .register_agent(announcement.agent_id, announcement.machine_id)
6470 .await;
6471
6472 if announcement.agent_id != own_agent_id {
6484 let key = (announcement.agent_id, announcement.announced_at);
6485 if should_rebroadcast_discovery_once(
6486 &mut rebroadcast_state,
6487 key,
6488 std::time::Instant::now(),
6489 ) {
6490 let pubsub = std::sync::Arc::clone(&rebroadcast_pubsub);
6491 let payload = raw_payload.clone();
6492 tokio::spawn(async move {
6493 if let Err(e) = pubsub
6494 .publish(IDENTITY_ANNOUNCE_TOPIC.to_string(), payload)
6495 .await
6496 {
6497 tracing::debug!("identity announcement re-broadcast failed: {e}");
6498 }
6499 });
6500 }
6501 }
6502
6503 if let Some(ref net) = &network {
6507 let ant_peer_id = ant_quic::PeerId(announcement.machine_id.0);
6508 if net.is_connected(&ant_peer_id).await {
6509 direct_messaging
6510 .mark_connected(announcement.agent_id, announcement.machine_id)
6511 .await;
6512 }
6513 }
6514
6515 if announcement.agent_id != own_agent_id
6520 && !auto_connect_addresses.is_empty()
6521 && !auto_connect_attempted.contains(&announcement.agent_id)
6522 {
6523 if let Some(ref net) = &network {
6524 let ant_peer = ant_quic::PeerId(announcement.machine_id.0);
6525 if !net.is_connected(&ant_peer).await {
6526 auto_connect_attempted.insert(announcement.agent_id);
6527 let net = std::sync::Arc::clone(net);
6528 let addresses = auto_connect_addresses.clone();
6529 tokio::spawn(async move {
6530 for addr in &addresses {
6531 match net.connect_addr(*addr).await {
6532 Ok(_) => {
6533 tracing::info!(
6534 "Auto-connected to discovered agent at {addr}",
6535 );
6536 return;
6537 }
6538 Err(e) => {
6539 tracing::debug!("Auto-connect to {addr} failed: {e}",);
6540 }
6541 }
6542 }
6543 tracing::debug!(
6544 "Auto-connect exhausted all {} addresses for discovered agent",
6545 addresses.len(),
6546 );
6547 });
6548 }
6549 }
6550 }
6551 }
6552 });
6553
6554 Ok(())
6555 }
6556
6557 fn unix_timestamp_secs() -> u64 {
6558 std::time::SystemTime::now()
6559 .duration_since(std::time::UNIX_EPOCH)
6560 .map_or(0, |d| d.as_secs())
6561 }
6562
6563 fn announcement_addresses(&self) -> Vec<std::net::SocketAddr> {
6564 match self.network.as_ref().and_then(|n| n.local_addr()) {
6565 Some(addr) if addr.port() > 0 => filter_discovery_announcement_addrs(
6566 collect_local_interface_addrs(addr.port()),
6567 self.network
6568 .as_ref()
6569 .is_some_and(|network| allow_local_discovery_addresses(network.config())),
6570 ),
6571 _ => Vec::new(),
6572 }
6573 }
6574
6575 fn build_identity_announcement(
6576 &self,
6577 include_user_identity: bool,
6578 human_consent: bool,
6579 ) -> error::Result<IdentityAnnouncement> {
6580 self.build_identity_announcement_with_addrs(IdentityAnnouncementBuildOptions {
6581 include_user_identity,
6582 human_consent,
6583 addresses: self.announcement_addresses(),
6584 assist_snapshot: None,
6585 reachable_via: Vec::new(),
6586 relay_candidates: Vec::new(),
6587 allow_local_scope: false,
6588 })
6589 }
6590
6591 fn build_identity_announcement_with_addrs(
6592 &self,
6593 options: IdentityAnnouncementBuildOptions<'_>,
6594 ) -> error::Result<IdentityAnnouncement> {
6595 let IdentityAnnouncementBuildOptions {
6596 include_user_identity,
6597 human_consent,
6598 addresses,
6599 assist_snapshot,
6600 reachable_via,
6601 relay_candidates,
6602 allow_local_scope,
6603 } = options;
6604 if include_user_identity && !human_consent {
6605 return Err(error::IdentityError::Storage(std::io::Error::other(
6606 "human identity disclosure requires explicit human consent — set human_consent: true in the request body",
6607 )));
6608 }
6609 let addresses = filter_discovery_announcement_addrs(addresses, allow_local_scope);
6610
6611 let (user_id, agent_certificate) = if include_user_identity {
6612 let user_id = self.user_id().ok_or_else(|| {
6613 error::IdentityError::Storage(std::io::Error::other(
6614 "human identity disclosure requested but no user identity is configured — set user_key_path in your config.toml to point at your user keypair file",
6615 ))
6616 })?;
6617 let cert = self.agent_certificate().cloned().ok_or_else(|| {
6618 error::IdentityError::Storage(std::io::Error::other(
6619 "human identity disclosure requested but agent certificate is missing",
6620 ))
6621 })?;
6622 (Some(user_id), Some(cert))
6623 } else {
6624 (None, None)
6625 };
6626
6627 let machine_public_key = self
6628 .identity
6629 .machine_keypair()
6630 .public_key()
6631 .as_bytes()
6632 .to_vec();
6633
6634 let unsigned = IdentityAnnouncementUnsigned {
6635 agent_id: self.agent_id(),
6636 machine_id: self.machine_id(),
6637 user_id,
6638 agent_certificate: agent_certificate.clone(),
6639 machine_public_key: machine_public_key.clone(),
6640 addresses,
6641 announced_at: Self::unix_timestamp_secs(),
6642 nat_type: assist_snapshot.and_then(|snapshot| snapshot.nat_type.clone()),
6643 can_receive_direct: assist_snapshot.and_then(|snapshot| snapshot.can_receive_direct),
6644 is_relay: assist_snapshot.and_then(|snapshot| snapshot.relay_capable),
6645 is_coordinator: assist_snapshot.and_then(|snapshot| snapshot.coordinator_capable),
6646 reachable_via,
6647 relay_candidates,
6648 };
6649 let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
6650 error::IdentityError::Serialization(format!(
6651 "failed to serialize unsigned identity announcement: {e}"
6652 ))
6653 })?;
6654 let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
6655 self.identity.machine_keypair().secret_key(),
6656 &unsigned_bytes,
6657 )
6658 .map_err(|e| {
6659 error::IdentityError::Storage(std::io::Error::other(format!(
6660 "failed to sign identity announcement with machine key: {:?}",
6661 e
6662 )))
6663 })?
6664 .as_bytes()
6665 .to_vec();
6666
6667 Ok(IdentityAnnouncement {
6668 agent_id: unsigned.agent_id,
6669 machine_id: unsigned.machine_id,
6670 user_id: unsigned.user_id,
6671 agent_certificate: unsigned.agent_certificate,
6672 machine_public_key,
6673 machine_signature,
6674 addresses: unsigned.addresses,
6675 announced_at: unsigned.announced_at,
6676 nat_type: unsigned.nat_type,
6677 can_receive_direct: unsigned.can_receive_direct,
6678 is_relay: unsigned.is_relay,
6679 is_coordinator: unsigned.is_coordinator,
6680 reachable_via: unsigned.reachable_via,
6681 relay_candidates: unsigned.relay_candidates,
6682 agent_public_key: self
6683 .identity
6684 .agent_keypair()
6685 .public_key()
6686 .as_bytes()
6687 .to_vec(),
6688 })
6689 }
6690
6691 pub async fn join_network(&self) -> error::Result<()> {
6700 let Some(network) = self.network.as_ref() else {
6701 tracing::debug!("join_network called but no network configured");
6702 return Ok(());
6703 };
6704
6705 if let Some(ref runtime) = self.gossip_runtime {
6706 runtime.start().await.map_err(|e| {
6707 error::IdentityError::Storage(std::io::Error::other(format!(
6708 "failed to start gossip runtime: {e}"
6709 )))
6710 })?;
6711 tracing::info!("Gossip runtime started");
6712 }
6713 if self.shutdown_token.is_cancelled() {
6717 tracing::info!("join_network aborted: shutdown already in progress");
6718 return Ok(());
6719 }
6720 self.start_identity_listener().await?;
6721 self.start_network_event_listener();
6722 self.start_direct_listener();
6723 self.start_stream_accept_loop();
6724
6725 let bootstrap_nodes = network.config().bootstrap_nodes.clone();
6726
6727 let min_connected = 3;
6728 let mut all_connected: Vec<std::net::SocketAddr> = Vec::new();
6729
6730 if let Some(ref cache) = self.bootstrap_cache {
6738 let coordinators = cache.select_coordinators(6).await;
6739 let coordinator_addrs: Vec<std::net::SocketAddr> = coordinators
6740 .iter()
6741 .flat_map(|peer| peer.preferred_addresses())
6742 .collect();
6743
6744 if !coordinator_addrs.is_empty() {
6745 tracing::info!(
6746 "Phase 0: Trying {} addresses from {} cached coordinators",
6747 coordinator_addrs.len(),
6748 coordinators.len()
6749 );
6750 let (succeeded, _failed) = self
6751 .connect_peers_parallel_tracked(network, &coordinator_addrs)
6752 .await;
6753 all_connected.extend(&succeeded);
6754 tracing::info!(
6755 "Phase 0: {}/{} coordinator addresses connected",
6756 succeeded.len(),
6757 coordinator_addrs.len()
6758 );
6759 }
6760 }
6761
6762 if all_connected.len() < min_connected {
6764 if let Some(ref cache) = self.bootstrap_cache {
6765 const PHASE1_PEER_CANDIDATES: usize = 12;
6766 let cached_peers = cache.select_peers(PHASE1_PEER_CANDIDATES).await;
6767 if !cached_peers.is_empty() {
6768 tracing::info!("Phase 1: Trying {} cached peers", cached_peers.len());
6769 let (succeeded, _failed) = self
6770 .connect_cached_peers_parallel_tracked(network, &cached_peers)
6771 .await;
6772 all_connected.extend(&succeeded);
6773 tracing::info!(
6774 "Phase 1: {}/{} cached peers connected",
6775 succeeded.len(),
6776 cached_peers.len()
6777 );
6778 }
6779 }
6780 } if all_connected.len() < min_connected && !bootstrap_nodes.is_empty() {
6785 let remaining: Vec<std::net::SocketAddr> = bootstrap_nodes
6786 .iter()
6787 .filter(|addr| !all_connected.contains(addr))
6788 .copied()
6789 .collect();
6790
6791 let (succeeded, mut failed) = self
6793 .connect_peers_parallel_tracked(network, &remaining)
6794 .await;
6795 all_connected.extend(&succeeded);
6796 tracing::info!(
6797 "Phase 2 round 1: {}/{} bootstrap peers connected",
6798 succeeded.len(),
6799 remaining.len()
6800 );
6801
6802 for round in 2..=3 {
6804 if failed.is_empty() {
6805 break;
6806 }
6807 let delay = std::time::Duration::from_secs(if round == 2 { 10 } else { 15 });
6808 tracing::info!(
6809 "Retrying {} failed peers in {}s (round {})",
6810 failed.len(),
6811 delay.as_secs(),
6812 round
6813 );
6814 tokio::time::sleep(delay).await;
6815
6816 let (succeeded, still_failed) =
6817 self.connect_peers_parallel_tracked(network, &failed).await;
6818 all_connected.extend(&succeeded);
6819 failed = still_failed;
6820 tracing::info!(
6821 "Phase 2 round {}: {} total peers connected",
6822 round,
6823 all_connected.len()
6824 );
6825 }
6826
6827 if !failed.is_empty() {
6828 tracing::warn!(
6829 "Could not connect to {} bootstrap peers: {:?}",
6830 failed.len(),
6831 failed
6832 );
6833 }
6834 }
6835
6836 tracing::info!(
6837 "Network join complete. Connected to {} peers.",
6838 all_connected.len()
6839 );
6840
6841 if let Some(ref runtime) = self.gossip_runtime {
6843 let seeds: Vec<String> = all_connected.iter().map(|addr| addr.to_string()).collect();
6844 if !seeds.is_empty() {
6845 if let Err(e) = runtime.membership().join(seeds).await {
6846 tracing::warn!("HyParView membership join failed: {e}");
6847 }
6848 }
6849 }
6850
6851 if let Some(ref pw) = self.presence {
6853 if let Some(ref runtime) = self.gossip_runtime {
6857 let active = runtime.membership().active_view();
6858 let active_view_count = active.len();
6859 let mut broadcast_peers = active;
6860
6861 let mut connected_peer_count = 0usize;
6862 if let Some(ref net) = self.network {
6863 let connected = net.connected_peers().await;
6864 connected_peer_count = connected.len();
6865 broadcast_peers.extend(
6866 connected
6867 .into_iter()
6868 .map(|peer| saorsa_gossip_types::PeerId::new(peer.0)),
6869 );
6870 }
6871
6872 pw.manager().replace_broadcast_peers(broadcast_peers).await;
6873 let broadcast_peer_count = pw.manager().broadcast_peer_count().await;
6874 tracing::info!(
6875 active_view_count,
6876 connected_peer_count,
6877 broadcast_peer_count,
6878 "Presence seeded broadcast peers"
6879 );
6880
6881 let pw_clone = pw.clone();
6891 let runtime_clone = runtime.clone();
6892 let network_clone = self.network.as_ref().map(std::sync::Arc::clone);
6893 let token = self.shutdown_token.clone();
6894 self.spawn_tracked(async move {
6895 let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
6896 interval.tick().await; loop {
6898 tokio::select! {
6899 _ = interval.tick() => {}
6900 _ = token.cancelled() => break,
6903 }
6904
6905 let active = runtime_clone.membership().active_view();
6906 let active_view_count = active.len();
6907 let mut broadcast_peers = active;
6908
6909 let mut connected_peer_count = 0usize;
6910 if let Some(ref net) = network_clone {
6911 let connected = net.connected_peers().await;
6912 connected_peer_count = connected.len();
6913 broadcast_peers.extend(
6914 connected
6915 .into_iter()
6916 .map(|peer| saorsa_gossip_types::PeerId::new(peer.0)),
6917 );
6918 }
6919
6920 pw_clone
6921 .manager()
6922 .replace_broadcast_peers(broadcast_peers)
6923 .await;
6924 let broadcast_peer_count = pw_clone.manager().broadcast_peer_count().await;
6925 tracing::info!(
6926 active_view_count,
6927 connected_peer_count,
6928 broadcast_peer_count,
6929 "Presence broadcast peer refresh"
6930 );
6931 }
6932 });
6933 }
6934
6935 if let Some(ref net) = self.network {
6942 if let Some(status) = net.node_status().await {
6943 let hints: Vec<String> = status
6944 .external_addrs
6945 .iter()
6946 .filter(|a| is_publicly_advertisable(**a))
6947 .map(|a| a.to_string())
6948 .collect();
6949 pw.manager().set_addr_hints(hints).await;
6950 }
6951 }
6952
6953 if !self.shutdown_token.is_cancelled() {
6958 if pw.config().enable_beacons {
6959 if let Err(e) = pw
6960 .manager()
6961 .start_beacons(pw.config().beacon_interval_secs)
6962 .await
6963 {
6964 tracing::warn!("Failed to start presence beacons: {e}");
6965 } else {
6966 tracing::info!(
6967 "Presence beacons started (interval={}s)",
6968 pw.config().beacon_interval_secs
6969 );
6970 }
6971 }
6972
6973 pw.start_event_loop(std::sync::Arc::clone(&self.identity_discovery_cache))
6977 .await;
6978 tracing::debug!("Presence event loop started");
6979 }
6980 }
6981
6982 if let Err(e) = self.announce_identity(false, false).await {
6983 tracing::warn!("Initial identity announcement failed: {}", e);
6984 }
6985 if let Err(e) = self.start_identity_heartbeat().await {
6986 tracing::warn!("Failed to start identity heartbeat: {e}");
6987 }
6988 if let Err(e) = self.start_discovery_cache_reaper().await {
6989 tracing::warn!("Failed to start discovery cache reaper: {e}");
6990 }
6991
6992 if let (Some(ref runtime), Some(ref network)) = (&self.gossip_runtime, &self.network) {
6999 let ctx = HeartbeatContext {
7000 identity: std::sync::Arc::clone(&self.identity),
7001 runtime: std::sync::Arc::clone(runtime),
7002 network: std::sync::Arc::clone(network),
7003 interval_secs: self.heartbeat_interval_secs,
7004 cache: std::sync::Arc::clone(&self.identity_discovery_cache),
7005 machine_cache: std::sync::Arc::clone(&self.machine_discovery_cache),
7006 user_identity_consented: std::sync::Arc::clone(&self.user_identity_consented),
7007 allow_local_discovery_addrs: allow_local_discovery_addresses(network.config()),
7008 revocation_set: std::sync::Arc::clone(&self.revocation_set),
7009 };
7010 self.spawn_tracked(async move {
7014 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
7015 if let Err(e) = ctx.announce().await {
7016 tracing::warn!("Delayed identity re-announcement failed: {e}");
7017 } else {
7018 tracing::info!(
7019 "Delayed identity re-announcement sent (gossip mesh stabilized)"
7020 );
7021 }
7022 });
7023 }
7024
7025 if let Err(e) = self.start_capability_advert_service().await {
7026 tracing::warn!("failed to start capability advert service: {e}");
7027 }
7028
7029 Ok(())
7030 }
7031
7032 #[must_use]
7034 pub fn capability_store(&self) -> std::sync::Arc<dm_capability::CapabilityStore> {
7035 std::sync::Arc::clone(&self.capability_store)
7036 }
7037
7038 pub async fn start_capability_advert_service(&self) -> error::Result<()> {
7050 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
7051 error::IdentityError::Storage(std::io::Error::other(
7052 "cannot start capability advert service: no gossip runtime configured",
7053 ))
7054 })?;
7055
7056 let signing = std::sync::Arc::new(gossip::SigningContext::from_keypair(
7057 self.identity.agent_keypair(),
7058 ));
7059 let caps_rx = self.dm_capabilities_tx.subscribe();
7060 let service = dm_capability_service::CapabilityAdvertService::spawn_default(
7061 std::sync::Arc::clone(runtime.pubsub()),
7062 signing,
7063 self.identity.agent_id(),
7064 self.identity.machine_id(),
7065 caps_rx,
7066 std::sync::Arc::clone(&self.capability_store),
7067 )
7068 .await
7069 .map_err(|e| {
7070 error::IdentityError::Storage(std::io::Error::other(format!(
7071 "capability advert service spawn failed: {e}"
7072 )))
7073 })?;
7074
7075 let mut guard = self.capability_advert_service.lock().await;
7076 if self.shutdown_token.is_cancelled() {
7080 service.abort();
7081 return Ok(());
7082 }
7083 if let Some(prev) = guard.take() {
7084 prev.abort();
7085 }
7086 *guard = Some(service);
7087 tracing::info!("Capability advert service started");
7088 Ok(())
7089 }
7090
7091 #[must_use]
7093 pub fn dm_inflight_acks(&self) -> std::sync::Arc<dm::InFlightAcks> {
7094 std::sync::Arc::clone(&self.dm_inflight_acks)
7095 }
7096
7097 #[must_use]
7099 pub fn recent_delivery_cache(&self) -> std::sync::Arc<dm::RecentDeliveryCache> {
7100 std::sync::Arc::clone(&self.recent_delivery_cache)
7101 }
7102
7103 pub async fn start_dm_inbox(
7110 &self,
7111 kem_keypair: std::sync::Arc<groups::kem_envelope::AgentKemKeypair>,
7112 config: dm_inbox::DmInboxConfig,
7113 ) -> error::Result<()> {
7114 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
7115 error::IdentityError::Storage(std::io::Error::other(
7116 "cannot start DM inbox: no gossip runtime configured",
7117 ))
7118 })?;
7119 let signing = std::sync::Arc::new(gossip::SigningContext::from_keypair(
7120 self.identity.agent_keypair(),
7121 ));
7122 let service = dm_inbox::DmInboxService::spawn(
7123 std::sync::Arc::clone(runtime.pubsub()),
7124 signing,
7125 self.identity.agent_id(),
7126 self.identity.machine_id(),
7127 std::sync::Arc::clone(&kem_keypair),
7128 std::sync::Arc::clone(&self.direct_messaging),
7129 std::sync::Arc::clone(&self.contact_store),
7130 std::sync::Arc::clone(&self.dm_inflight_acks),
7131 std::sync::Arc::clone(&self.recent_delivery_cache),
7132 config,
7133 std::sync::Arc::clone(&self.revocation_set),
7134 std::sync::Arc::clone(&self.authenticated_machine_bindings),
7135 )
7136 .await
7137 .map_err(|e| {
7138 error::IdentityError::Storage(std::io::Error::other(format!(
7139 "DM inbox spawn failed: {e}"
7140 )))
7141 })?;
7142 let mut guard = self.dm_inbox_service.lock().await;
7143 if self.shutdown_token.is_cancelled() {
7148 service.abort();
7149 return Ok(());
7150 }
7151 if let Some(prev) = guard.take() {
7152 prev.abort();
7153 }
7154 *guard = Some(service);
7155
7156 let upgraded =
7160 dm::DmCapabilities::pending().with_kem_public_key(kem_keypair.public_bytes.clone());
7161 self.dm_capabilities_tx.send_replace(upgraded);
7167 tracing::info!("DM inbox service started");
7168 Ok(())
7169 }
7170
7171 pub async fn stop_dm_inbox(&self) {
7173 let mut guard = self.dm_inbox_service.lock().await;
7174 if let Some(service) = guard.take() {
7175 service.abort();
7176 }
7177 }
7178
7179 async fn connect_cached_peers_parallel_tracked(
7181 &self,
7182 network: &std::sync::Arc<network::NetworkNode>,
7183 peers: &[ant_quic::CachedPeer],
7184 ) -> (Vec<std::net::SocketAddr>, Vec<ant_quic::PeerId>) {
7185 use tokio::time::{timeout, Duration};
7186 const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
7187
7188 let handles: Vec<_> = peers
7189 .iter()
7190 .map(|peer| {
7191 let net = network.clone();
7192 let peer_id = peer.peer_id;
7193 tokio::spawn(async move {
7194 tracing::debug!("Connecting to cached peer: {:?}", peer_id);
7195 match timeout(CONNECT_TIMEOUT, net.connect_cached_peer(peer_id)).await {
7196 Ok(Ok(addr)) => {
7197 tracing::info!("Connected to cached peer {:?} at {}", peer_id, addr);
7198 Ok(addr)
7199 }
7200 Ok(Err(e)) => {
7201 tracing::warn!("Failed to connect to cached peer {:?}: {}", peer_id, e);
7202 Err(peer_id)
7203 }
7204 Err(_) => {
7205 tracing::warn!(
7206 "Connection to cached peer {:?} timed out after {}s",
7207 peer_id,
7208 CONNECT_TIMEOUT.as_secs()
7209 );
7210 Err(peer_id)
7211 }
7212 }
7213 })
7214 })
7215 .collect();
7216
7217 let mut succeeded = Vec::new();
7218 let mut failed = Vec::new();
7219 for handle in handles {
7220 match handle.await {
7221 Ok(Ok(addr)) => succeeded.push(addr),
7222 Ok(Err(peer_id)) => failed.push(peer_id),
7223 Err(e) => tracing::error!("Connection task panicked: {}", e),
7224 }
7225 }
7226 (succeeded, failed)
7227 }
7228
7229 async fn connect_peers_parallel_tracked(
7231 &self,
7232 network: &std::sync::Arc<network::NetworkNode>,
7233 addrs: &[std::net::SocketAddr],
7234 ) -> (Vec<std::net::SocketAddr>, Vec<std::net::SocketAddr>) {
7235 use tokio::time::{timeout, Duration};
7236
7237 const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
7240
7241 let handles: Vec<_> = addrs
7242 .iter()
7243 .map(|addr| {
7244 let net = network.clone();
7245 let addr = *addr;
7246 tokio::spawn(async move {
7247 tracing::debug!("Connecting to peer: {}", addr);
7248 match timeout(CONNECT_TIMEOUT, net.connect_addr(addr)).await {
7249 Ok(Ok(_)) => {
7250 tracing::info!("Connected to peer: {}", addr);
7251 Ok(addr)
7252 }
7253 Ok(Err(e)) => {
7254 tracing::warn!("Failed to connect to {}: {}", addr, e);
7255 Err(addr)
7256 }
7257 Err(_) => {
7258 tracing::warn!(
7259 "Connection to {} timed out after {}s",
7260 addr,
7261 CONNECT_TIMEOUT.as_secs()
7262 );
7263 Err(addr)
7264 }
7265 }
7266 })
7267 })
7268 .collect();
7269
7270 let mut succeeded = Vec::new();
7271 let mut failed = Vec::new();
7272 for handle in handles {
7273 match handle.await {
7274 Ok(Ok(addr)) => succeeded.push(addr),
7275 Ok(Err(addr)) => failed.push(addr),
7276 Err(e) => tracing::error!("Connection task panicked: {}", e),
7277 }
7278 }
7279 (succeeded, failed)
7280 }
7281
7282 pub async fn subscribe(&self, topic: &str) -> error::Result<Subscription> {
7292 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
7293 error::IdentityError::Storage(std::io::Error::other(
7294 "gossip runtime not initialized - configure agent with network first",
7295 ))
7296 })?;
7297 Ok(runtime.pubsub().subscribe(topic.to_string()).await)
7298 }
7299
7300 pub async fn publish(&self, topic: &str, payload: Vec<u8>) -> error::Result<()> {
7312 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
7313 error::IdentityError::Storage(std::io::Error::other(
7314 "gossip runtime not initialized - configure agent with network first",
7315 ))
7316 })?;
7317 runtime
7318 .pubsub()
7319 .publish(topic.to_string(), bytes::Bytes::from(payload))
7320 .await
7321 .map_err(|e| {
7322 error::IdentityError::Storage(std::io::Error::other(format!(
7323 "publish failed: {}",
7324 e
7325 )))
7326 })
7327 }
7328
7329 pub async fn peers(&self) -> error::Result<Vec<saorsa_gossip_types::PeerId>> {
7337 let network = self.network.as_ref().ok_or_else(|| {
7338 error::IdentityError::Storage(std::io::Error::other(
7339 "network not initialized - configure agent with network first",
7340 ))
7341 })?;
7342 let ant_peers = network.connected_peers().await;
7343 Ok(ant_peers
7344 .into_iter()
7345 .map(|p| saorsa_gossip_types::PeerId::new(p.0))
7346 .collect())
7347 }
7348
7349 pub async fn presence(&self) -> error::Result<Vec<identity::AgentId>> {
7357 self.start_identity_listener().await?;
7358 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
7359 let mut agents: Vec<_> = self
7360 .identity_discovery_cache
7361 .read()
7362 .await
7363 .values()
7364 .filter(|a| discovery_record_is_live(a.announced_at, a.last_seen, cutoff))
7365 .map(|a| a.agent_id)
7366 .collect();
7367 agents.sort_by_key(|a| a.0);
7368 Ok(agents)
7369 }
7370
7371 pub async fn subscribe_presence(
7385 &self,
7386 ) -> error::NetworkResult<tokio::sync::broadcast::Receiver<presence::PresenceEvent>> {
7387 let pw = self.presence.as_ref().ok_or_else(|| {
7388 error::NetworkError::NodeError("presence system not initialized".to_string())
7389 })?;
7390 pw.start_event_loop(std::sync::Arc::clone(&self.identity_discovery_cache))
7392 .await;
7393 Ok(pw.subscribe_events())
7394 }
7395
7396 pub async fn cached_agent(&self, id: &identity::AgentId) -> Option<DiscoveredAgent> {
7402 self.identity_discovery_cache.read().await.get(id).cloned()
7403 }
7404
7405 pub async fn is_agent_machine_verified(
7420 &self,
7421 agent_id: &identity::AgentId,
7422 machine_id: &identity::MachineId,
7423 ) -> bool {
7424 {
7428 let revoked = self.revocation_set.read().await;
7429 if revoked.is_agent_revoked(agent_id) || revoked.is_machine_revoked(machine_id) {
7430 return false;
7431 }
7432 }
7433
7434 let cache = self.identity_discovery_cache.read().await;
7435 let Some(entry) = cache.get(agent_id) else {
7436 return false;
7437 };
7438 if entry.machine_id != *machine_id {
7439 return false;
7440 }
7441 if identity::is_expired(entry.cert_not_after, Self::unix_timestamp_secs()) {
7443 return false;
7444 }
7445 true
7446 }
7447
7448 pub fn revocation_set(&self) -> std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>> {
7454 std::sync::Arc::clone(&self.revocation_set)
7455 }
7456
7457 pub(crate) fn identity_discovery_cache(
7461 &self,
7462 ) -> std::sync::Arc<
7463 tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
7464 > {
7465 std::sync::Arc::clone(&self.identity_discovery_cache)
7466 }
7467
7468 pub(crate) fn contact_store(
7471 &self,
7472 ) -> std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>> {
7473 std::sync::Arc::clone(&self.contact_store)
7474 }
7475
7476 pub async fn revocation_records(&self) -> Vec<revocation::RevocationRecord> {
7480 self.revocation_set.read().await.all_records()
7481 }
7482
7483 pub async fn revoke(
7501 &self,
7502 issuer_keypair: &identity::AgentKeypair,
7503 subject: revocation::RevokedSubject,
7504 reason: Option<String>,
7505 subject_cert: Option<&identity::AgentCertificate>,
7506 ) -> error::Result<revocation::RevocationRecord> {
7507 let now = Self::unix_timestamp_secs();
7508 let record = revocation::RevocationRecord::sign(
7509 subject,
7510 issuer_keypair.public_key(),
7511 issuer_keypair.secret_key(),
7512 now,
7513 reason,
7514 )?;
7515 self.apply_and_publish_revocation(record.clone(), subject_cert)
7516 .await?;
7517 Ok(record)
7518 }
7519
7520 async fn apply_and_publish_revocation(
7525 &self,
7526 record: revocation::RevocationRecord,
7527 subject_cert: Option<&identity::AgentCertificate>,
7528 ) -> error::Result<()> {
7529 {
7531 let mut set = self.revocation_set.write().await;
7532 if let Err(e) = set.verify_and_insert(record.clone(), subject_cert) {
7533 return Err(error::IdentityError::CertificateVerification(format!(
7534 "revocation rejected: {e}"
7535 )));
7536 }
7537 }
7538
7539 storage::save_revocation_set(
7541 &*self.revocation_set.read().await,
7542 self.identity_dir.as_deref(),
7543 )
7544 .await?;
7545
7546 self.evict_revoked_subject(&record.subject).await;
7548
7549 if let Some(rt) = &self.gossip_runtime {
7551 let records = self.revocation_set.read().await.all_records();
7552 match bincode::serialize(&records) {
7553 Ok(bytes) if !bytes.is_empty() => {
7554 let _ = rt
7555 .pubsub()
7556 .publish(REVOCATION_TOPIC.to_string(), bytes::Bytes::from(bytes))
7557 .await;
7558 }
7559 _ => {}
7560 }
7561 }
7562
7563 Ok(())
7564 }
7565
7566 async fn evict_revoked_subject(&self, subject: &revocation::RevokedSubject) {
7568 match subject {
7575 revocation::RevokedSubject::Agent(agent_id) => {
7576 let mut cache = self.identity_discovery_cache.write().await;
7578 if let Some(entry) = cache.remove(agent_id) {
7579 drop(cache);
7581 let mut mcache = self.machine_discovery_cache.write().await;
7582 mcache.remove(&entry.machine_id);
7583 } else {
7584 drop(cache);
7585 }
7586 {
7589 let mut cs = self.contact_store.write().await;
7590 cs.set_trust(agent_id, contacts::TrustLevel::Blocked);
7591 }
7592 tracing::info!(
7593 agent = %hex::encode(agent_id.as_bytes()),
7594 "evicted revoked agent from discovery cache"
7595 );
7596 }
7597 revocation::RevokedSubject::Machine(machine_id) => {
7598 let mut mcache = self.machine_discovery_cache.write().await;
7599 mcache.remove(machine_id);
7600 drop(mcache);
7601 let mut cache = self.identity_discovery_cache.write().await;
7603 cache.retain(|_, agent| agent.machine_id != *machine_id);
7604 tracing::info!(
7605 machine = %hex::encode(machine_id.as_bytes()),
7606 "evicted revoked machine from discovery cache"
7607 );
7608 }
7609 }
7610 }
7611
7612 pub async fn discover_agents_foaf(
7631 &self,
7632 ttl: u8,
7633 timeout_ms: u64,
7634 ) -> error::NetworkResult<Vec<DiscoveredAgent>> {
7635 let pw = self.presence.as_ref().ok_or_else(|| {
7636 error::NetworkError::NodeError("presence system not initialized".to_string())
7637 })?;
7638
7639 let topic = presence::global_presence_topic();
7640 let raw_results: Vec<(
7641 saorsa_gossip_types::PeerId,
7642 saorsa_gossip_types::PresenceRecord,
7643 )> = pw
7644 .manager()
7645 .initiate_foaf_query(topic, ttl, timeout_ms)
7646 .await
7647 .map_err(|e| error::NetworkError::NodeError(e.to_string()))?;
7648
7649 let cache = self.identity_discovery_cache.read().await;
7650
7651 let mut seen: std::collections::HashSet<identity::AgentId> =
7653 std::collections::HashSet::new();
7654 let mut agents: Vec<DiscoveredAgent> = Vec::with_capacity(raw_results.len());
7655
7656 for (peer_id, record) in &raw_results {
7657 if let Some(agent) =
7658 presence::presence_record_to_discovered_agent(*peer_id, record, &cache)
7659 {
7660 if seen.insert(agent.agent_id) {
7661 agents.push(agent);
7662 }
7663 }
7664 }
7665
7666 Ok(agents)
7667 }
7668
7669 pub async fn discover_agent_by_id(
7683 &self,
7684 target_id: identity::AgentId,
7685 ttl: u8,
7686 timeout_ms: u64,
7687 ) -> error::NetworkResult<Option<DiscoveredAgent>> {
7688 {
7690 let cache = self.identity_discovery_cache.read().await;
7691 if let Some(agent) = cache.get(&target_id) {
7692 return Ok(Some(agent.clone()));
7693 }
7694 }
7695
7696 let agents = self.discover_agents_foaf(ttl, timeout_ms).await?;
7698 Ok(agents.into_iter().find(|a| a.agent_id == target_id))
7699 }
7700
7701 pub async fn find_agent(
7718 &self,
7719 agent_id: identity::AgentId,
7720 ) -> error::Result<Option<Vec<std::net::SocketAddr>>> {
7721 self.start_identity_listener().await?;
7722
7723 if let Some(addrs) = self
7725 .identity_discovery_cache
7726 .read()
7727 .await
7728 .get(&agent_id)
7729 .map(|e| e.addresses.clone())
7730 {
7731 return Ok(Some(addrs));
7732 }
7733
7734 let runtime = match self.gossip_runtime.as_ref() {
7736 Some(r) => r,
7737 None => return Ok(None),
7738 };
7739 let shard_topic = shard_topic_for_agent(&agent_id);
7740 let mut sub = runtime.pubsub().subscribe(shard_topic).await;
7741 let cache = std::sync::Arc::clone(&self.identity_discovery_cache);
7742 let machine_cache = std::sync::Arc::clone(&self.machine_discovery_cache);
7743 let allow_local_scope = self
7744 .network
7745 .as_ref()
7746 .is_some_and(|network| allow_local_discovery_addresses(network.config()));
7747 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7748
7749 loop {
7750 if tokio::time::Instant::now() >= deadline {
7751 break;
7752 }
7753 let timeout = tokio::time::sleep_until(deadline);
7754 tokio::select! {
7755 Some(msg) = sub.recv() => {
7756 if let Ok(ann) = deserialize_identity_announcement(&msg.payload) {
7757 if ann.verify().is_ok() && ann.agent_id == agent_id {
7758 let now = std::time::SystemTime::now()
7759 .duration_since(std::time::UNIX_EPOCH)
7760 .map_or(0, |d| d.as_secs());
7761 let filtered = filter_discovery_announcement_addrs(
7762 ann.addresses.iter().copied(),
7763 allow_local_scope,
7764 );
7765 let addrs = filtered.clone();
7766 let discovered_agent = DiscoveredAgent {
7767 agent_id: ann.agent_id,
7768 machine_id: ann.machine_id,
7769 user_id: ann.user_id,
7770 addresses: filtered,
7771 announced_at: ann.announced_at,
7772 last_seen: now,
7773 machine_public_key: ann.machine_public_key.clone(),
7774 nat_type: ann.nat_type.clone(),
7775 can_receive_direct: ann.can_receive_direct,
7776 is_relay: ann.is_relay,
7777 is_coordinator: ann.is_coordinator,
7778 reachable_via: ann.reachable_via.clone(),
7779 relay_candidates: ann.relay_candidates.clone(),
7780 cert_not_after: ann
7781 .agent_certificate
7782 .as_ref()
7783 .and_then(|c| c.not_after()),
7784 agent_certificate: ann.agent_certificate.clone(),
7785 agent_public_key: ann.agent_public_key.clone(),
7786 };
7787 upsert_discovered_machine_from_agent(&machine_cache, &discovered_agent)
7788 .await;
7789 upsert_discovered_agent(&cache, discovered_agent).await;
7790 return Ok(Some(addrs));
7791 }
7792 }
7793 }
7794 _ = timeout => break,
7795 }
7796 }
7797
7798 if let Some(addrs) = self.find_agent_rendezvous(agent_id, 5).await? {
7801 let now = std::time::SystemTime::now()
7802 .duration_since(std::time::UNIX_EPOCH)
7803 .map_or(0, |d| d.as_secs());
7804 upsert_discovered_agent(
7805 &cache,
7806 DiscoveredAgent {
7807 agent_id,
7808 machine_id: identity::MachineId([0u8; 32]),
7809 user_id: None,
7810 addresses: addrs.clone(),
7811 announced_at: now,
7812 last_seen: now,
7813 machine_public_key: Vec::new(),
7814 nat_type: None,
7815 can_receive_direct: None,
7816 is_relay: None,
7817 is_coordinator: None,
7818 reachable_via: Vec::new(),
7819 relay_candidates: Vec::new(),
7820 cert_not_after: None,
7821 agent_certificate: None,
7822 agent_public_key: Vec::new(),
7823 },
7824 )
7825 .await;
7826 return Ok(Some(addrs));
7827 }
7828
7829 Ok(None)
7830 }
7831
7832 pub async fn find_machine(
7841 &self,
7842 machine_id: identity::MachineId,
7843 timeout_secs: u64,
7844 ) -> error::Result<Option<DiscoveredMachine>> {
7845 self.start_identity_listener().await?;
7846
7847 if let Some(machine) = self
7848 .machine_discovery_cache
7849 .read()
7850 .await
7851 .get(&machine_id)
7852 .cloned()
7853 {
7854 return Ok(Some(machine));
7855 }
7856
7857 let runtime = match self.gossip_runtime.as_ref() {
7858 Some(r) => r,
7859 None => return Ok(None),
7860 };
7861 let shard_topic = shard_topic_for_machine(&machine_id);
7862 let mut sub = runtime.pubsub().subscribe(shard_topic).await;
7863 let machine_cache = std::sync::Arc::clone(&self.machine_discovery_cache);
7864 let allow_local_scope = self
7865 .network
7866 .as_ref()
7867 .is_some_and(|network| allow_local_discovery_addresses(network.config()));
7868 let deadline =
7869 tokio::time::Instant::now() + std::time::Duration::from_secs(timeout_secs.clamp(1, 60));
7870
7871 loop {
7872 if tokio::time::Instant::now() >= deadline {
7873 break;
7874 }
7875 let timeout = tokio::time::sleep_until(deadline);
7876 tokio::select! {
7877 Some(msg) = sub.recv() => {
7878 if let Ok(ann) = deserialize_machine_announcement(&msg.payload) {
7879 if ann.verify().is_ok() && ann.machine_id == machine_id {
7880 let now = std::time::SystemTime::now()
7881 .duration_since(std::time::UNIX_EPOCH)
7882 .map_or(0, |d| d.as_secs());
7883 let filtered = filter_discovery_announcement_addrs(
7884 ann.addresses.iter().copied(),
7885 allow_local_scope,
7886 );
7887 let discovered = DiscoveredMachine::from_machine_announcement(
7888 &ann,
7889 filtered,
7890 now,
7891 );
7892 upsert_discovered_machine(&machine_cache, discovered).await;
7893 return Ok(machine_cache.read().await.get(&machine_id).cloned());
7894 }
7895 }
7896 }
7897 _ = timeout => break,
7898 }
7899 }
7900
7901 Ok(None)
7902 }
7903
7904 pub async fn find_agents_by_user(
7917 &self,
7918 user_id: identity::UserId,
7919 ) -> error::Result<Vec<DiscoveredAgent>> {
7920 self.start_identity_listener().await?;
7921 let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
7922 Ok(self
7923 .identity_discovery_cache
7924 .read()
7925 .await
7926 .values()
7927 .filter(|a| {
7928 discovery_record_is_live(a.announced_at, a.last_seen, cutoff)
7929 && a.user_id == Some(user_id)
7930 })
7931 .cloned()
7932 .collect())
7933 }
7934
7935 #[must_use]
7943 pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
7944 self.network.as_ref().and_then(|n| n.local_addr())
7945 }
7946
7947 pub async fn bound_addr(&self) -> Option<std::net::SocketAddr> {
7953 if let Some(ref network) = self.network {
7954 let addr = network.bound_addr().await;
7955 match (addr, self.local_addr()) {
7959 (Some(bound), Some(config)) if config.is_ipv4() && bound.is_ipv6() => {
7960 Some(std::net::SocketAddr::new(config.ip(), bound.port()))
7961 }
7962 (Some(bound), _) => Some(bound),
7963 _ => None,
7964 }
7965 } else {
7966 None
7967 }
7968 }
7969
7970 pub fn build_announcement(
7978 &self,
7979 include_user: bool,
7980 consent: bool,
7981 ) -> error::Result<IdentityAnnouncement> {
7982 self.build_identity_announcement(include_user, consent)
7983 }
7984
7985 pub fn build_machine_announcement(&self) -> error::Result<MachineAnnouncement> {
7996 build_machine_announcement_for_identity(
7997 &self.identity,
7998 self.announcement_addresses(),
7999 Self::unix_timestamp_secs(),
8000 None,
8001 Vec::new(),
8002 Vec::new(),
8003 self.network
8004 .as_ref()
8005 .is_some_and(|network| allow_local_discovery_addresses(network.config())),
8006 )
8007 }
8008
8009 fn start_network_event_listener(&self) {
8020 if self
8021 .network_event_listener_started
8022 .swap(true, std::sync::atomic::Ordering::AcqRel)
8023 {
8024 return;
8025 }
8026
8027 let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
8028 return;
8029 };
8030 let cache = std::sync::Arc::clone(&self.identity_discovery_cache);
8031 let dm = std::sync::Arc::clone(&self.direct_messaging);
8032
8033 let lifecycle_network = std::sync::Arc::clone(&network);
8034 let lifecycle_dm = std::sync::Arc::clone(&dm);
8035 let event_token = self.shutdown_token.clone();
8036 let lifecycle_token = self.shutdown_token.clone();
8037
8038 self.spawn_tracked(async move {
8039 let mut rx = network.subscribe();
8040 tracing::info!("Network event reconciliation listener started");
8041
8042 loop {
8043 let event = tokio::select! {
8044 _ = event_token.cancelled() => break,
8048 recv = rx.recv() => match recv {
8049 Ok(event) => event,
8050 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
8051 tracing::warn!("Network event listener lagged by {skipped} events");
8052 continue;
8053 }
8054 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
8055 },
8056 };
8057
8058 match event {
8059 network::NetworkEvent::PeerConnected { peer_id, .. } => {
8060 let machine_id = identity::MachineId(peer_id);
8061 let cached_agent_id = {
8062 let cache = cache.read().await;
8063 cache
8064 .values()
8065 .find(|entry| entry.machine_id == machine_id)
8066 .map(|entry| entry.agent_id)
8067 };
8068 let agent_id = match cached_agent_id {
8069 Some(agent_id) => Some(agent_id),
8070 None => dm.lookup_agent(&machine_id).await,
8071 };
8072 if let Some(agent_id) = agent_id {
8073 dm.mark_connected(agent_id, machine_id).await;
8074 }
8075 }
8076 network::NetworkEvent::PeerDisconnected { peer_id } => {
8077 let machine_id = identity::MachineId(peer_id);
8078 let cached_agent_id = {
8079 let cache = cache.read().await;
8080 cache
8081 .values()
8082 .find(|entry| entry.machine_id == machine_id)
8083 .map(|entry| entry.agent_id)
8084 };
8085 let agent_id = match cached_agent_id {
8086 Some(agent_id) => Some(agent_id),
8087 None => dm.lookup_agent(&machine_id).await,
8088 };
8089 if let Some(agent_id) = agent_id {
8090 dm.mark_disconnected(&agent_id).await;
8091 }
8092 }
8093 _ => {}
8094 }
8095 }
8096 });
8097
8098 self.spawn_tracked(async move {
8099 let Some(mut rx) = lifecycle_network.subscribe_all_peer_events().await else {
8100 tracing::debug!(
8101 "Peer lifecycle listener unavailable: network node not initialised"
8102 );
8103 return;
8104 };
8105 tracing::info!("Peer lifecycle watcher started for direct messaging");
8106 loop {
8107 let (peer_id, event) = tokio::select! {
8108 _ = lifecycle_token.cancelled() => break,
8109 recv = rx.recv() => match recv {
8110 Ok(event) => event,
8111 Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
8112 tracing::warn!("Peer lifecycle watcher lagged by {skipped} events");
8113 continue;
8114 }
8115 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
8116 },
8117 };
8118 let machine_id = identity::MachineId(peer_id.0);
8119 match event {
8120 ant_quic::PeerLifecycleEvent::Established { generation } => {
8121 lifecycle_dm.record_lifecycle_established(machine_id, Some(generation));
8122 }
8123 ant_quic::PeerLifecycleEvent::Replaced { new_generation, .. } => {
8124 lifecycle_dm.record_lifecycle_replaced(machine_id, new_generation);
8125 }
8126 ant_quic::PeerLifecycleEvent::Closing { generation, reason } => {
8127 lifecycle_dm.record_lifecycle_blocked(
8128 machine_id,
8129 Some(generation),
8130 format!("closing: {reason}"),
8131 );
8132 }
8133 ant_quic::PeerLifecycleEvent::Closed { generation, reason } => {
8134 lifecycle_dm.record_lifecycle_blocked(
8135 machine_id,
8136 Some(generation),
8137 format!("closed: {reason}"),
8138 );
8139 }
8140 ant_quic::PeerLifecycleEvent::ReaderExited { generation } => {
8141 tracing::debug!(
8142 machine_prefix = %network::hex_prefix(machine_id.as_bytes(), 4),
8143 generation,
8144 "peer reader exited; waiting for Closed/Established before blocking direct DM"
8145 );
8146 }
8147 }
8148 }
8149 });
8150 }
8151
8152 fn start_direct_listener(&self) {
8160 if self
8161 .direct_listener_started
8162 .swap(true, std::sync::atomic::Ordering::AcqRel)
8163 {
8164 return;
8165 }
8166
8167 let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
8168 return;
8169 };
8170 let dm = std::sync::Arc::clone(&self.direct_messaging);
8171 let discovery_cache = std::sync::Arc::clone(&self.identity_discovery_cache);
8172 let contact_store = std::sync::Arc::clone(&self.contact_store);
8173 let revocation_set = std::sync::Arc::clone(&self.revocation_set);
8174 let token = self.shutdown_token.clone();
8175
8176 self.spawn_tracked(async move {
8177 tracing::info!(target: "x0x::direct", stage = "listener", "direct message listener started");
8178 loop {
8179 let recv = tokio::select! {
8183 _ = token.cancelled() => break,
8184 r = network.recv_direct() => r,
8185 };
8186 let Some((ant_peer_id, payload)) = recv else {
8187 tracing::warn!(
8188 target: "x0x::direct",
8189 stage = "listener",
8190 "network.recv_direct channel closed — listener exiting"
8191 );
8192 break;
8193 };
8194
8195 let raw_bytes = payload.len();
8196
8197 if payload.len() < 32 {
8199 tracing::warn!(
8200 target: "x0x::direct",
8201 stage = "listener",
8202 machine_prefix = %crate::logging::LogTransportPeerId::from(&ant_peer_id),
8203 raw_bytes,
8204 outcome = "drop_too_short",
8205 "direct message too short to contain sender id"
8206 );
8207 continue;
8208 }
8209
8210 let mut sender_bytes = [0u8; 32];
8211 sender_bytes.copy_from_slice(&payload[..32]);
8212 let sender = identity::AgentId(sender_bytes);
8213 let machine_id = identity::MachineId(ant_peer_id.0);
8214 let data = payload[32..].to_vec();
8215 let payload_bytes = data.len();
8216 let digest = direct::dm_payload_digest_hex(&data);
8217
8218 tracing::debug!(
8219 target: "dm.trace",
8220 stage = "inbound_envelope_received",
8221 sender = %hex::encode(sender.as_bytes()),
8222 machine_id = %hex::encode(machine_id.as_bytes()),
8223 path = "raw_quic",
8224 bytes = payload_bytes,
8225 raw_bytes,
8226 digest = %digest,
8227 );
8228
8229 let (verified, cert_not_after) = {
8231 let cache = discovery_cache.read().await;
8232 cache
8233 .get(&sender)
8234 .map(|entry| (entry.machine_id == machine_id, entry.cert_not_after))
8235 .unwrap_or((false, None))
8236 };
8237
8238 let trust_decision = {
8240 let contacts = contact_store.read().await;
8241 let evaluator = trust::TrustEvaluator::new(&contacts);
8242 let ctx = trust::TrustContext {
8243 agent_id: &sender,
8244 machine_id: &machine_id,
8245 };
8246 Some(evaluator.evaluate(&ctx))
8247 };
8248
8249 tracing::debug!(
8250 target: "dm.trace",
8251 stage = "inbound_trust_evaluated",
8252 sender = %hex::encode(sender.as_bytes()),
8253 machine_id = %hex::encode(machine_id.as_bytes()),
8254 path = "raw_quic",
8255 verified,
8256 decision = ?trust_decision,
8257 digest = %digest,
8258 );
8259
8260 tracing::info!(
8261 target: "x0x::direct",
8262 stage = "recv",
8263 sender_prefix = %network::hex_prefix(&sender.0, 4),
8264 machine_prefix = %network::hex_prefix(&machine_id.0, 4),
8265 raw_bytes,
8266 payload_bytes,
8267 verified,
8268 trust_decision = ?trust_decision,
8269 "direct message received; dispatching to subscribers"
8270 );
8271
8272 let peer_revoked = {
8282 let revoked = revocation_set.read().await;
8283 direct::inbound_peer_revoked(&revoked, &sender, &machine_id)
8284 };
8285 if peer_revoked {
8286 dm.record_incoming_dropped_revoked();
8287 tracing::info!(
8288 target: "x0x::direct",
8289 stage = "recv",
8290 sender_prefix = %network::hex_prefix(&sender.0, 4),
8291 machine_prefix = %network::hex_prefix(&machine_id.0, 4),
8292 outcome = "drop_revoked",
8293 "direct message from revoked sender dropped (direct-path revocation gate, mirrors EP3)"
8294 );
8295 continue;
8296 }
8297 if identity::is_expired(cert_not_after, Agent::unix_timestamp_secs()) {
8305 dm.record_incoming_dropped_expired();
8306 tracing::info!(
8307 target: "x0x::direct",
8308 stage = "recv",
8309 sender_prefix = %network::hex_prefix(&sender.0, 4),
8310 machine_prefix = %network::hex_prefix(&machine_id.0, 4),
8311 outcome = "drop_expired",
8312 "direct message from sender with expired cert dropped (runtime expiry gate, issue #191)"
8313 );
8314 continue;
8315 }
8316
8317 dm.mark_connected(sender, machine_id).await;
8319
8320 let delivered = dm
8322 .handle_incoming(machine_id, sender, data, verified, trust_decision)
8323 .await;
8324
8325 tracing::debug!(
8326 target: "dm.trace",
8327 stage = "inbound_broadcast_published",
8328 sender = %hex::encode(sender.as_bytes()),
8329 machine_id = %hex::encode(machine_id.as_bytes()),
8330 path = "raw_quic",
8331 delivered,
8332 subscribers = dm.subscriber_count(),
8333 digest = %digest,
8334 );
8335
8336 tracing::debug!(
8337 target: "x0x::direct",
8338 stage = "recv",
8339 sender_prefix = %network::hex_prefix(&sender.0, 4),
8340 payload_bytes,
8341 subscriber_count = dm.subscriber_count(),
8342 "direct message dispatched"
8343 );
8344 }
8345 });
8346 }
8347
8348 pub async fn open_peer_stream(
8358 &self,
8359 agent_id: &identity::AgentId,
8360 protocol: streams::StreamProtocol,
8361 ) -> error::NetworkResult<streams::PeerStream> {
8362 let (machine_id, cert_not_after) = {
8363 let cache = self.identity_discovery_cache.read().await;
8364 cache
8365 .get(agent_id)
8366 .map(|entry| (entry.machine_id, entry.cert_not_after))
8367 .ok_or(error::NetworkError::PeerNotVerified {
8368 agent_id: agent_id.0,
8369 })?
8370 };
8371 let expired = identity::is_expired(cert_not_after, Self::unix_timestamp_secs());
8376
8377 let trust_decision = {
8378 let contacts = self.contact_store.read().await;
8379 let evaluator = trust::TrustEvaluator::new(&contacts);
8380 Some(evaluator.evaluate(&trust::TrustContext {
8381 agent_id,
8382 machine_id: &machine_id,
8383 }))
8384 };
8385 let (revoked_agent, revoked_machine) = {
8386 let revoked = self.revocation_set.read().await;
8387 (
8388 revoked.is_agent_revoked(agent_id),
8389 revoked.is_machine_revoked(&machine_id),
8390 )
8391 };
8392 streams::stream_gate(
8393 agent_id,
8394 trust_decision,
8395 revoked_agent,
8396 revoked_machine,
8397 expired,
8398 )?;
8399
8400 let network = self
8401 .network
8402 .as_ref()
8403 .ok_or_else(|| error::NetworkError::NodeError("network not initialized".to_string()))?;
8404 let peer = ant_quic::PeerId(machine_id.0);
8405 let (mut send, recv) = network.open_bi(&peer).await?;
8406 streams::write_protocol_prefix(&mut send, protocol).await?;
8407 tracing::info!(
8408 target: "x0x::streams",
8409 agent = %hex::encode(agent_id.as_bytes()),
8410 machine = %hex::encode(machine_id.as_bytes()),
8411 protocol = ?protocol,
8412 "outbound peer stream opened (identity gate cleared)"
8413 );
8414 Ok(streams::PeerStream::new(
8415 vec![*agent_id],
8416 machine_id,
8417 protocol,
8418 send,
8419 recv,
8420 ))
8421 }
8422
8423 pub async fn next_incoming_stream(&self) -> Option<streams::PeerStream> {
8428 let mut rx = self.stream_accept.receiver().lock().await;
8429 rx.recv().await
8430 }
8431
8432 fn start_stream_accept_loop(&self) {
8441 if !self.stream_accept.start_once() {
8442 return;
8443 }
8444 let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
8445 return;
8446 };
8447 let discovery_cache = std::sync::Arc::clone(&self.identity_discovery_cache);
8448 let contact_store = std::sync::Arc::clone(&self.contact_store);
8449 let revocation_set = std::sync::Arc::clone(&self.revocation_set);
8450 let incoming = std::sync::Arc::clone(&self.stream_accept);
8451 let token = self.shutdown_token.clone();
8452
8453 self.spawn_tracked(async move {
8454 tracing::info!(target: "x0x::streams", "byte-stream accept loop started");
8455 loop {
8456 let accepted = tokio::select! {
8457 _ = token.cancelled() => break,
8458 r = network.accept_bi() => r,
8459 };
8460 let (ant_peer_id, send, mut recv) = match accepted {
8461 Ok(triple) => triple,
8462 Err(e) => {
8463 tracing::warn!(target: "x0x::streams", error=%e, "accept_bi failed; continuing");
8464 continue;
8465 }
8466 };
8467 let machine_id = identity::MachineId(ant_peer_id.0);
8468
8469 let agents: Vec<(identity::AgentId, Option<u64>)> = {
8478 let cache = discovery_cache.read().await;
8479 let mut found: Vec<(identity::AgentId, Option<u64>)> = cache
8480 .values()
8481 .filter(|a| a.machine_id == machine_id)
8482 .map(|a| (a.agent_id, a.cert_not_after))
8483 .collect();
8484 found.sort_by_key(|(a, _)| a.0);
8487 found
8488 };
8489 if agents.is_empty() {
8490 tracing::info!(
8491 target: "x0x::streams",
8492 machine = %hex::encode(machine_id.as_bytes()),
8493 outcome = "deny_not_verified",
8494 "inbound stream from machine with no known agent — denied"
8495 );
8496 continue;
8497 }
8498 let now_secs = Agent::unix_timestamp_secs();
8499 let mut gate_denied: Option<(identity::AgentId, error::NetworkError)> = None;
8500 for (agent_id, cert_not_after) in &agents {
8501 let expired = identity::is_expired(*cert_not_after, now_secs);
8504 let trust_decision = {
8505 let contacts = contact_store.read().await;
8506 let evaluator = trust::TrustEvaluator::new(&contacts);
8507 Some(evaluator.evaluate(&trust::TrustContext {
8508 agent_id,
8509 machine_id: &machine_id,
8510 }))
8511 };
8512 let (revoked_agent, revoked_machine) = {
8513 let revoked = revocation_set.read().await;
8514 (
8515 revoked.is_agent_revoked(agent_id),
8516 revoked.is_machine_revoked(&machine_id),
8517 )
8518 };
8519 if let Err(e) = streams::stream_gate(
8520 agent_id,
8521 trust_decision,
8522 revoked_agent,
8523 revoked_machine,
8524 expired,
8525 ) {
8526 gate_denied = Some((*agent_id, e));
8527 break;
8528 }
8529 }
8530 if let Some((agent_id, e)) = gate_denied {
8531 tracing::info!(
8532 target: "x0x::streams",
8533 agent = %hex::encode(agent_id.as_bytes()),
8534 machine = %hex::encode(machine_id.as_bytes()),
8535 agent_count = agents.len(),
8536 outcome = "deny_gate",
8537 error = %e,
8538 "inbound stream denied at identity gate (one agent on the machine failed)"
8539 );
8540 continue;
8541 }
8542
8543 let agents: Vec<identity::AgentId> =
8546 agents.into_iter().map(|(a, _)| a).collect();
8547
8548 let incoming_for_task = std::sync::Arc::clone(&incoming);
8555 tokio::spawn(async move {
8556 let protocol = match tokio::time::timeout(
8559 streams::PREFIX_READ_TIMEOUT,
8560 streams::read_protocol_prefix(&mut recv),
8561 )
8562 .await
8563 {
8564 Ok(Ok(p)) => p,
8565 Ok(Err(e)) => {
8566 tracing::info!(
8567 target: "x0x::streams",
8568 machine = %hex::encode(machine_id.as_bytes()),
8569 outcome = "deny_protocol",
8570 error = %e,
8571 "inbound stream protocol prefix rejected"
8572 );
8573 return;
8574 }
8575 Err(_) => {
8576 tracing::info!(
8577 target: "x0x::streams",
8578 machine = %hex::encode(machine_id.as_bytes()),
8579 outcome = "deny_prefix_timeout",
8580 "inbound stream prefix byte timed out — resetting"
8581 );
8582 return;
8583 }
8584 };
8585 let peer_stream =
8586 streams::PeerStream::new(agents, machine_id, protocol, send, recv);
8587 if incoming_for_task.sender().try_send(peer_stream).is_err() {
8590 tracing::debug!(
8591 target: "x0x::streams",
8592 "incoming-stream channel full; dropping accepted stream"
8593 );
8594 }
8595 });
8596 }
8597 });
8598 }
8599
8600 pub async fn start_identity_heartbeat(&self) -> error::Result<()> {
8608 let mut handle_guard = self.heartbeat_handle.lock().await;
8609 if self.shutdown_token.is_cancelled() {
8615 return Ok(());
8616 }
8617 if handle_guard.is_some() {
8618 return Ok(());
8619 }
8620 let Some(runtime) = self.gossip_runtime.as_ref().map(std::sync::Arc::clone) else {
8621 return Err(error::IdentityError::Storage(std::io::Error::other(
8622 "gossip runtime not initialized — cannot start heartbeat",
8623 )));
8624 };
8625 let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
8626 return Err(error::IdentityError::Storage(std::io::Error::other(
8627 "network not initialized — cannot start heartbeat",
8628 )));
8629 };
8630 let allow_local_discovery_addrs = allow_local_discovery_addresses(network.config());
8631 let ctx = HeartbeatContext {
8632 identity: std::sync::Arc::clone(&self.identity),
8633 runtime,
8634 network,
8635 interval_secs: self.heartbeat_interval_secs,
8636 cache: std::sync::Arc::clone(&self.identity_discovery_cache),
8637 machine_cache: std::sync::Arc::clone(&self.machine_discovery_cache),
8638 user_identity_consented: std::sync::Arc::clone(&self.user_identity_consented),
8639 allow_local_discovery_addrs,
8640 revocation_set: std::sync::Arc::clone(&self.revocation_set),
8641 };
8642 let handle = tokio::task::spawn(async move {
8643 let mut ticker =
8644 tokio::time::interval(std::time::Duration::from_secs(ctx.interval_secs));
8645 ticker.tick().await; loop {
8647 ticker.tick().await;
8648 if let Err(e) = ctx.announce().await {
8649 tracing::warn!("identity heartbeat announce failed: {e}");
8650 }
8651 }
8652 });
8653 *handle_guard = Some(handle);
8654 Ok(())
8655 }
8656
8657 pub async fn advertise_identity(&self, validity_ms: u64) -> error::Result<()> {
8687 use saorsa_gossip_rendezvous::{Capability, ProviderSummary};
8688
8689 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
8690 error::IdentityError::Storage(std::io::Error::other(
8691 "gossip runtime not initialized — cannot advertise identity",
8692 ))
8693 })?;
8694
8695 let peer_id = runtime.peer_id();
8696 let addresses = self.announcement_addresses();
8697 let addr_bytes = bincode::serialize(&addresses).map_err(|e| {
8698 error::IdentityError::Serialization(format!(
8699 "failed to serialize addresses for rendezvous: {e}"
8700 ))
8701 })?;
8702
8703 let mut summary = ProviderSummary::new(
8704 self.agent_id().0,
8705 peer_id,
8706 vec![Capability::Identity],
8707 validity_ms,
8708 )
8709 .with_extensions(addr_bytes);
8710
8711 summary
8712 .sign_raw(self.identity.machine_keypair().secret_key().as_bytes())
8713 .map_err(|e| {
8714 error::IdentityError::Storage(std::io::Error::other(format!(
8715 "failed to sign rendezvous summary: {e}"
8716 )))
8717 })?;
8718
8719 let cbor_bytes = summary.to_cbor().map_err(|e| {
8720 error::IdentityError::Serialization(format!(
8721 "failed to CBOR-encode rendezvous summary: {e}"
8722 ))
8723 })?;
8724
8725 let topic = rendezvous_shard_topic_for_agent(&self.agent_id());
8726 runtime
8727 .pubsub()
8728 .publish(topic, bytes::Bytes::from(cbor_bytes))
8729 .await
8730 .map_err(|e| {
8731 error::IdentityError::Storage(std::io::Error::other(format!(
8732 "failed to publish rendezvous summary: {e}"
8733 )))
8734 })?;
8735
8736 self.rendezvous_advertised
8737 .store(true, std::sync::atomic::Ordering::Relaxed);
8738 Ok(())
8739 }
8740
8741 pub async fn find_agent_rendezvous(
8754 &self,
8755 agent_id: identity::AgentId,
8756 timeout_secs: u64,
8757 ) -> error::Result<Option<Vec<std::net::SocketAddr>>> {
8758 use saorsa_gossip_rendezvous::ProviderSummary;
8759
8760 let runtime = match self.gossip_runtime.as_ref() {
8761 Some(r) => r,
8762 None => return Ok(None),
8763 };
8764
8765 let topic = rendezvous_shard_topic_for_agent(&agent_id);
8766 let mut sub = runtime.pubsub().subscribe(topic).await;
8767 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
8768
8769 loop {
8770 if tokio::time::Instant::now() >= deadline {
8771 break;
8772 }
8773 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
8774 tokio::select! {
8775 Some(msg) = sub.recv() => {
8776 let summary = match ProviderSummary::from_cbor(&msg.payload) {
8777 Ok(s) => s,
8778 Err(_) => continue,
8779 };
8780 if summary.target != agent_id.0 {
8781 continue;
8782 }
8783 let cached_pub = self
8789 .identity_discovery_cache
8790 .read()
8791 .await
8792 .get(&agent_id)
8793 .map(|e| e.machine_public_key.clone());
8794 if let Some(pub_bytes) = cached_pub {
8795 if !pub_bytes.is_empty()
8796 && !summary.verify_raw(&pub_bytes).unwrap_or(false)
8797 {
8798 tracing::warn!(
8799 "Rendezvous summary signature verification failed for agent {:?}; discarding",
8800 agent_id
8801 );
8802 continue;
8803 }
8804 }
8805 let addrs: Vec<std::net::SocketAddr> = summary
8807 .extensions
8808 .as_deref()
8809 .and_then(|b| {
8810 use bincode::Options;
8811 bincode::DefaultOptions::new()
8812 .with_fixint_encoding()
8813 .with_limit(crate::network::MAX_MESSAGE_DESERIALIZE_SIZE)
8814 .deserialize(b)
8815 .ok()
8816 })
8817 .unwrap_or_default();
8818 if !addrs.is_empty() {
8819 return Ok(Some(addrs));
8820 }
8821 }
8822 _ = tokio::time::sleep(remaining) => break,
8823 }
8824 }
8825
8826 Ok(None)
8827 }
8828
8829 #[doc(hidden)]
8849 pub fn insert_capability_for_testing(
8850 &self,
8851 agent_id: identity::AgentId,
8852 machine_id: identity::MachineId,
8853 capabilities: dm::DmCapabilities,
8854 ) {
8855 self.capability_store
8856 .insert(agent_id, machine_id, capabilities, dm::now_unix_ms());
8857 }
8858
8859 #[doc(hidden)]
8863 pub async fn insert_discovered_agent_for_testing(&self, agent: DiscoveredAgent) {
8864 let agent_id = agent.agent_id;
8865 let machine_id = agent.machine_id;
8866 upsert_discovered_machine_from_agent(&self.machine_discovery_cache, &agent).await;
8867 upsert_discovered_agent(&self.identity_discovery_cache, agent).await;
8868
8869 if machine_id.0 != [0u8; 32] {
8870 self.direct_messaging
8871 .register_agent(agent_id, machine_id)
8872 .await;
8873 if let Some(ref network) = self.network {
8874 let ant_peer_id = ant_quic::PeerId(machine_id.0);
8875 if network.is_connected(&ant_peer_id).await {
8876 self.direct_messaging
8877 .mark_connected(agent_id, machine_id)
8878 .await;
8879 }
8880 }
8881 }
8882 }
8883
8884 #[doc(hidden)]
8891 pub async fn set_contact_trusted_for_testing(&self, agent_id: identity::AgentId) {
8892 let mut store = self.contact_store.write().await;
8893 store.add(contacts::Contact {
8894 agent_id,
8895 trust_level: contacts::TrustLevel::Trusted,
8896 label: None,
8897 added_at: 0,
8898 last_seen: None,
8899 identity_type: contacts::IdentityType::Anonymous,
8900 machines: Vec::new(),
8901 dm_capabilities: None,
8902 });
8903 }
8904
8905 #[doc(hidden)]
8925 pub async fn push_relayed_dm_for_testing(
8926 &self,
8927 from_peer: ant_quic::PeerId,
8928 relayed: peer_relay::RelayedDm,
8929 ) -> bool {
8930 let Some(ref network) = self.network else {
8931 return false;
8932 };
8933 let sender_agent_id = relayed.header.sender_agent_id;
8934 network
8935 .test_relayed_dm_sender()
8936 .send((from_peer, sender_agent_id, relayed))
8937 .await
8938 .is_ok()
8939 }
8940
8941 pub async fn create_task_list(&self, name: &str, topic: &str) -> error::Result<TaskListHandle> {
8965 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
8966 error::IdentityError::Storage(std::io::Error::other(
8967 "gossip runtime not initialized - configure agent with network first",
8968 ))
8969 })?;
8970
8971 let peer_id = runtime.peer_id();
8972 let list_id = crdt::TaskListId::from_content(name, &self.agent_id(), 0);
8973 let task_list = crdt::TaskList::new(list_id, name.to_string(), peer_id);
8974
8975 let sync = crdt::TaskListSync::new(
8976 task_list,
8977 std::sync::Arc::clone(runtime.pubsub()),
8978 topic.to_string(),
8979 peer_id,
8980 )
8981 .map_err(|e| {
8982 error::IdentityError::Storage(std::io::Error::other(format!(
8983 "task list sync creation failed: {}",
8984 e
8985 )))
8986 })?;
8987
8988 let sync = std::sync::Arc::new(sync);
8989 sync.start_with_spawner(|fut| self.spawn_tracked(fut))
8990 .await
8991 .map_err(|e| {
8992 error::IdentityError::Storage(std::io::Error::other(format!(
8993 "task list sync start failed: {}",
8994 e
8995 )))
8996 })?;
8997
8998 Ok(TaskListHandle {
8999 sync,
9000 agent_id: self.agent_id(),
9001 peer_id,
9002 })
9003 }
9004
9005 pub async fn join_task_list(&self, topic: &str) -> error::Result<TaskListHandle> {
9028 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
9029 error::IdentityError::Storage(std::io::Error::other(
9030 "gossip runtime not initialized - configure agent with network first",
9031 ))
9032 })?;
9033
9034 let peer_id = runtime.peer_id();
9035 let list_id = crdt::TaskListId::from_content(topic, &self.agent_id(), 0);
9037 let task_list = crdt::TaskList::new(list_id, String::new(), peer_id);
9038
9039 let sync = crdt::TaskListSync::new(
9040 task_list,
9041 std::sync::Arc::clone(runtime.pubsub()),
9042 topic.to_string(),
9043 peer_id,
9044 )
9045 .map_err(|e| {
9046 error::IdentityError::Storage(std::io::Error::other(format!(
9047 "task list sync creation failed: {}",
9048 e
9049 )))
9050 })?;
9051
9052 let sync = std::sync::Arc::new(sync);
9053 sync.start_with_spawner(|fut| self.spawn_tracked(fut))
9054 .await
9055 .map_err(|e| {
9056 error::IdentityError::Storage(std::io::Error::other(format!(
9057 "task list sync start failed: {}",
9058 e
9059 )))
9060 })?;
9061
9062 Ok(TaskListHandle {
9063 sync,
9064 agent_id: self.agent_id(),
9065 peer_id,
9066 })
9067 }
9068}
9069
9070impl AgentBuilder {
9071 pub fn with_machine_key<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
9083 self.machine_key_path = Some(path.as_ref().to_path_buf());
9084 self
9085 }
9086
9087 pub fn with_agent_key(mut self, keypair: identity::AgentKeypair) -> Self {
9106 self.agent_keypair = Some(keypair);
9107 self
9108 }
9109
9110 pub fn with_agent_key_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
9125 self.agent_key_path = Some(path.as_ref().to_path_buf());
9126 self
9127 }
9128
9129 #[must_use]
9144 pub fn with_agent_cert_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
9145 self.agent_cert_path = Some(path.as_ref().to_path_buf());
9146 self
9147 }
9148
9149 pub fn with_network_config(mut self, config: network::NetworkConfig) -> Self {
9163 self.network_config = Some(config);
9164 self
9165 }
9166
9167 #[must_use]
9174 pub fn with_gossip_config(mut self, config: gossip::GossipConfig) -> Self {
9175 self.gossip_config = Some(config);
9176 self
9177 }
9178
9179 pub fn with_peer_cache_dir<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
9185 self.peer_cache_dir = Some(path.as_ref().to_path_buf());
9186 self
9187 }
9188
9189 pub fn with_peer_cache_disabled(mut self) -> Self {
9198 self.disable_peer_cache = true;
9199 self
9200 }
9201
9202 pub fn with_user_key(mut self, keypair: identity::UserKeypair) -> Self {
9220 self.user_keypair = Some(keypair);
9221 self
9222 }
9223
9224 pub fn with_user_key_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
9240 self.user_key_path = Some(path.as_ref().to_path_buf());
9241 self
9242 }
9243
9244 #[must_use]
9252 pub fn with_heartbeat_interval(mut self, secs: u64) -> Self {
9253 self.heartbeat_interval_secs = Some(secs);
9254 self
9255 }
9256
9257 #[must_use]
9268 pub fn with_identity_ttl(mut self, secs: u64) -> Self {
9269 self.identity_ttl_secs = Some(secs);
9270 self
9271 }
9272
9273 #[must_use]
9275 pub fn with_presence_beacon_interval(mut self, secs: u64) -> Self {
9276 self.presence_beacon_interval_secs = Some(secs);
9277 self
9278 }
9279
9280 #[must_use]
9282 pub fn with_presence_event_poll_interval(mut self, secs: u64) -> Self {
9283 self.presence_event_poll_interval_secs = Some(secs);
9284 self
9285 }
9286
9287 #[must_use]
9289 pub fn with_presence_offline_timeout(mut self, secs: u64) -> Self {
9290 self.presence_offline_timeout_secs = Some(secs);
9291 self
9292 }
9293
9294 #[must_use]
9303 pub fn with_contact_store_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
9304 self.contact_store_path = Some(path.as_ref().to_path_buf());
9305 self
9306 }
9307
9308 #[must_use]
9320 pub fn with_identity_dir<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
9321 self.identity_dir = Some(path.as_ref().to_path_buf());
9322 self
9323 }
9324
9325 pub async fn build(self) -> error::Result<Agent> {
9341 let machine_keypair = if let Some(path) = self.machine_key_path {
9343 match storage::load_machine_keypair_from(&path).await {
9345 Ok(kp) => kp,
9346 Err(_) => {
9347 let kp = identity::MachineKeypair::generate()?;
9349 storage::save_machine_keypair_to(&kp, &path).await?;
9350 kp
9351 }
9352 }
9353 } else if storage::machine_keypair_exists().await {
9354 storage::load_machine_keypair().await?
9356 } else {
9357 let kp = identity::MachineKeypair::generate()?;
9359 storage::save_machine_keypair(&kp).await?;
9360 kp
9361 };
9362
9363 let agent_keypair = if let Some(kp) = self.agent_keypair {
9365 kp
9367 } else if let Some(path) = self.agent_key_path {
9368 match storage::load_agent_keypair_from(&path).await {
9370 Ok(kp) => kp,
9371 Err(_) => {
9372 let kp = identity::AgentKeypair::generate()?;
9373 storage::save_agent_keypair_to(&kp, &path).await?;
9374 kp
9375 }
9376 }
9377 } else if storage::agent_keypair_exists().await {
9378 storage::load_agent_keypair_default().await?
9380 } else {
9381 let kp = identity::AgentKeypair::generate()?;
9383 storage::save_agent_keypair_default(&kp).await?;
9384 kp
9385 };
9386
9387 let user_keypair = if let Some(kp) = self.user_keypair {
9389 Some(kp)
9390 } else if let Some(path) = self.user_key_path {
9391 storage::load_user_keypair_from(&path).await.ok()
9393 } else if storage::user_keypair_exists().await {
9394 storage::load_user_keypair().await.ok()
9396 } else {
9397 None
9398 };
9399
9400 let identity = if let Some(user_kp) = user_keypair {
9418 let cert_path = self.agent_cert_path.clone();
9419 let existing_cert = if let Some(ref p) = cert_path {
9420 if tokio::fs::try_exists(p).await.unwrap_or(false) {
9421 storage::load_agent_certificate_from(p).await.ok()
9422 } else {
9423 None
9424 }
9425 } else if storage::agent_certificate_exists().await {
9426 storage::load_agent_certificate().await.ok()
9427 } else {
9428 None
9429 };
9430
9431 let cert_still_valid = existing_cert.as_ref().is_some_and(|c| {
9432 let user_match = c
9433 .user_id()
9434 .map(|uid| uid == user_kp.user_id())
9435 .unwrap_or(false);
9436 let agent_match = c
9437 .agent_id()
9438 .map(|aid| aid == agent_keypair.agent_id())
9439 .unwrap_or(false);
9440 user_match && agent_match
9441 });
9442
9443 let cert = if cert_still_valid {
9444 existing_cert.ok_or_else(|| {
9445 error::IdentityError::Storage(std::io::Error::other(
9446 "agent certificate validity check succeeded with no certificate loaded",
9447 ))
9448 })?
9449 } else {
9450 let new_cert = identity::AgentCertificate::issue(&user_kp, &agent_keypair)?;
9451 if let Some(ref p) = cert_path {
9452 storage::save_agent_certificate_to(&new_cert, p).await?;
9453 } else {
9454 storage::save_agent_certificate(&new_cert).await?;
9455 }
9456 new_cert
9457 };
9458 identity::Identity::new_with_user(machine_keypair, agent_keypair, user_kp, cert)
9459 } else {
9460 identity::Identity::new(machine_keypair, agent_keypair)
9461 };
9462
9463 let bootstrap_cache = if self.network_config.is_some() && !self.disable_peer_cache {
9466 let cache_dir = self.peer_cache_dir.unwrap_or_else(|| {
9467 dirs::home_dir()
9468 .unwrap_or_else(|| std::path::PathBuf::from("."))
9469 .join(".x0x")
9470 .join("peers")
9471 });
9472 let config = ant_quic::BootstrapCacheConfig::builder()
9473 .cache_dir(cache_dir)
9474 .min_peers_to_save(1)
9475 .build();
9476 match ant_quic::BootstrapCache::open(config).await {
9477 Ok(cache) => {
9478 let cache = std::sync::Arc::new(cache);
9479 std::sync::Arc::clone(&cache).start_maintenance();
9480 Some(cache)
9481 }
9482 Err(e) => {
9483 tracing::warn!("Failed to open bootstrap cache: {e}");
9484 None
9485 }
9486 }
9487 } else {
9488 None
9489 };
9490
9491 let machine_keypair = {
9494 let pk = ant_quic::MlDsaPublicKey::from_bytes(
9495 identity.machine_keypair().public_key().as_bytes(),
9496 )
9497 .map_err(|e| {
9498 error::IdentityError::Storage(std::io::Error::other(format!(
9499 "invalid machine public key: {e}"
9500 )))
9501 })?;
9502 let sk = ant_quic::MlDsaSecretKey::from_bytes(
9503 identity.machine_keypair().secret_key().as_bytes(),
9504 )
9505 .map_err(|e| {
9506 error::IdentityError::Storage(std::io::Error::other(format!(
9507 "invalid machine secret key: {e}"
9508 )))
9509 })?;
9510 Some((pk, sk))
9511 };
9512
9513 let peer_relay_config = self
9518 .network_config
9519 .as_ref()
9520 .map(|cfg| cfg.peer_relay.clone())
9521 .unwrap_or_default();
9522 let mut parsed_relay_candidates = Vec::with_capacity(peer_relay_config.candidates.len());
9523 for hex_str in &peer_relay_config.candidates {
9524 let trimmed = hex_str.trim();
9525 let bytes = hex::decode(trimmed).map_err(|e| {
9526 error::IdentityError::Storage(std::io::Error::other(format!(
9527 "invalid relay candidate hex {trimmed:?}: {e}"
9528 )))
9529 })?;
9530 let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
9531 error::IdentityError::Storage(std::io::Error::other(format!(
9532 "relay candidate must be 32 bytes (got {} bytes)",
9533 v.len()
9534 )))
9535 })?;
9536 parsed_relay_candidates.push(identity::AgentId(arr));
9537 }
9538 let peer_relay = std::sync::Arc::new(peer_relay::PeerRelay::with_policy(
9539 peer_relay_config.to_policy(),
9540 ));
9541 let relay_candidates =
9542 std::sync::Arc::new(tokio::sync::RwLock::new(parsed_relay_candidates));
9543
9544 let identity_discovery_cache: std::sync::Arc<
9549 tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
9550 > = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
9551
9552 let network = if let Some(config) = self.network_config {
9553 let node = network::NetworkNode::new(config, bootstrap_cache.clone(), machine_keypair)
9554 .await
9555 .map_err(|e| {
9556 error::IdentityError::Storage(std::io::Error::other(format!(
9557 "network initialization failed: {}",
9558 e
9559 )))
9560 })?;
9561
9562 debug_assert_eq!(
9564 node.peer_id().0,
9565 identity.machine_id().0,
9566 "ant-quic PeerId must equal MachineId after identity unification"
9567 );
9568
9569 Some(std::sync::Arc::new(node))
9570 } else {
9571 None
9572 };
9573
9574 let revocation_set = std::sync::Arc::new(tokio::sync::RwLock::new(
9579 storage::load_revocation_set(self.identity_dir.as_deref()).await,
9580 ));
9581
9582 let contacts_path = self.contact_store_path.unwrap_or_else(|| {
9586 dirs::home_dir()
9587 .unwrap_or_else(|| std::path::PathBuf::from("."))
9588 .join(".x0x")
9589 .join("contacts.json")
9590 });
9591 let contact_store = std::sync::Arc::new(tokio::sync::RwLock::new(
9592 contacts::ContactStore::new(contacts_path),
9593 ));
9594
9595 if let Some(ref net) = network {
9602 spawn_relay_dm_listener(
9603 std::sync::Arc::clone(net),
9604 std::sync::Arc::clone(&peer_relay),
9605 std::sync::Arc::clone(&identity_discovery_cache),
9606 std::sync::Arc::clone(&revocation_set),
9607 std::sync::Arc::clone(&contact_store),
9608 identity.agent_id(),
9609 );
9610 }
9611
9612 let signing_ctx = std::sync::Arc::new(gossip::SigningContext::from_keypair(
9614 identity.agent_keypair(),
9615 ));
9616
9617 let gossip_runtime = if let Some(ref net) = network {
9619 let runtime = gossip::GossipRuntime::new(
9620 self.gossip_config.unwrap_or_default(),
9621 std::sync::Arc::clone(net),
9622 Some(signing_ctx),
9623 )
9624 .await
9625 .map_err(|e| {
9626 error::IdentityError::Storage(std::io::Error::other(format!(
9627 "gossip runtime initialization failed: {}",
9628 e
9629 )))
9630 })?;
9631 Some(std::sync::Arc::new(runtime))
9632 } else {
9633 None
9634 };
9635
9636 let gossip_cache_adapter = bootstrap_cache.as_ref().map(|cache| {
9638 saorsa_gossip_coordinator::GossipCacheAdapter::new(std::sync::Arc::clone(cache))
9639 });
9640
9641 let direct_messaging = std::sync::Arc::new(direct::DirectMessaging::new());
9643
9644 let presence = if let Some(ref net) = network {
9646 let peer_id = saorsa_gossip_transport::GossipTransport::local_peer_id(net.as_ref());
9647 let mut presence_config = presence::PresenceConfig::default();
9648 if let Some(secs) = self.presence_beacon_interval_secs {
9649 presence_config.beacon_interval_secs = secs;
9650 }
9651 if let Some(secs) = self.presence_event_poll_interval_secs {
9652 presence_config.event_poll_interval_secs = secs;
9653 }
9654 if let Some(secs) = self.presence_offline_timeout_secs {
9655 presence_config.adaptive_timeout_fallback_secs = secs;
9656 }
9657 let pw = presence::PresenceWrapper::new(
9663 peer_id,
9664 identity.machine_keypair().to_bytes(),
9665 std::sync::Arc::clone(net),
9666 presence_config,
9667 bootstrap_cache.clone(),
9668 )
9669 .map_err(|e| {
9670 error::IdentityError::Storage(std::io::Error::other(format!(
9671 "presence initialization failed: {}",
9672 e
9673 )))
9674 })?;
9675 let pw_arc = std::sync::Arc::new(pw);
9676 if let Some(ref rt) = gossip_runtime {
9678 rt.set_presence(std::sync::Arc::clone(&pw_arc));
9679 }
9680 Some(pw_arc)
9681 } else {
9682 None
9683 };
9684
9685 Ok(Agent {
9688 identity: std::sync::Arc::new(identity),
9689 network,
9690 gossip_runtime,
9691 bootstrap_cache,
9692 gossip_cache_adapter,
9693 identity_discovery_cache,
9694 authenticated_machine_bindings: std::sync::Arc::new(tokio::sync::RwLock::new(
9695 dm_inbox::AuthenticatedMachineBindingCache::default(),
9696 )),
9697 machine_discovery_cache: std::sync::Arc::new(tokio::sync::RwLock::new(
9698 std::collections::HashMap::new(),
9699 )),
9700 user_discovery_cache: std::sync::Arc::new(tokio::sync::RwLock::new(
9701 std::collections::HashMap::new(),
9702 )),
9703 identity_listener_started: std::sync::atomic::AtomicBool::new(false),
9704 heartbeat_interval_secs: self
9705 .heartbeat_interval_secs
9706 .unwrap_or(IDENTITY_HEARTBEAT_INTERVAL_SECS),
9707 identity_ttl_secs: self.identity_ttl_secs.unwrap_or(IDENTITY_TTL_SECS),
9708 heartbeat_handle: tokio::sync::Mutex::new(None),
9709 discovery_cache_reaper_handle: tokio::sync::Mutex::new(None),
9710 rendezvous_advertised: std::sync::atomic::AtomicBool::new(false),
9711 contact_store,
9712 direct_messaging,
9713 network_event_listener_started: std::sync::atomic::AtomicBool::new(false),
9714 direct_listener_started: std::sync::atomic::AtomicBool::new(false),
9715 presence,
9716 user_identity_consented: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
9717 capability_store: std::sync::Arc::new(dm_capability::CapabilityStore::new()),
9718 dm_capabilities_tx: std::sync::Arc::new({
9719 let (tx, _rx) = tokio::sync::watch::channel(dm::DmCapabilities::pending());
9720 tx
9721 }),
9722 dm_inflight_acks: std::sync::Arc::new(dm::InFlightAcks::new()),
9723 recent_delivery_cache: std::sync::Arc::new(dm::RecentDeliveryCache::with_defaults()),
9724 capability_advert_service: tokio::sync::Mutex::new(None),
9725 dm_inbox_service: tokio::sync::Mutex::new(None),
9726 revocation_set,
9727 identity_dir: self.identity_dir,
9728 shutdown_token: tokio_util::sync::CancellationToken::new(),
9729 tracked_tasks: std::sync::Arc::new(std::sync::Mutex::new(TrackedTasks {
9730 closed: false,
9731 handles: Vec::new(),
9732 })),
9733 peer_relay,
9734 relay_candidates,
9735 stream_accept: std::sync::Arc::new(streams::StreamAccept::new(256)),
9736 })
9737 }
9738}
9739
9740#[derive(Clone)]
9754pub struct TaskListHandle {
9755 sync: std::sync::Arc<crdt::TaskListSync>,
9756 agent_id: identity::AgentId,
9757 peer_id: saorsa_gossip_types::PeerId,
9758}
9759
9760impl std::fmt::Debug for TaskListHandle {
9761 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9762 f.debug_struct("TaskListHandle")
9763 .field("agent_id", &self.agent_id)
9764 .field("peer_id", &self.peer_id)
9765 .finish_non_exhaustive()
9766 }
9767}
9768
9769impl TaskListHandle {
9770 pub async fn add_task(
9785 &self,
9786 title: String,
9787 description: String,
9788 ) -> error::Result<crdt::TaskId> {
9789 let (task_id, delta) = {
9790 let mut list = self.sync.write().await;
9791 let seq = list.next_seq();
9792 let task_id = crdt::TaskId::new(&title, &self.agent_id, seq);
9793 let metadata = crdt::TaskMetadata::new(title, description, 128, self.agent_id, seq);
9794 let task = crdt::TaskItem::new(task_id, metadata, self.peer_id);
9795 list.add_task(task.clone(), self.peer_id, seq)
9796 .map_err(|e| {
9797 error::IdentityError::Storage(std::io::Error::other(format!(
9798 "add_task failed: {}",
9799 e
9800 )))
9801 })?;
9802 let tag = (self.peer_id, seq);
9803 let delta = crdt::TaskListDelta::for_add(task_id, task, tag, list.current_version());
9804 (task_id, delta)
9805 };
9806 if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
9808 tracing::warn!("failed to publish add_task delta: {}", e);
9809 }
9810 Ok(task_id)
9811 }
9812
9813 pub async fn claim_task(&self, task_id: crdt::TaskId) -> error::Result<()> {
9823 let delta = {
9824 let mut list = self.sync.write().await;
9825 let seq = list.next_seq();
9826 list.claim_task(&task_id, self.agent_id, self.peer_id, seq)
9827 .map_err(|e| {
9828 error::IdentityError::Storage(std::io::Error::other(format!(
9829 "claim_task failed: {}",
9830 e
9831 )))
9832 })?;
9833 let full_task = list
9835 .get_task(&task_id)
9836 .ok_or_else(|| {
9837 error::IdentityError::Storage(std::io::Error::other(
9838 "task disappeared after claim",
9839 ))
9840 })?
9841 .clone();
9842 crdt::TaskListDelta::for_state_change(task_id, full_task, list.current_version())
9843 };
9844 if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
9845 tracing::warn!("failed to publish claim_task delta: {}", e);
9846 }
9847 Ok(())
9848 }
9849
9850 pub async fn complete_task(&self, task_id: crdt::TaskId) -> error::Result<()> {
9860 let delta = {
9861 let mut list = self.sync.write().await;
9862 let seq = list.next_seq();
9863 list.complete_task(&task_id, self.agent_id, self.peer_id, seq)
9864 .map_err(|e| {
9865 error::IdentityError::Storage(std::io::Error::other(format!(
9866 "complete_task failed: {}",
9867 e
9868 )))
9869 })?;
9870 let full_task = list
9871 .get_task(&task_id)
9872 .ok_or_else(|| {
9873 error::IdentityError::Storage(std::io::Error::other(
9874 "task disappeared after complete",
9875 ))
9876 })?
9877 .clone();
9878 crdt::TaskListDelta::for_state_change(task_id, full_task, list.current_version())
9879 };
9880 if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
9881 tracing::warn!("failed to publish complete_task delta: {}", e);
9882 }
9883 Ok(())
9884 }
9885
9886 pub async fn list_tasks(&self) -> error::Result<Vec<TaskSnapshot>> {
9896 let list = self.sync.read().await;
9897 let tasks = list.tasks_ordered();
9898 Ok(tasks
9899 .into_iter()
9900 .map(|task| TaskSnapshot {
9901 id: *task.id(),
9902 title: task.title().to_string(),
9903 description: task.description().to_string(),
9904 state: task.current_state(),
9905 assignee: task.assignee().copied(),
9906 owner: None,
9907 priority: task.priority(),
9908 })
9909 .collect())
9910 }
9911
9912 pub async fn reorder(&self, task_ids: Vec<crdt::TaskId>) -> error::Result<()> {
9922 let delta = {
9923 let mut list = self.sync.write().await;
9924 list.reorder(task_ids.clone(), self.peer_id).map_err(|e| {
9925 error::IdentityError::Storage(std::io::Error::other(format!(
9926 "reorder failed: {}",
9927 e
9928 )))
9929 })?;
9930 crdt::TaskListDelta::for_reorder(
9933 list.ordering_register().clone(),
9934 list.current_version(),
9935 )
9936 };
9937 if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
9938 tracing::warn!("failed to publish reorder delta: {}", e);
9939 }
9940 Ok(())
9941 }
9942}
9943
9944impl Agent {
9949 pub async fn create_kv_store(&self, name: &str, topic: &str) -> error::Result<KvStoreHandle> {
9958 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
9959 error::IdentityError::Storage(std::io::Error::other(
9960 "gossip runtime not initialized - configure agent with network first",
9961 ))
9962 })?;
9963
9964 let peer_id = runtime.peer_id();
9965 let store_id = kv::KvStoreId::from_content(name, &self.agent_id());
9966 let store = kv::KvStore::new(
9967 store_id,
9968 name.to_string(),
9969 self.agent_id(),
9970 kv::AccessPolicy::Signed,
9971 );
9972
9973 let sync = kv::KvStoreSync::new(
9974 store,
9975 std::sync::Arc::clone(runtime.pubsub()),
9976 topic.to_string(),
9977 peer_id,
9978 )
9979 .map_err(|e| {
9980 error::IdentityError::Storage(std::io::Error::other(format!(
9981 "kv store sync creation failed: {e}",
9982 )))
9983 })?;
9984
9985 let sync = std::sync::Arc::new(sync);
9986 sync.start_with_spawner(|fut| self.spawn_tracked(fut))
9987 .await
9988 .map_err(|e| {
9989 error::IdentityError::Storage(std::io::Error::other(format!(
9990 "kv store sync start failed: {e}",
9991 )))
9992 })?;
9993
9994 Ok(KvStoreHandle {
9995 sync,
9996 agent_id: self.agent_id(),
9997 peer_id,
9998 })
9999 }
10000
10001 pub async fn join_kv_store(&self, topic: &str) -> error::Result<KvStoreHandle> {
10011 let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
10012 error::IdentityError::Storage(std::io::Error::other(
10013 "gossip runtime not initialized - configure agent with network first",
10014 ))
10015 })?;
10016
10017 let peer_id = runtime.peer_id();
10018 let store_id = kv::KvStoreId::from_content(topic, &self.agent_id());
10019 let store = kv::KvStore::new(
10022 store_id,
10023 String::new(),
10024 self.agent_id(),
10025 kv::AccessPolicy::Encrypted {
10026 group_id: Vec::new(),
10027 },
10028 );
10029
10030 let sync = kv::KvStoreSync::new(
10031 store,
10032 std::sync::Arc::clone(runtime.pubsub()),
10033 topic.to_string(),
10034 peer_id,
10035 )
10036 .map_err(|e| {
10037 error::IdentityError::Storage(std::io::Error::other(format!(
10038 "kv store sync creation failed: {e}",
10039 )))
10040 })?;
10041
10042 let sync = std::sync::Arc::new(sync);
10043 sync.start_with_spawner(|fut| self.spawn_tracked(fut))
10044 .await
10045 .map_err(|e| {
10046 error::IdentityError::Storage(std::io::Error::other(format!(
10047 "kv store sync start failed: {e}",
10048 )))
10049 })?;
10050
10051 Ok(KvStoreHandle {
10052 sync,
10053 agent_id: self.agent_id(),
10054 peer_id,
10055 })
10056 }
10057}
10058
10059#[derive(Clone)]
10064pub struct KvStoreHandle {
10065 sync: std::sync::Arc<kv::KvStoreSync>,
10066 agent_id: identity::AgentId,
10067 peer_id: saorsa_gossip_types::PeerId,
10068}
10069
10070impl std::fmt::Debug for KvStoreHandle {
10071 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10072 f.debug_struct("KvStoreHandle")
10073 .field("agent_id", &self.agent_id)
10074 .field("peer_id", &self.peer_id)
10075 .finish_non_exhaustive()
10076 }
10077}
10078
10079impl KvStoreHandle {
10080 #[must_use]
10082 pub fn peer_id(&self) -> saorsa_gossip_types::PeerId {
10083 self.peer_id
10084 }
10085
10086 pub async fn put(
10095 &self,
10096 key: String,
10097 value: Vec<u8>,
10098 content_type: String,
10099 ) -> error::Result<()> {
10100 let _ = self.put_with_delta(key, value, content_type).await?;
10101 Ok(())
10102 }
10103
10104 pub async fn put_with_delta(
10113 &self,
10114 key: String,
10115 value: Vec<u8>,
10116 content_type: String,
10117 ) -> error::Result<kv::KvStoreDelta> {
10118 let delta = {
10119 let mut store = self.sync.write().await;
10120 store
10121 .put(
10122 key.clone(),
10123 value.clone(),
10124 content_type.clone(),
10125 self.peer_id,
10126 )
10127 .map_err(|e| {
10128 error::IdentityError::Storage(std::io::Error::other(format!(
10129 "kv put failed: {e}",
10130 )))
10131 })?;
10132 let entry = store.get(&key).cloned();
10133 let version = store.current_version();
10134 match entry {
10135 Some(e) => {
10136 kv::KvStoreDelta::for_put(key, e, (self.peer_id, store.next_seq()), version)
10137 }
10138 None => {
10139 return Err(error::IdentityError::Storage(std::io::Error::other(
10140 "kv put succeeded but entry was not readable",
10141 )));
10142 }
10143 }
10144 };
10145 if let Err(e) = self.sync.publish_delta(self.peer_id, delta.clone()).await {
10146 tracing::warn!("failed to publish kv put delta: {e}");
10147 }
10148 Ok(delta)
10149 }
10150
10151 pub async fn get(&self, key: &str) -> error::Result<Option<KvEntrySnapshot>> {
10159 let store = self.sync.read().await;
10160 Ok(store.get(key).map(|e| KvEntrySnapshot {
10161 key: e.key.clone(),
10162 value: e.value.clone(),
10163 content_hash: hex::encode(e.content_hash),
10164 content_type: e.content_type.clone(),
10165 metadata: e.metadata.clone(),
10166 created_at: e.created_at,
10167 updated_at: e.updated_at,
10168 }))
10169 }
10170
10171 pub async fn remove(&self, key: &str) -> error::Result<()> {
10177 let _ = self.remove_with_delta(key).await?;
10178 Ok(())
10179 }
10180
10181 pub async fn remove_with_delta(&self, key: &str) -> error::Result<kv::KvStoreDelta> {
10187 let delta = {
10188 let mut store = self.sync.write().await;
10189 store.remove(key).map_err(|e| {
10190 error::IdentityError::Storage(std::io::Error::other(format!(
10191 "kv remove failed: {e}",
10192 )))
10193 })?;
10194 let mut d = kv::KvStoreDelta::new(store.current_version());
10195 d.removed
10196 .insert(key.to_string(), std::collections::HashSet::new());
10197 d
10198 };
10199 if let Err(e) = self.sync.publish_delta(self.peer_id, delta.clone()).await {
10200 tracing::warn!("failed to publish kv remove delta: {e}");
10201 }
10202 Ok(delta)
10203 }
10204
10205 pub async fn apply_remote_delta(
10211 &self,
10212 peer_id: saorsa_gossip_types::PeerId,
10213 delta: &kv::KvStoreDelta,
10214 writer: Option<identity::AgentId>,
10215 ) -> error::Result<()> {
10216 let mut store = self.sync.write().await;
10217 store
10218 .merge_delta(delta, peer_id, writer.as_ref())
10219 .map_err(|e| {
10220 error::IdentityError::Storage(std::io::Error::other(format!(
10221 "kv direct delta merge failed: {e}",
10222 )))
10223 })
10224 }
10225
10226 pub async fn keys(&self) -> error::Result<Vec<KvEntrySnapshot>> {
10232 let store = self.sync.read().await;
10233 Ok(store
10234 .active_entries()
10235 .into_iter()
10236 .map(|e| KvEntrySnapshot {
10237 key: e.key.clone(),
10238 value: e.value.clone(),
10239 content_hash: hex::encode(e.content_hash),
10240 content_type: e.content_type.clone(),
10241 metadata: e.metadata.clone(),
10242 created_at: e.created_at,
10243 updated_at: e.updated_at,
10244 })
10245 .collect())
10246 }
10247
10248 pub async fn name(&self) -> error::Result<String> {
10254 let store = self.sync.read().await;
10255 Ok(store.name().to_string())
10256 }
10257}
10258
10259#[derive(Debug, Clone, serde::Serialize)]
10261pub struct KvEntrySnapshot {
10262 pub key: String,
10264 pub value: Vec<u8>,
10266 pub content_hash: String,
10268 pub content_type: String,
10270 pub metadata: std::collections::HashMap<String, String>,
10272 pub created_at: u64,
10274 pub updated_at: u64,
10276}
10277
10278#[derive(Debug, Clone)]
10283pub struct TaskSnapshot {
10284 pub id: crdt::TaskId,
10286 pub title: String,
10288 pub description: String,
10290 pub state: crdt::CheckboxState,
10292 pub assignee: Option<identity::AgentId>,
10294 pub owner: Option<identity::UserId>,
10296 pub priority: u8,
10298}
10299
10300pub const VERSION: &str = env!("CARGO_PKG_VERSION");
10302
10303pub const NAME: &str = "x0x";
10305
10306fn spawn_relay_dm_listener(
10346 network: std::sync::Arc<network::NetworkNode>,
10347 peer_relay: std::sync::Arc<peer_relay::PeerRelay>,
10348 identity_discovery_cache: std::sync::Arc<
10349 tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
10350 >,
10351 revocation_set: std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>>,
10352 contact_store: std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>,
10353 local_agent_id: identity::AgentId,
10354) {
10355 tokio::spawn(async move {
10356 tracing::info!(target: "x0x::relay", stage = "listener", "relay-DM listener started");
10357 loop {
10358 let Some((relay_peer_id, _relay_sender_agent_id, relayed)) =
10359 network.recv_relayed_dm().await
10360 else {
10361 tracing::warn!(
10362 target: "x0x::relay",
10363 stage = "listener",
10364 "network.recv_relayed_dm channel closed - listener exiting"
10365 );
10366 break;
10367 };
10368 let now_ms = dm::now_unix_ms();
10369 let sender_agent_id = identity::AgentId(relayed.header.sender_agent_id);
10389 let (is_sender_contact, is_sender_blocked) = {
10390 let store = contact_store.read().await;
10391 match store.get(&sender_agent_id) {
10392 Some(c) => (
10393 matches!(
10394 c.trust_level,
10395 contacts::TrustLevel::Known | contacts::TrustLevel::Trusted
10396 ),
10397 c.trust_level == contacts::TrustLevel::Blocked,
10398 ),
10399 None => (false, false),
10400 }
10401 };
10402 let disposition = peer_relay.disposition_for(
10403 &relayed,
10404 &local_agent_id,
10405 now_ms,
10406 is_sender_contact,
10407 is_sender_blocked,
10408 );
10409
10410 if matches!(
10416 disposition,
10417 peer_relay::RelayDisposition::DeliverLocally
10418 | peer_relay::RelayDisposition::Forward { .. }
10419 ) {
10420 let origin = identity::AgentId(relayed.inner.sender_agent_id);
10421 let revoked = { revocation_set.read().await.is_agent_revoked(&origin) };
10422 if revoked {
10423 peer_relay.record_relay_dropped_revoked();
10424 tracing::info!(
10425 target: "x0x::relay",
10426 stage = "revoked_drop",
10427 relay_peer = ?relay_peer_id,
10428 origin = %hex::encode(origin.as_bytes()),
10429 "relayed DM dropped: origin agent is revoked"
10430 );
10431 continue;
10432 }
10433 }
10434
10435 match disposition {
10436 peer_relay::RelayDisposition::DeliverLocally => {
10437 let sender_machine_id = relayed.inner.sender_machine_id;
10438 let sender_peer_id = ant_quic::PeerId(sender_machine_id);
10439 let inner_wire = match postcard::to_allocvec(&relayed.inner) {
10440 Ok(b) => b,
10441 Err(e) => {
10442 tracing::warn!(
10443 target: "x0x::relay",
10444 stage = "deliver_local",
10445 relay_peer = ?relay_peer_id,
10446 error = %e,
10447 "failed to re-encode inner envelope for local delivery"
10448 );
10449 continue;
10450 }
10451 };
10452 let mut payload = Vec::with_capacity(32 + inner_wire.len());
10455 payload.extend_from_slice(&relayed.inner.sender_agent_id);
10456 payload.extend_from_slice(&inner_wire);
10457 if let Err(e) = network
10458 .inject_inbound_direct(sender_peer_id, bytes::Bytes::from(payload))
10459 .await
10460 {
10461 tracing::warn!(
10462 target: "x0x::relay",
10463 stage = "deliver_local",
10464 relay_peer = ?relay_peer_id,
10465 error = %e,
10466 "DeliverLocally inject onto direct channel failed"
10467 );
10468 }
10469 }
10470 peer_relay::RelayDisposition::Forward { dst_agent_id } => {
10471 let dst = identity::AgentId(dst_agent_id);
10472 let dst_machine_id = {
10473 let cache = identity_discovery_cache.read().await;
10474 cache.get(&dst).map(|d| d.machine_id)
10475 };
10476 let Some(dst_machine_id) = dst_machine_id else {
10477 tracing::warn!(
10478 target: "x0x::relay",
10479 stage = "forward",
10480 dst = %hex::encode(dst.as_bytes()),
10481 "Forward dropped: dst not in identity-discovery cache"
10482 );
10483 continue;
10484 };
10485 let inner_wire = match postcard::to_allocvec(&relayed.inner) {
10486 Ok(b) => b,
10487 Err(e) => {
10488 tracing::warn!(
10489 target: "x0x::relay",
10490 stage = "forward",
10491 dst = %hex::encode(dst.as_bytes()),
10492 error = %e,
10493 "failed to re-encode inner envelope for forward"
10494 );
10495 continue;
10496 }
10497 };
10498 let forward_bytes = u64::try_from(inner_wire.len()).unwrap_or(u64::MAX);
10499 let reservation = match peer_relay
10500 .reserve_forward(relayed.header.sender_agent_id, forward_bytes)
10501 {
10502 Ok(reservation) => reservation,
10503 Err(reason) => {
10504 tracing::debug!(
10505 target: "x0x::relay",
10506 stage = "refuse",
10507 relay_peer = ?relay_peer_id,
10508 reason = ?reason,
10509 "RelayedDm forward refused by quota admission"
10510 );
10511 continue;
10512 }
10513 };
10514 let dst_peer_id = ant_quic::PeerId(dst_machine_id.0);
10515 match network
10516 .send_direct_typed(
10517 &dst_peer_id,
10518 local_agent_id.as_bytes(),
10519 network::DIRECT_MESSAGE_STREAM_TYPE,
10520 &inner_wire,
10521 )
10522 .await
10523 {
10524 Ok(()) => reservation.commit(),
10525 Err(e) => {
10526 tracing::warn!(
10527 target: "x0x::relay",
10528 stage = "forward",
10529 dst = %hex::encode(dst.as_bytes()),
10530 error = %e,
10531 "Forward send_direct_typed failed"
10532 );
10533 }
10534 }
10535 }
10536 peer_relay::RelayDisposition::Refuse(reason) => {
10537 tracing::debug!(
10538 target: "x0x::relay",
10539 stage = "refuse",
10540 relay_peer = ?relay_peer_id,
10541 reason = ?reason,
10542 "RelayedDm refused"
10543 );
10544 }
10545 }
10546 }
10547 });
10548}
10549
10550#[cfg(test)]
10551mod tests {
10552 use super::*;
10553
10554 fn sa(s: &str) -> std::net::SocketAddr {
10555 s.parse().expect("valid SocketAddr literal in test")
10556 }
10557
10558 #[test]
10559 fn discovery_rebroadcast_is_one_shot_per_announcement_key() {
10560 let now = std::time::Instant::now();
10561 let mut state = std::collections::HashMap::new();
10562
10563 assert!(should_rebroadcast_discovery_once(
10564 &mut state,
10565 (7_u8, 42_u64),
10566 now
10567 ));
10568 assert!(!should_rebroadcast_discovery_once(
10569 &mut state,
10570 (7_u8, 42_u64),
10571 now + std::time::Duration::from_secs(20),
10572 ));
10573 assert!(should_rebroadcast_discovery_once(
10574 &mut state,
10575 (7_u8, 43_u64),
10576 now + std::time::Duration::from_secs(20),
10577 ));
10578 }
10579
10580 #[test]
10581 fn discovery_ttl_uses_local_last_seen_not_sender_timestamp() {
10582 let cutoff = 900;
10583
10584 assert!(discovery_record_is_live(100, 1_000, cutoff));
10585 assert!(discovery_record_is_live(10_000, cutoff, cutoff));
10586 assert!(!discovery_record_is_live(10_000, cutoff - 1, cutoff));
10587 }
10588
10589 #[test]
10590 fn is_publicly_advertisable_rejects_lan_and_special_scopes() {
10591 assert!(
10593 !is_publicly_advertisable(sa("127.0.0.1:5483")),
10594 "loopback v4"
10595 );
10596 assert!(!is_publicly_advertisable(sa("10.1.2.3:5483")), "rfc1918 /8");
10597 assert!(
10598 !is_publicly_advertisable(sa("172.20.0.5:5483")),
10599 "rfc1918 /12"
10600 );
10601 assert!(
10602 !is_publicly_advertisable(sa("192.168.1.5:5483")),
10603 "rfc1918 /16"
10604 );
10605 assert!(
10606 !is_publicly_advertisable(sa("169.254.1.1:5483")),
10607 "link-local v4"
10608 );
10609 assert!(
10610 !is_publicly_advertisable(sa("100.64.1.1:5483")),
10611 "CGNAT (unreachable outside carrier)"
10612 );
10613 assert!(
10614 !is_publicly_advertisable(sa("0.0.0.0:5483")),
10615 "unspecified v4"
10616 );
10617
10618 assert!(!is_publicly_advertisable(sa("[::1]:5483")), "loopback v6");
10620 assert!(
10621 !is_publicly_advertisable(sa("[fe80::1]:5483")),
10622 "link-local v6"
10623 );
10624 assert!(!is_publicly_advertisable(sa("[fd00::1]:5483")), "ULA v6");
10625
10626 assert!(
10628 !is_publicly_advertisable(sa("1.2.3.4:0")),
10629 "port 0 on global v4"
10630 );
10631
10632 assert!(is_publicly_advertisable(sa("1.2.3.4:5483")), "global v4");
10634 assert!(
10635 is_publicly_advertisable(sa("[2001:db8::1]:5483")),
10636 "global v6 (documentation doc but is_globally_routable permits)",
10637 );
10638 assert!(
10639 is_publicly_advertisable(sa("8.8.8.8:9000")),
10640 "global v4 on non-default port",
10641 );
10642
10643 assert!(
10646 !is_publicly_advertisable(sa("192.0.2.1:5483")),
10647 "TEST-NET-1 documentation range"
10648 );
10649 assert!(
10650 !is_publicly_advertisable(sa("203.0.113.10:5483")),
10651 "TEST-NET-3 documentation range"
10652 );
10653 }
10654
10655 #[test]
10656 fn public_address_filter_drops_global_discovery_unsafe_candidates() {
10657 let filtered = filter_publicly_advertisable_addrs(vec![
10658 sa("127.0.0.1:5483"),
10659 sa("10.1.2.3:5483"),
10660 sa("100.64.1.1:5483"),
10661 sa("169.254.1.1:5483"),
10662 sa("1.2.3.4:0"),
10663 sa("[::1]:5483"),
10664 sa("[fd00::1]:5483"),
10665 sa("8.8.8.8:5483"),
10666 sa("[2001:db8::1]:5483"),
10667 ]);
10668
10669 assert_eq!(filtered, vec![sa("8.8.8.8:5483"), sa("[2001:db8::1]:5483")]);
10670 }
10671
10672 #[test]
10673 fn local_discovery_filter_keeps_same_partition_candidates() {
10674 let filtered = filter_discovery_announcement_addrs(
10675 vec![
10676 sa("127.0.0.1:5483"),
10677 sa("10.1.2.3:5483"),
10678 sa("100.64.1.1:5483"),
10679 sa("169.254.1.1:5483"),
10680 sa("1.2.3.4:0"),
10681 sa("[::1]:5483"),
10682 sa("[fd00::1]:5483"),
10683 sa("8.8.8.8:5483"),
10684 ],
10685 true,
10686 );
10687
10688 assert_eq!(
10689 filtered,
10690 vec![
10691 sa("127.0.0.1:5483"),
10692 sa("10.1.2.3:5483"),
10693 sa("100.64.1.1:5483"),
10694 sa("[::1]:5483"),
10695 sa("[fd00::1]:5483"),
10696 sa("8.8.8.8:5483"),
10697 ],
10698 );
10699 }
10700
10701 #[test]
10702 fn local_discovery_scope_tracks_bootstrap_partition() {
10703 let mut config = network::NetworkConfig {
10704 bootstrap_nodes: Vec::new(),
10705 ..network::NetworkConfig::default()
10706 };
10707 assert!(allow_local_discovery_addresses(&config));
10708
10709 config.bootstrap_nodes = vec![sa("127.0.0.1:5483"), sa("192.168.1.10:5483")];
10710 assert!(allow_local_discovery_addresses(&config));
10711
10712 config.bootstrap_nodes = vec![sa("127.0.0.1:5483"), sa("8.8.8.8:5483")];
10713 assert!(!allow_local_discovery_addresses(&config));
10714 }
10715
10716 #[test]
10717 fn local_direct_probe_addrs_prioritizes_same_lan_ipv4() {
10718 let local = [std::net::Ipv4Addr::new(192, 168, 1, 212)];
10719 let ranked = local_direct_probe_addrs_with_local_v4s(
10720 &[
10721 sa("100.118.167.101:27749"),
10722 sa("192.168.0.1:27749"),
10723 sa("192.168.1.108:27749"),
10724 sa("[2a0d:3344:32d:2e10::1]:27749"),
10725 sa("[fd7a:115c:a1e0::b01:a7ac]:27749"),
10726 ],
10727 &local,
10728 );
10729
10730 assert_eq!(
10731 ranked,
10732 vec![
10733 sa("192.168.1.108:27749"),
10734 sa("192.168.0.1:27749"),
10735 sa("100.118.167.101:27749"),
10736 ],
10737 );
10738 }
10739
10740 #[tokio::test]
10741 async fn announcement_builders_filter_global_discovery_addresses() {
10742 let dir = tempfile::tempdir().expect("tmpdir");
10743 let agent = Agent::builder()
10744 .with_machine_key(dir.path().join("machine.key"))
10745 .with_agent_key_path(dir.path().join("agent.key"))
10746 .build()
10747 .await
10748 .expect("agent");
10749 let addresses = vec![
10750 sa("192.168.1.5:5483"),
10751 sa("100.64.1.1:5483"),
10752 sa("1.2.3.4:0"),
10753 sa("8.8.8.8:5483"),
10754 sa("[fd00::1]:5483"),
10755 sa("[2001:db8::1]:5483"),
10756 ];
10757 let expected = vec![sa("8.8.8.8:5483"), sa("[2001:db8::1]:5483")];
10758
10759 let identity_announcement = agent
10760 .build_identity_announcement_with_addrs(IdentityAnnouncementBuildOptions {
10761 include_user_identity: false,
10762 human_consent: false,
10763 addresses: addresses.clone(),
10764 assist_snapshot: None,
10765 reachable_via: Vec::new(),
10766 relay_candidates: Vec::new(),
10767 allow_local_scope: false,
10768 })
10769 .expect("identity announcement");
10770 assert_eq!(identity_announcement.addresses, expected);
10771 identity_announcement
10772 .verify()
10773 .expect("filtered identity announcement verifies");
10774
10775 let machine_announcement = build_machine_announcement_for_identity(
10776 &agent.identity,
10777 addresses,
10778 1,
10779 None,
10780 Vec::new(),
10781 Vec::new(),
10782 false,
10783 )
10784 .expect("machine announcement");
10785 assert_eq!(machine_announcement.addresses, expected);
10786 machine_announcement
10787 .verify()
10788 .expect("filtered machine announcement verifies");
10789 }
10790
10791 #[test]
10792 fn presence_parse_addr_hints_drops_private_scopes() {
10793 let hints = vec![
10797 "127.0.0.1:5483".to_string(),
10798 "10.200.0.1:5483".to_string(),
10799 "[fd00::1]:5483".to_string(),
10800 "1.2.3.4:5483".to_string(),
10801 "[2001:db8::1]:5483".to_string(),
10802 "not-an-address".to_string(),
10803 ];
10804 let parsed = presence::parse_addr_hints(&hints);
10805 let got: Vec<String> = parsed.iter().map(|a| a.to_string()).collect();
10806 assert_eq!(
10807 got,
10808 vec!["1.2.3.4:5483".to_string(), "[2001:db8::1]:5483".to_string()],
10809 "only globally-advertisable addresses survive inbound parsing"
10810 );
10811 }
10812
10813 #[test]
10814 fn name_is_palindrome() {
10815 let name = NAME;
10816 let reversed: String = name.chars().rev().collect();
10817 assert_eq!(name, reversed, "x0x must be a palindrome");
10818 }
10819
10820 #[test]
10821 fn name_is_three_bytes() {
10822 assert_eq!(NAME.len(), 3, "x0x must be exactly three bytes");
10823 }
10824
10825 #[test]
10826 fn name_is_ai_native() {
10827 assert!(NAME.chars().all(|c| c.is_ascii_alphanumeric()));
10830 }
10831
10832 #[test]
10833 fn raw_quic_receive_ack_failure_falls_back_when_gossip_available() {
10834 let err = error::NetworkError::ConnectionFailed(
10835 "send_with_receive_ack failed: Connection closed: Superseded".to_string(),
10836 );
10837 assert!(
10838 !Agent::raw_quic_error_should_stop_fallback(&err, true),
10839 "transient raw ACK lifecycle churn should not suppress gossip fallback"
10840 );
10841 assert!(
10842 Agent::raw_quic_error_should_stop_fallback(&err, false),
10843 "without a gossip capability, the raw ACK failure remains terminal"
10844 );
10845 }
10846
10847 #[test]
10848 fn raw_quic_receive_backpressure_falls_back_when_gossip_available() {
10849 let err = error::NetworkError::RemoteReceiveBackpressured(
10850 "send_with_receive_ack failed: Remote receive pipeline rejected payload: Backpressured"
10851 .to_string(),
10852 );
10853 assert!(
10854 !Agent::raw_quic_error_should_stop_fallback(&err, true),
10855 "receiver congestion should allow gossip fallback"
10856 );
10857 assert!(
10858 Agent::raw_quic_error_should_stop_fallback(&err, false),
10859 "without a gossip capability, receiver congestion remains terminal"
10860 );
10861 assert!(matches!(
10862 Agent::map_raw_quic_dm_error(err),
10863 dm::DmError::ReceiverBackpressured { .. }
10864 ));
10865 }
10866
10867 #[test]
10868 fn raw_quic_payload_errors_still_stop_fallback() {
10869 let err = error::NetworkError::PayloadTooLarge { size: 2, max: 1 };
10870 assert!(Agent::raw_quic_error_should_stop_fallback(&err, true));
10871 }
10872
10873 #[tokio::test]
10874 async fn unusable_capability_advert_falls_back_to_contact_card() {
10875 let dir = tempfile::tempdir().expect("tmpdir");
10876 let agent = Agent::builder()
10877 .with_machine_key(dir.path().join("machine.key"))
10878 .with_agent_key_path(dir.path().join("agent.key"))
10879 .with_contact_store_path(dir.path().join("contacts.json"))
10880 .build()
10881 .await
10882 .expect("agent");
10883 let target = identity::AgentId([7_u8; 32]);
10884 let target_machine = identity::MachineId([9_u8; 32]);
10885
10886 agent.capability_store().insert(
10887 target,
10888 target_machine,
10889 dm::DmCapabilities::pending(),
10890 dm_capability::now_unix_ms(),
10891 );
10892 agent.contacts().write().await.add(contacts::Contact {
10893 agent_id: target,
10894 trust_level: contacts::TrustLevel::Trusted,
10895 label: None,
10896 added_at: 0,
10897 last_seen: None,
10898 identity_type: contacts::IdentityType::Known,
10899 machines: Vec::new(),
10900 dm_capabilities: Some(dm::DmCapabilities::v1_gossip_ready(vec![42_u8; 1184])),
10901 });
10902
10903 let err = agent
10904 .send_direct_with_config(
10905 &target,
10906 b"contact-card-capability".to_vec(),
10907 dm::DmSendConfig {
10908 require_gossip: true,
10909 ..dm::DmSendConfig::default()
10910 },
10911 )
10912 .await
10913 .expect_err("contact-card capability should be used before gossip runtime fails");
10914
10915 assert!(
10916 matches!(err, dm::DmError::LocalGossipUnavailable(_)),
10917 "unexpected error: {err:?}"
10918 );
10919 }
10920
10921 #[tokio::test]
10922 async fn self_dm_uses_loopback_and_delivers_to_subscribers() {
10923 let dir = tempfile::tempdir().expect("tmpdir");
10924 let agent = Agent::builder()
10925 .with_machine_key(dir.path().join("machine.key"))
10926 .with_agent_key_path(dir.path().join("agent.key"))
10927 .with_contact_store_path(dir.path().join("contacts.json"))
10928 .build()
10929 .await
10930 .expect("agent");
10931 let mut rx = agent.subscribe_direct();
10932 let payload = b"loopback-self-dm".to_vec();
10933
10934 let receipt = agent
10935 .send_direct_with_config(
10936 &agent.agent_id(),
10937 payload.clone(),
10938 dm::DmSendConfig::default(),
10939 )
10940 .await
10941 .expect("self-DM should use loopback path");
10942
10943 assert_eq!(receipt.path, dm::DmPath::Loopback);
10944 let msg = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
10945 .await
10946 .expect("self-DM should be delivered promptly")
10947 .expect("subscriber should remain open");
10948 assert_eq!(msg.sender, agent.agent_id());
10949 assert_eq!(msg.machine_id, agent.machine_id());
10950 assert_eq!(msg.payload, payload);
10951 assert!(msg.verified);
10952 assert_eq!(msg.trust_decision, Some(trust::TrustDecision::Accept));
10953
10954 let diagnostics = agent.direct_messaging().diagnostics_snapshot();
10955 assert_eq!(diagnostics.stats.outgoing_send_succeeded, 1);
10956 assert_eq!(diagnostics.stats.outgoing_path_loopback, 1);
10957 assert_eq!(diagnostics.stats.incoming_envelopes_total, 1);
10958 assert_eq!(diagnostics.stats.incoming_delivered_to_subscribe, 1);
10959 }
10960
10961 fn loopback_network_config() -> network::NetworkConfig {
10962 network::NetworkConfig {
10963 bind_addr: Some("127.0.0.1:0".parse().expect("loopback addr")),
10964 bootstrap_nodes: Vec::new(),
10965 ..network::NetworkConfig::default()
10966 }
10967 }
10968
10969 fn normalize_loopback_addr(addr: std::net::SocketAddr) -> std::net::SocketAddr {
10970 if addr.ip().is_unspecified() {
10971 std::net::SocketAddr::new(
10972 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
10973 addr.port(),
10974 )
10975 } else {
10976 addr
10977 }
10978 }
10979
10980 #[tokio::test]
10981 async fn shutdown_aborts_identity_heartbeat_task() {
10982 struct DropFlag(std::sync::Arc<std::sync::atomic::AtomicBool>);
10983
10984 impl Drop for DropFlag {
10985 fn drop(&mut self) {
10986 self.0.store(true, std::sync::atomic::Ordering::Release);
10987 }
10988 }
10989
10990 let dir = tempfile::tempdir().expect("tmpdir");
10991 let agent = Agent::builder()
10992 .with_machine_key(dir.path().join("machine.key"))
10993 .with_agent_key_path(dir.path().join("agent.key"))
10994 .with_contact_store_path(dir.path().join("contacts.json"))
10995 .build()
10996 .await
10997 .expect("agent");
10998
10999 let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
11000 let dropped_for_task = std::sync::Arc::clone(&dropped);
11001 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
11002 let handle = tokio::spawn(async move {
11003 let _drop_flag = DropFlag(dropped_for_task);
11004 let _ = started_tx.send(());
11005 std::future::pending::<()>().await;
11006 });
11007 started_rx.await.expect("heartbeat task started");
11008
11009 *agent.heartbeat_handle.lock().await = Some(handle);
11010 assert!(agent.heartbeat_handle.lock().await.is_some());
11011
11012 agent.shutdown().await;
11013 assert!(agent.heartbeat_handle.lock().await.is_none());
11014 assert!(dropped.load(std::sync::atomic::Ordering::Acquire));
11015 }
11016
11017 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
11023 async fn shutdown_drains_crdt_kv_sync_tasks() {
11024 let dir = tempfile::tempdir().expect("tmpdir");
11025 let agent = Agent::builder()
11026 .with_machine_key(dir.path().join("machine.key"))
11027 .with_agent_key_path(dir.path().join("agent.key"))
11028 .with_contact_store_path(dir.path().join("contacts.json"))
11029 .with_peer_cache_disabled()
11030 .with_network_config(loopback_network_config())
11031 .build()
11032 .await
11033 .expect("agent");
11034
11035 let baseline = agent
11040 .tracked_tasks
11041 .lock()
11042 .expect("tracked_tasks")
11043 .handles
11044 .len();
11045
11046 agent
11047 .create_kv_store("ws15-store", "ws15-store-topic")
11048 .await
11049 .expect("create kv store");
11050 agent
11051 .create_task_list("ws15-list", "ws15-list-topic")
11052 .await
11053 .expect("create task list");
11054
11055 let (registered, abort_handles): (usize, Vec<tokio::task::AbortHandle>) = {
11058 let guard = agent.tracked_tasks.lock().expect("tracked_tasks");
11059 let aborts = guard.handles.iter().map(|h| h.abort_handle()).collect();
11060 (guard.handles.len(), aborts)
11061 };
11062 assert!(
11063 registered > baseline,
11064 "CRDT/KV sync loops must register with spawn_tracked \
11065 (registry {registered}, baseline {baseline})"
11066 );
11067
11068 agent.shutdown().await;
11069
11070 let after = agent.tracked_tasks.lock().expect("tracked_tasks");
11073 assert!(
11074 after.closed,
11075 "tracked-task registry must be closed after shutdown"
11076 );
11077 assert!(
11078 after.handles.is_empty(),
11079 "tracked-task registry must be drained after shutdown ({} left)",
11080 after.handles.len()
11081 );
11082 drop(after);
11083
11084 for handle in &abort_handles {
11086 assert!(
11087 handle.is_finished(),
11088 "a CRDT/KV sync task did not terminate after shutdown"
11089 );
11090 }
11091 }
11092
11093 #[tokio::test]
11112 async fn begin_shutdown_closes_registry_and_cancels_token() {
11113 let dir = tempfile::tempdir().expect("tmpdir");
11114 let agent = Agent::builder()
11115 .with_machine_key(dir.path().join("machine.key"))
11116 .with_agent_key_path(dir.path().join("agent.key"))
11117 .with_contact_store_path(dir.path().join("contacts.json"))
11118 .build()
11119 .await
11120 .expect("agent");
11121
11122 assert!(
11123 !agent.shutdown_token.is_cancelled(),
11124 "token must not be cancelled before shutdown begins"
11125 );
11126 assert!(
11127 !agent.tracked_tasks.lock().expect("tracked_tasks").closed,
11128 "registry must be open before shutdown begins"
11129 );
11130
11131 agent.begin_shutdown();
11132
11133 assert!(
11134 agent.shutdown_token.is_cancelled(),
11135 "begin_shutdown must cancel the shutdown token"
11136 );
11137 assert!(
11138 agent.tracked_tasks.lock().expect("tracked_tasks").closed,
11139 "begin_shutdown must close the tracked-task registry"
11140 );
11141
11142 }
11146
11147 #[tokio::test]
11153 async fn spawn_tracked_refuses_after_begin_shutdown() {
11154 let dir = tempfile::tempdir().expect("tmpdir");
11155 let agent = Agent::builder()
11156 .with_machine_key(dir.path().join("machine.key"))
11157 .with_agent_key_path(dir.path().join("agent.key"))
11158 .with_contact_store_path(dir.path().join("contacts.json"))
11159 .build()
11160 .await
11161 .expect("agent");
11162
11163 agent.spawn_tracked(async {});
11165 let before = agent
11166 .tracked_tasks
11167 .lock()
11168 .expect("tracked_tasks")
11169 .handles
11170 .len();
11171 assert_eq!(
11172 before, 1,
11173 "spawn_tracked must register a task before begin_shutdown"
11174 );
11175
11176 agent.begin_shutdown();
11178 agent.spawn_tracked(async {});
11179
11180 {
11181 let after = agent.tracked_tasks.lock().expect("tracked_tasks");
11182 assert_eq!(
11183 after.handles.len(),
11184 before,
11185 "spawn_tracked must be a no-op (handle NOT pushed) after begin_shutdown \
11186 closed the registry — a racing join_network would otherwise leak a task"
11187 );
11188 assert!(after.closed, "registry must remain closed");
11189 }
11190
11191 agent.shutdown().await;
11193 }
11194
11195 #[tokio::test]
11200 async fn shutdown_is_idempotent_and_never_panics_on_second_call() {
11201 let dir = tempfile::tempdir().expect("tmpdir");
11202 let agent = Agent::builder()
11203 .with_machine_key(dir.path().join("machine.key"))
11204 .with_agent_key_path(dir.path().join("agent.key"))
11205 .with_contact_store_path(dir.path().join("contacts.json"))
11206 .build()
11207 .await
11208 .expect("agent");
11209
11210 agent.shutdown().await;
11211 agent.shutdown().await;
11214
11215 let registry = agent.tracked_tasks.lock().expect("tracked_tasks");
11216 assert!(
11217 registry.closed,
11218 "registry must remain closed after a double shutdown"
11219 );
11220 assert!(
11221 registry.handles.is_empty(),
11222 "registry must be drained after a double shutdown"
11223 );
11224 assert!(
11225 agent.shutdown_token.is_cancelled(),
11226 "token must remain cancelled after a double shutdown"
11227 );
11228 }
11229
11230 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
11244 async fn shutdown_graces_a_token_respecting_tracked_task() {
11245 let dir = tempfile::tempdir().expect("tmpdir");
11246 let agent = Agent::builder()
11247 .with_machine_key(dir.path().join("machine.key"))
11248 .with_agent_key_path(dir.path().join("agent.key"))
11249 .with_contact_store_path(dir.path().join("contacts.json"))
11250 .build()
11251 .await
11252 .expect("agent");
11253
11254 let completed_gracefully = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
11258 let token = agent.shutdown_token.clone();
11259 let cg = std::sync::Arc::clone(&completed_gracefully);
11260 agent.spawn_tracked(async move {
11261 token.cancelled().await;
11263 cg.store(true, std::sync::atomic::Ordering::SeqCst);
11264 });
11265
11266 agent.shutdown().await;
11267
11268 assert!(
11269 completed_gracefully.load(std::sync::atomic::Ordering::SeqCst),
11270 "token-respecting task must complete GRACEFULLY (set its flag before \
11271 returning) — proves the grace-await precedes any force-abort"
11272 );
11273 }
11274
11275 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
11276 async fn connected_peer_clears_stale_lifecycle_block_before_raw_send() {
11277 let dir = tempfile::tempdir().expect("tmpdir");
11278 let alice = Agent::builder()
11279 .with_machine_key(dir.path().join("alice-machine.key"))
11280 .with_agent_key_path(dir.path().join("alice-agent.key"))
11281 .with_contact_store_path(dir.path().join("alice-contacts.json"))
11282 .with_peer_cache_disabled()
11283 .with_network_config(loopback_network_config())
11284 .build()
11285 .await
11286 .expect("alice");
11287 let bob = Agent::builder()
11288 .with_machine_key(dir.path().join("bob-machine.key"))
11289 .with_agent_key_path(dir.path().join("bob-agent.key"))
11290 .with_contact_store_path(dir.path().join("bob-contacts.json"))
11291 .with_peer_cache_disabled()
11292 .with_network_config(loopback_network_config())
11293 .build()
11294 .await
11295 .expect("bob");
11296
11297 let bob_network = bob.network().expect("bob network");
11298 let bob_addr = normalize_loopback_addr(bob_network.bound_addr().await.expect("bob bound"));
11299 let alice_network = alice.network().expect("alice network");
11300 let connected_peer = alice_network
11301 .connect_addr(bob_addr)
11302 .await
11303 .expect("alice connects to bob");
11304 assert_eq!(connected_peer.0, bob.machine_id().0);
11305
11306 let bob_peer = ant_quic::PeerId(bob.machine_id().0);
11307 let connected_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
11308 while tokio::time::Instant::now() < connected_deadline {
11309 if alice_network.is_connected(&bob_peer).await {
11310 break;
11311 }
11312 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
11313 }
11314 assert!(alice_network.is_connected(&bob_peer).await);
11315
11316 alice
11317 .direct_messaging()
11318 .mark_connected(bob.agent_id(), bob.machine_id())
11319 .await;
11320 alice.direct_messaging().record_lifecycle_blocked(
11321 bob.machine_id(),
11322 Some(1),
11323 "closed: stale test block",
11324 );
11325 assert!(alice
11326 .direct_messaging()
11327 .lifecycle_block_reason(&bob.machine_id())
11328 .is_some());
11329
11330 let receipt = alice
11331 .send_direct_with_config(
11332 &bob.agent_id(),
11333 b"stale-block-clear".to_vec(),
11334 dm::DmSendConfig {
11335 prefer_raw_quic_if_connected: true,
11336 ..dm::DmSendConfig::default()
11337 },
11338 )
11339 .await
11340 .expect("raw send should ignore and clear stale lifecycle block");
11341 assert_eq!(receipt.path, dm::DmPath::RawQuic);
11342 assert!(alice
11343 .direct_messaging()
11344 .lifecycle_block_reason(&bob.machine_id())
11345 .is_none());
11346 }
11347
11348 #[tokio::test]
11349 async fn agent_peer_relay_defaults_to_disabled_when_unconfigured() {
11350 let dir = tempfile::tempdir().expect("tmpdir");
11355 let agent = Agent::builder()
11356 .with_machine_key(dir.path().join("machine.key"))
11357 .with_agent_key_path(dir.path().join("agent.key"))
11358 .with_contact_store_path(dir.path().join("contacts.json"))
11359 .build()
11360 .await
11361 .expect("agent");
11362 assert!(
11363 !agent.peer_relay().policy().enabled,
11364 "default Agent must have the relay engine disabled"
11365 );
11366 assert!(
11367 agent.relay_candidates().await.is_empty(),
11368 "default Agent must have no relay candidates"
11369 );
11370 }
11371
11372 #[tokio::test]
11373 async fn agent_peer_relay_honors_configured_policy_and_candidates() {
11374 let dir = tempfile::tempdir().expect("tmpdir");
11379 let candidate_a = [0xAA_u8; 32];
11380 let candidate_b = [0xBB_u8; 32];
11381 let mut net_cfg = loopback_network_config();
11382 net_cfg.peer_relay = network::PeerRelayConfig {
11383 enabled: true,
11384 require_contact_to_relay: false,
11385 fail_threshold: 7,
11386 fail_window_ms: 90_000,
11387 candidates: vec![hex::encode(candidate_a), hex::encode(candidate_b)],
11388 ..Default::default()
11389 };
11390 let agent = Agent::builder()
11391 .with_machine_key(dir.path().join("machine.key"))
11392 .with_agent_key_path(dir.path().join("agent.key"))
11393 .with_contact_store_path(dir.path().join("contacts.json"))
11394 .with_peer_cache_disabled()
11395 .with_network_config(net_cfg)
11396 .build()
11397 .await
11398 .expect("agent");
11399
11400 let policy = agent.peer_relay().policy();
11401 assert!(policy.enabled, "configured `enabled = true` must propagate");
11402 assert_eq!(policy.fail_threshold, 7);
11403 assert_eq!(policy.fail_window, std::time::Duration::from_millis(90_000));
11404
11405 let candidates = agent.relay_candidates().await;
11406 assert_eq!(candidates.len(), 2, "both TOML candidates seeded");
11407 assert!(candidates.iter().any(|c| c.0 == candidate_a));
11408 assert!(candidates.iter().any(|c| c.0 == candidate_b));
11409 }
11410
11411 #[tokio::test]
11412 async fn send_direct_failure_records_failure_on_peer_relay() {
11413 let dir = tempfile::tempdir().expect("tmpdir");
11420 let agent = Agent::builder()
11421 .with_machine_key(dir.path().join("machine.key"))
11422 .with_agent_key_path(dir.path().join("agent.key"))
11423 .with_contact_store_path(dir.path().join("contacts.json"))
11424 .build()
11425 .await
11426 .expect("agent");
11427 let unreachable = identity::AgentId([0x42; 32]);
11428
11429 let result = agent
11430 .send_direct_with_config(
11431 &unreachable,
11432 b"x0x-0070b-bookkeeping".to_vec(),
11433 dm::DmSendConfig::default(),
11434 )
11435 .await;
11436 assert!(
11437 result.is_err(),
11438 "no network configured - direct send must fail"
11439 );
11440 assert_eq!(
11441 agent.peer_relay().tracked_peer_count(),
11442 1,
11443 "failure must have produced a per-peer relay-engine entry"
11444 );
11445 }
11446
11447 #[tokio::test]
11448 async fn send_direct_self_loopback_does_not_disturb_peer_relay() {
11449 let dir = tempfile::tempdir().expect("tmpdir");
11455 let agent = Agent::builder()
11456 .with_machine_key(dir.path().join("machine.key"))
11457 .with_agent_key_path(dir.path().join("agent.key"))
11458 .with_contact_store_path(dir.path().join("contacts.json"))
11459 .build()
11460 .await
11461 .expect("agent");
11462 let _receipt = agent
11463 .send_direct_with_config(
11464 &agent.agent_id(),
11465 b"loopback".to_vec(),
11466 dm::DmSendConfig::default(),
11467 )
11468 .await
11469 .expect("loopback self-DM");
11470 assert_eq!(
11471 agent.peer_relay().tracked_peer_count(),
11472 0,
11473 "loopback path must not register with the relay engine"
11474 );
11475 }
11476
11477 #[tokio::test]
11478 async fn try_relay_fallback_returns_no_candidate_when_list_empty() {
11479 let dir = tempfile::tempdir().expect("tmpdir");
11484 let mut net_cfg = loopback_network_config();
11485 net_cfg.peer_relay = network::PeerRelayConfig {
11486 enabled: true,
11487 require_contact_to_relay: false,
11488 fail_threshold: 3,
11489 fail_window_ms: 60_000,
11490 candidates: Vec::new(),
11491 ..Default::default()
11492 };
11493 let agent = Agent::builder()
11494 .with_machine_key(dir.path().join("machine.key"))
11495 .with_agent_key_path(dir.path().join("agent.key"))
11496 .with_contact_store_path(dir.path().join("contacts.json"))
11497 .with_peer_cache_disabled()
11498 .with_network_config(net_cfg)
11499 .build()
11500 .await
11501 .expect("agent");
11502 let to = identity::AgentId([0xCD; 32]);
11503
11504 let err = agent
11505 .try_relay_fallback(&to, b"payload".to_vec(), &[0u8; 32])
11506 .await
11507 .expect_err("empty candidate list must short-circuit");
11508 assert!(
11509 matches!(err, dm::DmError::NoRelayCandidate),
11510 "expected NoRelayCandidate, got {err:?}"
11511 );
11512 }
11513
11514 #[tokio::test]
11515 async fn try_relay_fallback_returns_no_candidate_when_machine_id_uncached() {
11516 let dir = tempfile::tempdir().expect("tmpdir");
11522 let candidate_hex = hex::encode([0xEE_u8; 32]);
11523 let mut net_cfg = loopback_network_config();
11524 net_cfg.peer_relay = network::PeerRelayConfig {
11525 enabled: true,
11526 require_contact_to_relay: false,
11527 fail_threshold: 3,
11528 fail_window_ms: 60_000,
11529 candidates: vec![candidate_hex],
11530 ..Default::default()
11531 };
11532 let agent = Agent::builder()
11533 .with_machine_key(dir.path().join("machine.key"))
11534 .with_agent_key_path(dir.path().join("agent.key"))
11535 .with_contact_store_path(dir.path().join("contacts.json"))
11536 .with_peer_cache_disabled()
11537 .with_network_config(net_cfg)
11538 .build()
11539 .await
11540 .expect("agent");
11541 let to = identity::AgentId([0xCD; 32]);
11542
11543 let err = agent
11544 .try_relay_fallback(&to, b"payload".to_vec(), &[0u8; 32])
11545 .await
11546 .expect_err("uncached candidate must short-circuit");
11547 assert!(
11548 matches!(err, dm::DmError::NoRelayCandidate),
11549 "expected NoRelayCandidate, got {err:?}"
11550 );
11551 }
11552
11553 #[tokio::test]
11554 async fn send_direct_below_threshold_does_not_attempt_relay() {
11555 let dir = tempfile::tempdir().expect("tmpdir");
11561 let mut net_cfg = loopback_network_config();
11562 net_cfg.peer_relay = network::PeerRelayConfig {
11563 enabled: true,
11564 require_contact_to_relay: false,
11565 fail_threshold: 5,
11566 fail_window_ms: 60_000,
11567 candidates: vec![hex::encode([0xEE_u8; 32])],
11568 ..Default::default()
11569 };
11570 let agent = Agent::builder()
11571 .with_machine_key(dir.path().join("machine.key"))
11572 .with_agent_key_path(dir.path().join("agent.key"))
11573 .with_contact_store_path(dir.path().join("contacts.json"))
11574 .with_peer_cache_disabled()
11575 .with_network_config(net_cfg)
11576 .build()
11577 .await
11578 .expect("agent");
11579 let to = identity::AgentId([0xCD; 32]);
11580
11581 let result = agent
11582 .send_direct_with_config(&to, b"first-attempt".to_vec(), dm::DmSendConfig::default())
11583 .await;
11584 assert!(result.is_err(), "no usable transport - direct send fails");
11585 let snap = agent.peer_relay().stats().snapshot();
11586 assert_eq!(
11587 snap.relay_sent, 0,
11588 "below threshold must not engage the relay path"
11589 );
11590 }
11591
11592 #[tokio::test]
11593 async fn send_direct_above_threshold_without_candidates_surfaces_direct_err() {
11594 let dir = tempfile::tempdir().expect("tmpdir");
11600 let mut net_cfg = loopback_network_config();
11601 net_cfg.peer_relay = network::PeerRelayConfig {
11602 enabled: true,
11603 require_contact_to_relay: false,
11604 fail_threshold: 3,
11605 fail_window_ms: 60_000,
11606 candidates: Vec::new(),
11607 ..Default::default()
11608 };
11609 let agent = Agent::builder()
11610 .with_machine_key(dir.path().join("machine.key"))
11611 .with_agent_key_path(dir.path().join("agent.key"))
11612 .with_contact_store_path(dir.path().join("contacts.json"))
11613 .with_peer_cache_disabled()
11614 .with_network_config(net_cfg)
11615 .build()
11616 .await
11617 .expect("agent");
11618 let to = identity::AgentId([0xCD; 32]);
11619
11620 for _ in 0..agent.peer_relay().policy().fail_threshold {
11623 agent.peer_relay().record_direct_failure(&to);
11624 }
11625 assert!(
11626 agent.peer_relay().needs_relay(&to),
11627 "engine must say the peer now needs a relay"
11628 );
11629
11630 let err = agent
11631 .send_direct_with_config(&to, b"x0x-0070b".to_vec(), dm::DmSendConfig::default())
11632 .await
11633 .expect_err("send must still fail when the relay path has no candidates");
11634 assert!(
11635 !matches!(err, dm::DmError::NoRelayCandidate),
11636 "relay-side errors must not leak - original direct error must surface, got {err:?}"
11637 );
11638 }
11639
11640 #[tokio::test]
11641 async fn send_direct_disabled_policy_does_not_engage_relay_seed() {
11642 let dir = tempfile::tempdir().expect("tmpdir");
11649 let agent = Agent::builder()
11650 .with_machine_key(dir.path().join("machine.key"))
11651 .with_agent_key_path(dir.path().join("agent.key"))
11652 .with_contact_store_path(dir.path().join("contacts.json"))
11653 .build()
11654 .await
11655 .expect("agent");
11656 let to = identity::AgentId([0xCD; 32]);
11657 for _ in 0..10 {
11658 agent.peer_relay().record_direct_failure(&to);
11659 }
11660 assert!(
11661 !agent.peer_relay().needs_relay(&to),
11662 "disabled policy must never trigger needs_relay"
11663 );
11664
11665 let err = agent
11666 .send_direct_with_config(&to, b"x0x-0070b".to_vec(), dm::DmSendConfig::default())
11667 .await
11668 .expect_err("no network - direct send must fail");
11669 assert!(
11670 !matches!(err, dm::DmError::NoRelayCandidate),
11671 "disabled policy must not surface any relay-side error, got {err:?}"
11672 );
11673 assert_eq!(
11674 agent.peer_relay().stats().snapshot().relay_sent,
11675 0,
11676 "disabled policy must not advance relay_sent"
11677 );
11678 }
11679
11680 #[tokio::test]
11681 async fn relay_dm_listener_refuses_bad_signature_and_ticks_counter() {
11682 let dir = tempfile::tempdir().expect("tmpdir");
11698 let mut net_cfg = loopback_network_config();
11699 net_cfg.peer_relay = network::PeerRelayConfig {
11700 enabled: true,
11701 require_contact_to_relay: false,
11702 fail_threshold: 3,
11703 fail_window_ms: 60_000,
11704 candidates: Vec::new(),
11705 ..Default::default()
11706 };
11707 let agent = Agent::builder()
11708 .with_machine_key(dir.path().join("machine.key"))
11709 .with_agent_key_path(dir.path().join("agent.key"))
11710 .with_contact_store_path(dir.path().join("contacts.json"))
11711 .with_peer_cache_disabled()
11712 .with_network_config(net_cfg)
11713 .build()
11714 .await
11715 .expect("agent");
11716
11717 let network = agent
11718 .network
11719 .as_ref()
11720 .expect("agent built with network config");
11721 let sender = network.test_relayed_dm_sender();
11722
11723 let relayed = peer_relay::RelayedDm {
11724 header: peer_relay::RelayHeader {
11725 version: peer_relay::RelayHeader::VERSION,
11726 dst_agent_id: agent.agent_id().0,
11727 sender_agent_id: [0x42; 32],
11728 sender_public_key: Vec::new(),
11730 originated_at_unix_ms: dm::now_unix_ms(),
11731 signature: Vec::new(),
11732 },
11733 inner: dm::DmEnvelope {
11734 protocol_version: 1,
11735 request_id: [0u8; 16],
11736 sender_agent_id: [0x42; 32],
11737 sender_machine_id: [0x43; 32],
11738 recipient_agent_id: agent.agent_id().0,
11739 created_at_unix_ms: 0,
11740 expires_at_unix_ms: 0,
11741 body: dm::DmBody::Payload(dm::DmPayload {
11742 kem_ciphertext: Vec::new(),
11743 body_nonce: [0u8; 12],
11744 body_ciphertext: Vec::new(),
11745 }),
11746 signature: Vec::new(),
11747 },
11748 };
11749
11750 let relay_peer = ant_quic::PeerId([0xEE; 32]);
11751 let relay_wire_sender = [0xEE; 32];
11752 sender
11753 .send((relay_peer, relay_wire_sender, relayed))
11754 .await
11755 .expect("relayed_dm channel must accept push");
11756
11757 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
11760 loop {
11761 let snap = agent.peer_relay().stats().snapshot();
11762 if snap.relay_refused_bad_signature == 1 {
11763 break;
11764 }
11765 if std::time::Instant::now() >= deadline {
11766 panic!(
11767 "relay-DM listener did not tick relay_refused_bad_signature within 2s - \
11768 snapshot: {snap:?}"
11769 );
11770 }
11771 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
11772 }
11773
11774 let snap = agent.peer_relay().stats().snapshot();
11775 assert_eq!(snap.relay_refused_bad_signature, 1);
11776 assert_eq!(snap.relay_received, 0, "bad-sig path must not deliver");
11777 assert_eq!(snap.relay_forwarded, 0, "bad-sig path must not forward");
11778 }
11779
11780 #[tokio::test]
11781 async fn agent_creates() {
11782 let agent = Agent::new().await;
11783 assert!(agent.is_ok());
11784 }
11785
11786 #[tokio::test]
11787 async fn agent_joins_network() {
11788 let agent = Agent::new().await.unwrap();
11789 assert!(agent.join_network().await.is_ok());
11790 }
11791
11792 #[tokio::test]
11793 async fn agent_subscribes() {
11794 let agent = Agent::new().await.unwrap();
11795 assert!(agent.subscribe("test-topic").await.is_err());
11797 }
11798
11799 #[tokio::test]
11800 async fn identity_announcement_machine_signature_verifies() {
11801 let agent = Agent::builder()
11802 .with_network_config(network::NetworkConfig::default())
11803 .build()
11804 .await
11805 .unwrap();
11806
11807 let announcement = agent.build_identity_announcement(false, false).unwrap();
11808 assert_eq!(announcement.agent_id, agent.agent_id());
11809 assert_eq!(announcement.machine_id, agent.machine_id());
11810 assert!(announcement.user_id.is_none());
11811 assert!(announcement.agent_certificate.is_none());
11812 assert!(announcement.verify().is_ok());
11813 }
11814
11815 #[tokio::test]
11816 async fn identity_announcement_requires_human_consent() {
11817 let agent = Agent::builder()
11818 .with_network_config(network::NetworkConfig::default())
11819 .build()
11820 .await
11821 .unwrap();
11822
11823 let err = agent.build_identity_announcement(true, false).unwrap_err();
11824 assert!(
11825 err.to_string().contains("explicit human consent"),
11826 "unexpected error: {err}"
11827 );
11828 }
11829
11830 #[tokio::test]
11831 async fn identity_announcement_with_user_requires_user_identity() {
11832 let agent = Agent::builder()
11833 .with_network_config(network::NetworkConfig::default())
11834 .build()
11835 .await
11836 .unwrap();
11837
11838 let err = agent.build_identity_announcement(true, true).unwrap_err();
11839 assert!(
11840 err.to_string().contains("no user identity is configured"),
11841 "unexpected error: {err}"
11842 );
11843 }
11844
11845 #[tokio::test]
11846 async fn announce_identity_populates_discovery_cache() {
11847 let user_key = identity::UserKeypair::generate().unwrap();
11848 let agent = Agent::builder()
11849 .with_network_config(network::NetworkConfig::default())
11850 .with_user_key(user_key)
11851 .build()
11852 .await
11853 .unwrap();
11854
11855 agent.announce_identity(true, true).await.unwrap();
11856 let discovered = agent.discovered_agent(agent.agent_id()).await.unwrap();
11857 let entry = discovered.expect("agent should discover its own announcement");
11858
11859 assert_eq!(entry.agent_id, agent.agent_id());
11860 assert_eq!(entry.machine_id, agent.machine_id());
11861 assert_eq!(entry.user_id, agent.user_id());
11862 }
11863
11864 #[tokio::test]
11873 async fn revoked_agent_fails_machine_verification_even_when_cached() {
11874 let user_key = identity::UserKeypair::generate().unwrap();
11875 let agent = Agent::builder()
11876 .with_network_config(network::NetworkConfig::default())
11877 .with_user_key(user_key)
11878 .build()
11879 .await
11880 .unwrap();
11881
11882 agent.announce_identity(true, true).await.unwrap();
11885 let agent_id = agent.agent_id();
11886 let machine_id = agent.machine_id();
11887 assert!(
11888 agent
11889 .is_agent_machine_verified(&agent_id, &machine_id)
11890 .await,
11891 "a cached, signed self-announcement must verify before revocation"
11892 );
11893
11894 let now = std::time::SystemTime::now()
11898 .duration_since(std::time::UNIX_EPOCH)
11899 .unwrap()
11900 .as_secs();
11901 let record = revocation::RevocationRecord::sign(
11902 revocation::RevokedSubject::Agent(agent_id),
11903 agent.identity().agent_keypair().public_key(),
11904 agent.identity().agent_keypair().secret_key(),
11905 now,
11906 Some("ep2 test: key compromised".to_string()),
11907 )
11908 .unwrap();
11909 {
11910 let set = agent.revocation_set();
11911 let mut set = set.write().await;
11912 set.verify_and_insert(record, None)
11913 .expect("self-revocation must verify and insert");
11914 }
11915
11916 assert!(
11919 !agent
11920 .is_agent_machine_verified(&agent_id, &machine_id)
11921 .await,
11922 "a revoked agent must never pass machine verification while cached"
11923 );
11924 }
11925
11926 #[test]
11935 fn issuer_revocation_propagates_over_gossip_via_cache_cert_lookup() {
11936 let user = identity::UserKeypair::generate().unwrap();
11937 let issued_agent = identity::AgentKeypair::generate().unwrap();
11938 let agent_id = issued_agent.agent_id();
11939 let cert = identity::AgentCertificate::issue(&user, &issued_agent).unwrap();
11943
11944 let mut cache = std::collections::HashMap::new();
11947 cache.insert(
11948 agent_id,
11949 DiscoveredAgent {
11950 agent_id,
11951 machine_id: identity::MachineId([0u8; 32]),
11952 user_id: None,
11953 addresses: Vec::new(),
11954 announced_at: 0,
11955 last_seen: 0,
11956 machine_public_key: Vec::new(),
11957 nat_type: None,
11958 can_receive_direct: None,
11959 is_relay: None,
11960 is_coordinator: None,
11961 reachable_via: Vec::new(),
11962 relay_candidates: Vec::new(),
11963 cert_not_after: cert.not_after(),
11964 agent_certificate: Some(cert.clone()),
11965 agent_public_key: cert.agent_public_key().to_vec(),
11966 },
11967 );
11968
11969 let subject_certs = collect_subject_certs(&cache);
11971 let looked_up = subject_certs
11972 .get(&agent_id)
11973 .expect("the cache lookup must find the subject cert");
11974
11975 let now = std::time::SystemTime::now()
11976 .duration_since(std::time::UNIX_EPOCH)
11977 .unwrap()
11978 .as_secs();
11979 let record = revocation::RevocationRecord::sign(
11981 revocation::RevokedSubject::Agent(agent_id),
11982 user.public_key(),
11983 user.secret_key(),
11984 now,
11985 Some("issuer revokes compromised agent".to_string()),
11986 )
11987 .unwrap();
11988
11989 let mut set = revocation::RevocationSet::new();
11992 assert!(
11993 set.verify_and_insert(record.clone(), Some(looked_up))
11994 .unwrap(),
11995 "issuer-revocation must verify and insert with the cache-resolved cert"
11996 );
11997 assert!(
11998 set.is_agent_revoked(&agent_id),
11999 "the agent must now be revoked network-wide"
12000 );
12001
12002 assert!(
12006 revocation::RevocationSet::new()
12007 .verify_and_insert(record, None)
12008 .is_err(),
12009 "an issuer-revocation must be rejected without the subject cert \
12010 (the pre-fix gossip behavior this closes)"
12011 );
12012 }
12013
12014 #[test]
12018 fn identity_announcement_backward_compat_no_nat_fields() {
12019 use identity::{AgentId, MachineId};
12020
12021 #[derive(serde::Serialize, serde::Deserialize)]
12024 struct OldIdentityAnnouncementUnsigned {
12025 agent_id: AgentId,
12026 machine_id: MachineId,
12027 user_id: Option<identity::UserId>,
12028 agent_certificate: Option<identity::AgentCertificate>,
12029 machine_public_key: Vec<u8>,
12030 addresses: Vec<std::net::SocketAddr>,
12031 announced_at: u64,
12032 }
12033
12034 let agent_id = AgentId([1u8; 32]);
12035 let machine_id = MachineId([2u8; 32]);
12036 let old = OldIdentityAnnouncementUnsigned {
12037 agent_id,
12038 machine_id,
12039 user_id: None,
12040 agent_certificate: None,
12041 machine_public_key: vec![0u8; 10],
12042 addresses: Vec::new(),
12043 announced_at: 1234,
12044 };
12045 let bytes = bincode::serialize(&old).expect("serialize old announcement");
12046
12047 let result = bincode::deserialize::<IdentityAnnouncementUnsigned>(&bytes);
12055 assert!(
12058 result.is_err(),
12059 "Old-format announcement should not decode as new struct (protocol upgrade required)"
12060 );
12061 }
12062
12063 #[tokio::test]
12064 async fn register_announced_machine_skips_self_agent() {
12065 let dir = tempfile::tempdir().expect("tmpdir");
12070 let store = std::sync::Arc::new(tokio::sync::RwLock::new(contacts::ContactStore::new(
12071 dir.path().join("contacts.json"),
12072 )));
12073 let own = identity::AgentId([1u8; 32]);
12074 let own_machine = identity::MachineId([2u8; 32]);
12075
12076 let added = super::register_announced_machine(&store, own, own, own_machine).await;
12077
12078 assert!(!added, "self-announcement must not register a machine");
12079 let store = store.read().await;
12080 assert!(
12081 store.machines(&own).is_empty(),
12082 "self-agent must have no machine record after a self-announcement"
12083 );
12084 assert!(
12089 store.list().iter().all(|contact| contact.agent_id != own),
12090 "self-agent must not appear in the contact store at all after a \
12091 self-announcement"
12092 );
12093 }
12094
12095 #[tokio::test]
12096 async fn register_announced_machine_registers_foreign_agent() {
12097 let dir = tempfile::tempdir().expect("tmpdir");
12102 let store = std::sync::Arc::new(tokio::sync::RwLock::new(contacts::ContactStore::new(
12103 dir.path().join("contacts.json"),
12104 )));
12105 let own = identity::AgentId([1u8; 32]);
12106 let peer = identity::AgentId([9u8; 32]);
12107 let peer_machine = identity::MachineId([7u8; 32]);
12108
12109 let added = super::register_announced_machine(&store, own, peer, peer_machine).await;
12110 assert!(added, "foreign announcement must register a new machine");
12111
12112 let added_again = super::register_announced_machine(&store, own, peer, peer_machine).await;
12113 assert!(
12114 !added_again,
12115 "re-announcing the same machine must be idempotent"
12116 );
12117
12118 let store = store.read().await;
12119 let machines = store.machines(&peer);
12120 assert_eq!(machines.len(), 1, "exactly one machine record");
12121 assert_eq!(machines[0].machine_id, peer_machine);
12122 assert!(
12123 store.machines(&own).is_empty(),
12124 "own agent must still have no contact entry"
12125 );
12126 }
12127
12128 #[test]
12129 fn announcement_assist_snapshot_uses_capabilities_not_activity() {
12130 let status = ant_quic::NodeStatus {
12131 nat_type: ant_quic::NatType::FullCone,
12132 can_receive_direct: true,
12133 relay_service_enabled: true,
12134 coordinator_service_enabled: true,
12135 is_relaying: false,
12136 is_coordinating: false,
12137 ..Default::default()
12138 };
12139
12140 let snapshot = AnnouncementAssistSnapshot::from_node_status(&status);
12141 assert_eq!(snapshot.nat_type.as_deref(), Some("Full Cone"));
12142 assert_eq!(snapshot.can_receive_direct, Some(true));
12143 assert_eq!(snapshot.relay_capable, Some(true));
12144 assert_eq!(snapshot.coordinator_capable, Some(true));
12145 assert_eq!(snapshot.relay_active, Some(false));
12146 assert_eq!(snapshot.coordinator_active, Some(false));
12147 }
12148
12149 #[test]
12151 fn identity_announcement_nat_fields_round_trip() {
12152 use identity::{AgentId, MachineId};
12153
12154 let unsigned = IdentityAnnouncementUnsigned {
12155 agent_id: AgentId([1u8; 32]),
12156 machine_id: MachineId([2u8; 32]),
12157 user_id: None,
12158 agent_certificate: None,
12159 machine_public_key: vec![0u8; 10],
12160 addresses: Vec::new(),
12161 announced_at: 9999,
12162 nat_type: Some("FullCone".to_string()),
12163 can_receive_direct: Some(true),
12164 is_relay: Some(false),
12165 is_coordinator: Some(true),
12166 reachable_via: vec![MachineId([5u8; 32])],
12167 relay_candidates: vec![MachineId([6u8; 32])],
12168 };
12169 let bytes = bincode::serialize(&unsigned).expect("serialize");
12170 let decoded: IdentityAnnouncementUnsigned =
12171 bincode::deserialize(&bytes).expect("deserialize");
12172 assert_eq!(decoded.nat_type.as_deref(), Some("FullCone"));
12173 assert_eq!(decoded.can_receive_direct, Some(true));
12174 assert_eq!(decoded.is_relay, Some(false));
12175 assert_eq!(decoded.is_coordinator, Some(true));
12176 assert_eq!(decoded.reachable_via, vec![MachineId([5u8; 32])]);
12177 assert_eq!(decoded.relay_candidates, vec![MachineId([6u8; 32])]);
12178 }
12179
12180 #[tokio::test]
12181 async fn announcement_decode_helpers_match_bincode_serialize_wire_format() {
12182 let temp = tempfile::tempdir().unwrap();
12183 let agent = Agent::builder()
12184 .with_machine_key(temp.path().join("machine.key"))
12185 .with_agent_key_path(temp.path().join("agent.key"))
12186 .with_agent_cert_path(temp.path().join("agent.cert"))
12187 .with_contact_store_path(temp.path().join("contacts.json"))
12188 .build()
12189 .await
12190 .unwrap();
12191
12192 let identity = agent.build_identity_announcement(false, false).unwrap();
12193 let identity_bytes = serialize_identity_announcement(&identity).unwrap();
12194 let decoded_identity = deserialize_identity_announcement(&identity_bytes).unwrap();
12195 assert_eq!(decoded_identity.agent_id, identity.agent_id);
12196 assert_eq!(decoded_identity.machine_id, identity.machine_id);
12197 assert_eq!(decoded_identity.agent_public_key, identity.agent_public_key);
12199 assert!(
12200 !decoded_identity.agent_public_key.is_empty(),
12201 "v2 announcement must carry the agent public key"
12202 );
12203 let machine = agent.build_machine_announcement().unwrap();
12204 let machine_bytes = bincode::serialize(&machine).unwrap();
12205 let decoded_machine = deserialize_machine_announcement(&machine_bytes).unwrap();
12206 assert_eq!(decoded_machine.machine_id, machine.machine_id);
12207 assert_eq!(decoded_machine.addresses, machine.addresses);
12208 }
12209
12210 #[tokio::test]
12211 async fn deserialize_identity_announcement_rejects_trailing_bytes() {
12212 let temp = tempfile::tempdir().unwrap();
12213 let agent = Agent::builder()
12214 .with_machine_key(temp.path().join("machine.key"))
12215 .with_agent_key_path(temp.path().join("agent.key"))
12216 .with_agent_cert_path(temp.path().join("agent.cert"))
12217 .with_contact_store_path(temp.path().join("contacts.json"))
12218 .build()
12219 .await
12220 .unwrap();
12221
12222 let announcement = agent.build_identity_announcement(false, false).unwrap();
12223 let mut bytes = bincode::serialize(&announcement).unwrap();
12224 bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
12225
12226 assert!(
12227 deserialize_identity_announcement(&bytes).is_err(),
12228 "identity announcements with trailing bytes must be rejected"
12229 );
12230 }
12231
12232 #[test]
12235 fn identity_announcement_no_nat_fields_round_trip() {
12236 use identity::{AgentId, MachineId};
12237
12238 let unsigned = IdentityAnnouncementUnsigned {
12239 agent_id: AgentId([3u8; 32]),
12240 machine_id: MachineId([4u8; 32]),
12241 user_id: None,
12242 agent_certificate: None,
12243 machine_public_key: vec![0u8; 10],
12244 addresses: Vec::new(),
12245 announced_at: 42,
12246 nat_type: None,
12247 can_receive_direct: None,
12248 is_relay: None,
12249 is_coordinator: None,
12250 reachable_via: Vec::new(),
12251 relay_candidates: Vec::new(),
12252 };
12253 let bytes = bincode::serialize(&unsigned).expect("serialize");
12254 let decoded: IdentityAnnouncementUnsigned =
12255 bincode::deserialize(&bytes).expect("deserialize");
12256 assert!(decoded.nat_type.is_none());
12257 assert!(decoded.can_receive_direct.is_none());
12258 assert!(decoded.is_relay.is_none());
12259 assert!(decoded.is_coordinator.is_none());
12260 assert!(decoded.reachable_via.is_empty());
12261 assert!(decoded.relay_candidates.is_empty());
12262 }
12263
12264 #[test]
12267 fn identity_announcement_reachable_via_round_trip() {
12268 use identity::{AgentId, MachineId};
12269
12270 let coord_a = MachineId([0xAAu8; 32]);
12271 let coord_b = MachineId([0xBBu8; 32]);
12272 let unsigned = IdentityAnnouncementUnsigned {
12273 agent_id: AgentId([9u8; 32]),
12274 machine_id: MachineId([8u8; 32]),
12275 user_id: None,
12276 agent_certificate: None,
12277 machine_public_key: vec![0u8; 10],
12278 addresses: Vec::new(),
12279 announced_at: 555,
12280 nat_type: Some("Symmetric".to_string()),
12281 can_receive_direct: Some(false),
12282 is_relay: Some(false),
12283 is_coordinator: Some(false),
12284 reachable_via: vec![coord_a, coord_b],
12285 relay_candidates: vec![coord_a],
12286 };
12287 let bytes = bincode::serialize(&unsigned).expect("serialize");
12288 let decoded: IdentityAnnouncementUnsigned =
12289 bincode::deserialize(&bytes).expect("deserialize");
12290 assert_eq!(decoded.reachable_via, vec![coord_a, coord_b]);
12291 assert_eq!(decoded.relay_candidates, vec![coord_a]);
12292
12293 let re_encoded = bincode::serialize(&decoded).expect("re-serialize");
12298 assert_eq!(
12299 bytes, re_encoded,
12300 "canonical bincode round-trip must be stable"
12301 );
12302 }
12303
12304 #[test]
12305 fn user_announcement_sign_and_verify() {
12306 let user_kp = identity::UserKeypair::generate().unwrap();
12307 let agent_kp_a = identity::AgentKeypair::generate().unwrap();
12308 let agent_kp_b = identity::AgentKeypair::generate().unwrap();
12309 let cert_a = identity::AgentCertificate::issue(&user_kp, &agent_kp_a).unwrap();
12310 let cert_b = identity::AgentCertificate::issue(&user_kp, &agent_kp_b).unwrap();
12311
12312 let announcement = UserAnnouncement::sign(&user_kp, vec![cert_a, cert_b], 1234).unwrap();
12313 announcement.verify().expect("freshly-signed must verify");
12314 assert_eq!(announcement.user_id, user_kp.user_id());
12315 assert_eq!(announcement.agent_certificates.len(), 2);
12316 }
12317
12318 #[test]
12319 fn deserialize_user_announcement_rejects_trailing_bytes() {
12320 let user_kp = identity::UserKeypair::generate().unwrap();
12321 let agent_kp = identity::AgentKeypair::generate().unwrap();
12322 let cert = identity::AgentCertificate::issue(&user_kp, &agent_kp).unwrap();
12323 let announcement = UserAnnouncement::sign(&user_kp, vec![cert], 1234).unwrap();
12324 let mut bytes = bincode::serialize(&announcement).unwrap();
12325 bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
12326
12327 assert!(
12328 deserialize_user_announcement(&bytes).is_err(),
12329 "user announcements with trailing bytes must be rejected"
12330 );
12331 }
12332
12333 #[test]
12334 fn user_announcement_rejects_foreign_certificate() {
12335 let user_kp = identity::UserKeypair::generate().unwrap();
12336 let other_user = identity::UserKeypair::generate().unwrap();
12337 let agent_kp = identity::AgentKeypair::generate().unwrap();
12338 let foreign_cert = identity::AgentCertificate::issue(&other_user, &agent_kp).unwrap();
12341 let err = UserAnnouncement::sign(&user_kp, vec![foreign_cert], 0).unwrap_err();
12342 assert!(
12343 err.to_string().contains("different user"),
12344 "unexpected error: {err}"
12345 );
12346 }
12347
12348 #[test]
12349 fn user_announcement_tampered_agent_cert_list_fails() {
12350 let user_kp = identity::UserKeypair::generate().unwrap();
12351 let agent_kp = identity::AgentKeypair::generate().unwrap();
12352 let cert = identity::AgentCertificate::issue(&user_kp, &agent_kp).unwrap();
12353
12354 let mut announcement = UserAnnouncement::sign(&user_kp, vec![cert], 10).unwrap();
12355 let other_user = identity::UserKeypair::generate().unwrap();
12357 let other_agent = identity::AgentKeypair::generate().unwrap();
12358 let foreign_cert = identity::AgentCertificate::issue(&other_user, &other_agent).unwrap();
12359 announcement.agent_certificates.push(foreign_cert);
12360 assert!(
12361 announcement.verify().is_err(),
12362 "announcement with appended foreign cert must fail verification"
12363 );
12364 }
12365
12366 #[test]
12367 fn user_announcement_tampered_user_public_key_fails() {
12368 let user_kp = identity::UserKeypair::generate().unwrap();
12369 let agent_kp = identity::AgentKeypair::generate().unwrap();
12370 let cert = identity::AgentCertificate::issue(&user_kp, &agent_kp).unwrap();
12371 let mut announcement = UserAnnouncement::sign(&user_kp, vec![cert], 10).unwrap();
12372 let other = identity::UserKeypair::generate().unwrap();
12373 announcement.user_public_key = other.public_key().as_bytes().to_vec();
12374 assert!(announcement.verify().is_err());
12375 }
12376
12377 #[test]
12378 fn user_shard_topic_is_deterministic() {
12379 let user_id = identity::UserId([5u8; 32]);
12380 let topic_a = shard_topic_for_user(&user_id);
12381 let topic_b = shard_topic_for_user(&user_id);
12382 assert_eq!(topic_a, topic_b);
12383 assert!(topic_a.starts_with("x0x.user.shard.v2."));
12384 }
12385}
12386#[test]
12387fn agent_shard_topic_is_deterministic() {
12388 let agent_id = identity::AgentId([6u8; 32]);
12389 let topic_a = shard_topic_for_agent(&agent_id);
12390 let topic_b = shard_topic_for_agent(&agent_id);
12391 assert_eq!(topic_a, topic_b);
12392 assert!(topic_a.starts_with("x0x.identity.shard.v2."));
12393}
12394
12395#[test]
12396fn machine_shard_topic_is_deterministic() {
12397 let machine_id = identity::MachineId([7u8; 32]);
12398 let topic_a = shard_topic_for_machine(&machine_id);
12399 let topic_b = shard_topic_for_machine(&machine_id);
12400 assert_eq!(topic_a, topic_b);
12401 assert!(topic_a.starts_with("x0x.machine.shard.v2."));
12402}
12403
12404#[test]
12405fn rendezvous_shard_topic_is_deterministic() {
12406 let agent_id = identity::AgentId([8u8; 32]);
12407 let topic_a = rendezvous_shard_topic_for_agent(&agent_id);
12408 let topic_b = rendezvous_shard_topic_for_agent(&agent_id);
12409 assert_eq!(topic_a, topic_b);
12410 assert!(topic_a.starts_with("x0x.rendezvous.shard."));
12411}
12412
12413#[test]
12414fn different_ids_produce_different_shard_topics() {
12415 let agent_a = identity::AgentId([1u8; 32]);
12416 let agent_b = identity::AgentId([2u8; 32]);
12417 let topic_a = shard_topic_for_agent(&agent_a);
12418 let topic_b = shard_topic_for_agent(&agent_b);
12419 assert_ne!(
12420 topic_a, topic_b,
12421 "different agent IDs should produce different shard topics"
12422 );
12423}
12424
12425#[test]
12426fn collect_local_interface_addrs_returns_non_empty() {
12427 let addrs = collect_local_interface_addrs(5483);
12428 assert!(!addrs.is_empty(), "should find at least one interface");
12429 for addr in &addrs {
12430 assert_eq!(addr.port(), 5483, "all addrs should use port 5483");
12431 }
12432}
12433
12434#[test]
12435fn collect_local_interface_addrs_returns_reasonable_results() {
12436 let addrs = collect_local_interface_addrs(9000);
12437 assert!(!addrs.is_empty(), "should find at least one interface");
12438 for addr in &addrs {
12439 assert_eq!(addr.port(), 9000, "all addrs should use port 9000");
12440 }
12441}
12442
12443#[test]
12444fn is_globally_routable_v4_private() {
12445 assert!(!is_globally_routable(std::net::IpAddr::V4(
12446 std::net::Ipv4Addr::new(10, 0, 0, 1)
12447 )));
12448 assert!(!is_globally_routable(std::net::IpAddr::V4(
12449 std::net::Ipv4Addr::new(172, 16, 0, 1)
12450 )));
12451 assert!(!is_globally_routable(std::net::IpAddr::V4(
12452 std::net::Ipv4Addr::new(192, 168, 1, 1)
12453 )));
12454}
12455
12456#[test]
12457fn is_globally_routable_v4_global() {
12458 assert!(is_globally_routable(std::net::IpAddr::V4(
12459 std::net::Ipv4Addr::new(8, 8, 8, 8)
12460 )));
12461 assert!(is_globally_routable(std::net::IpAddr::V4(
12462 std::net::Ipv4Addr::new(1, 2, 3, 4)
12463 )));
12464}
12465
12466#[test]
12467fn is_globally_routable_v6_private() {
12468 assert!(!is_globally_routable(std::net::IpAddr::V6(
12469 std::net::Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)
12470 )));
12471 assert!(!is_globally_routable(std::net::IpAddr::V6(
12472 std::net::Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1)
12473 )));
12474 assert!(!is_globally_routable(std::net::IpAddr::V6(
12475 std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)
12476 )));
12477}
12478
12479#[test]
12480fn is_globally_routable_v6_global() {
12481 assert!(is_globally_routable(std::net::IpAddr::V6(
12482 std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)
12483 )));
12484 assert!(is_globally_routable(std::net::IpAddr::V6(
12485 std::net::Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888)
12486 )));
12487}
12488
12489#[test]
12490fn is_globally_routable_v4_cgnat() {
12491 assert!(!is_globally_routable(std::net::IpAddr::V4(
12492 std::net::Ipv4Addr::new(100, 64, 0, 1)
12493 )));
12494 assert!(!is_globally_routable(std::net::IpAddr::V4(
12495 std::net::Ipv4Addr::new(100, 127, 255, 255)
12496 )));
12497}
12498
12499#[test]
12500fn is_globally_routable_v4_documentation() {
12501 assert!(!is_globally_routable(std::net::IpAddr::V4(
12502 std::net::Ipv4Addr::new(192, 0, 2, 1)
12503 )));
12504 assert!(!is_globally_routable(std::net::IpAddr::V4(
12505 std::net::Ipv4Addr::new(198, 51, 100, 1)
12506 )));
12507 assert!(!is_globally_routable(std::net::IpAddr::V4(
12508 std::net::Ipv4Addr::new(203, 0, 113, 1)
12509 )));
12510}
12511
12512#[test]
12513fn is_globally_routable_v4_broadcast() {
12514 assert!(!is_globally_routable(std::net::IpAddr::V4(
12515 std::net::Ipv4Addr::new(255, 255, 255, 255)
12516 )));
12517}
12518
12519#[test]
12520fn is_globally_routable_v4_unspecified() {
12521 assert!(!is_globally_routable(std::net::IpAddr::V4(
12522 std::net::Ipv4Addr::new(0, 0, 0, 0)
12523 )));
12524}
12525
12526#[test]
12527fn is_globally_routable_v6_unspecified() {
12528 assert!(!is_globally_routable(std::net::IpAddr::V6(
12529 std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)
12530 )));
12531}
12532
12533#[test]
12534fn is_globally_routable_v6_unique_local() {
12535 assert!(!is_globally_routable(std::net::IpAddr::V6(
12536 std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1)
12537 )));
12538}
12539
12540#[test]
12541fn is_globally_routable_v6_site_local() {
12542 assert!(!is_globally_routable(std::net::IpAddr::V6(
12543 std::net::Ipv6Addr::new(0xfec0, 0, 0, 0, 0, 0, 0, 1)
12544 )));
12545}
12546
12547#[test]
12548fn push_unique_adds_new_item() {
12549 let mut items = vec![1, 2, 3];
12550 push_unique(&mut items, 4);
12551 assert_eq!(items, vec![1, 2, 3, 4]);
12552}
12553
12554#[test]
12555fn push_unique_skips_existing_item() {
12556 let mut items = vec![1, 2, 3];
12557 push_unique(&mut items, 2);
12558 assert_eq!(items, vec![1, 2, 3]);
12559}
12560
12561#[test]
12562fn push_unique_works_with_empty() {
12563 let mut items: Vec<i32> = vec![];
12564 push_unique(&mut items, 42);
12565 assert_eq!(items, vec![42]);
12566}
12567
12568#[cfg(test)]
12569fn discovered_agent_fixture(
12570 tag: u8,
12571 announced_at: u64,
12572 addrs: &[&str],
12573 user_id: Option<identity::UserId>,
12574) -> DiscoveredAgent {
12575 DiscoveredAgent {
12576 agent_id: identity::AgentId([tag; 32]),
12577 machine_id: identity::MachineId([tag; 32]),
12578 user_id,
12579 addresses: addrs
12580 .iter()
12581 .map(|a| a.parse().expect("valid socket addr"))
12582 .collect(),
12583 announced_at,
12584 last_seen: announced_at,
12585 machine_public_key: vec![tag],
12586 nat_type: None,
12587 can_receive_direct: None,
12588 is_relay: None,
12589 is_coordinator: None,
12590 reachable_via: Vec::new(),
12591 relay_candidates: Vec::new(),
12592 cert_not_after: None,
12593 agent_certificate: None,
12594 agent_public_key: Vec::new(),
12595 }
12596}
12597
12598#[cfg(test)]
12599fn signed_identity_announcement_fixture(
12600 agent_id: identity::AgentId,
12601 machine: &identity::MachineKeypair,
12602 announced_at: u64,
12603) -> IdentityAnnouncement {
12604 let machine_public_key = machine.public_key().as_bytes().to_vec();
12605 let unsigned = IdentityAnnouncementUnsigned {
12606 agent_id,
12607 machine_id: machine.machine_id(),
12608 user_id: None,
12609 agent_certificate: None,
12610 machine_public_key: machine_public_key.clone(),
12611 addresses: Vec::new(),
12612 announced_at,
12613 nat_type: None,
12614 can_receive_direct: None,
12615 is_relay: None,
12616 is_coordinator: None,
12617 reachable_via: Vec::new(),
12618 relay_candidates: Vec::new(),
12619 };
12620 let unsigned_bytes = bincode::serialize(&unsigned).expect("serialize announcement");
12621 let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
12622 machine.secret_key(),
12623 &unsigned_bytes,
12624 )
12625 .expect("sign announcement")
12626 .as_bytes()
12627 .to_vec();
12628 IdentityAnnouncement {
12629 agent_id,
12630 machine_id: machine.machine_id(),
12631 user_id: None,
12632 agent_certificate: None,
12633 machine_public_key,
12634 machine_signature,
12635 addresses: Vec::new(),
12636 announced_at,
12637 nat_type: None,
12638 can_receive_direct: None,
12639 is_relay: None,
12640 is_coordinator: None,
12641 reachable_via: Vec::new(),
12642 relay_candidates: Vec::new(),
12643 agent_public_key: Vec::new(),
12644 }
12645}
12646
12647#[cfg(test)]
12648fn verified_identity_origin_message(sender: &identity::AgentKeypair) -> gossip::PubSubMessage {
12649 gossip::PubSubMessage {
12650 topic: "identity-ingest-test".to_string(),
12651 payload: bytes::Bytes::new(),
12652 sender: Some(sender.agent_id()),
12653 sender_public_key: Some(sender.public_key().as_bytes().to_vec()),
12654 verified: true,
12655 trust_level: None,
12656 }
12657}
12658
12659#[tokio::test]
12660async fn direct_origin_identity_ingest_populates_authenticated_binding() {
12661 let sender = identity::AgentKeypair::generate().expect("sender keygen");
12662 let machine = identity::MachineKeypair::generate().expect("machine keygen");
12663 let now = 1_000;
12664 let announcement = signed_identity_announcement_fixture(sender.agent_id(), &machine, now);
12665 announcement.verify().expect("valid machine announcement");
12666 let message = verified_identity_origin_message(&sender);
12667 let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
12668 dm_inbox::AuthenticatedMachineBindingCache::default(),
12669 ));
12670
12671 assert!(
12672 record_authenticated_machine_binding_from_message(&bindings, &message, &announcement, now,)
12673 .await
12674 );
12675 assert_eq!(
12676 dm_inbox::authenticated_machine_binding_for_testing(&bindings, &sender.agent_id()).await,
12677 Some(machine.machine_id())
12678 );
12679}
12680
12681#[tokio::test]
12682async fn wrong_origin_machine_announcement_cannot_populate_or_overwrite_binding() {
12683 let victim = identity::AgentKeypair::generate().expect("victim keygen");
12684 let attacker = identity::AgentKeypair::generate().expect("attacker keygen");
12685 let trusted_machine = identity::MachineKeypair::generate().expect("trusted machine keygen");
12686 let attacker_machine = identity::MachineKeypair::generate().expect("attacker machine keygen");
12687 let now = 2_000;
12688 let trusted = signed_identity_announcement_fixture(victim.agent_id(), &trusted_machine, now);
12689 let trusted_message = verified_identity_origin_message(&victim);
12690 let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
12691 dm_inbox::AuthenticatedMachineBindingCache::default(),
12692 ));
12693 assert!(
12694 record_authenticated_machine_binding_from_message(
12695 &bindings,
12696 &trusted_message,
12697 &trusted,
12698 now,
12699 )
12700 .await
12701 );
12702
12703 let poisoned =
12704 signed_identity_announcement_fixture(victim.agent_id(), &attacker_machine, now + 1);
12705 poisoned.verify().expect("valid attacker machine signature");
12706 let attacker_message = verified_identity_origin_message(&attacker);
12707 assert!(
12708 !record_authenticated_machine_binding_from_message(
12709 &bindings,
12710 &attacker_message,
12711 &poisoned,
12712 now + 1,
12713 )
12714 .await
12715 );
12716 assert_eq!(
12717 dm_inbox::authenticated_machine_binding_for_testing(&bindings, &victim.agent_id()).await,
12718 Some(trusted_machine.machine_id())
12719 );
12720}
12721
12722#[tokio::test]
12723async fn verified_rebroadcast_cannot_populate_or_overwrite_authenticated_binding() {
12724 let origin = identity::AgentKeypair::generate().expect("origin keygen");
12725 let relay = identity::AgentKeypair::generate().expect("relay keygen");
12726 let machine_a = identity::MachineKeypair::generate().expect("machine A keygen");
12727 let machine_b = identity::MachineKeypair::generate().expect("machine B keygen");
12728 let now = 3_000;
12729 let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
12730 dm_inbox::AuthenticatedMachineBindingCache::default(),
12731 ));
12732 let direct = signed_identity_announcement_fixture(origin.agent_id(), &machine_a, now);
12733 assert!(
12734 record_authenticated_machine_binding_from_message(
12735 &bindings,
12736 &verified_identity_origin_message(&origin),
12737 &direct,
12738 now,
12739 )
12740 .await
12741 );
12742
12743 let rebroadcast = signed_identity_announcement_fixture(origin.agent_id(), &machine_b, now + 1);
12744 let relay_message = verified_identity_origin_message(&relay);
12745 assert!(
12746 !record_authenticated_machine_binding_from_message(
12747 &bindings,
12748 &relay_message,
12749 &rebroadcast,
12750 now + 1,
12751 )
12752 .await
12753 );
12754 assert_eq!(
12755 dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
12756 Some(machine_a.machine_id())
12757 );
12758}
12759
12760#[tokio::test]
12761async fn missing_or_invalid_sender_key_cannot_populate_authenticated_binding() {
12762 let origin = identity::AgentKeypair::generate().expect("origin keygen");
12763 let machine = identity::MachineKeypair::generate().expect("machine keygen");
12764 let now = 4_000;
12765 let announcement = signed_identity_announcement_fixture(origin.agent_id(), &machine, now);
12766 let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
12767 dm_inbox::AuthenticatedMachineBindingCache::default(),
12768 ));
12769
12770 let mut missing = verified_identity_origin_message(&origin);
12771 missing.sender_public_key = None;
12772 assert!(
12773 !record_authenticated_machine_binding_from_message(
12774 &bindings,
12775 &missing,
12776 &announcement,
12777 now,
12778 )
12779 .await
12780 );
12781 let mut invalid = verified_identity_origin_message(&origin);
12782 invalid.sender_public_key = Some(vec![0xFF; 8]);
12783 assert!(
12784 !record_authenticated_machine_binding_from_message(
12785 &bindings,
12786 &invalid,
12787 &announcement,
12788 now,
12789 )
12790 .await
12791 );
12792 assert_eq!(
12793 dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
12794 None
12795 );
12796}
12797
12798#[tokio::test]
12799async fn far_future_direct_origin_cannot_poison_portable_move_ordering() {
12800 let origin = identity::AgentKeypair::generate().expect("origin keygen");
12801 let machine_a = identity::MachineKeypair::generate().expect("machine A keygen");
12802 let machine_b = identity::MachineKeypair::generate().expect("machine B keygen");
12803 let now = 5_000;
12804 let message = verified_identity_origin_message(&origin);
12805 let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
12806 dm_inbox::AuthenticatedMachineBindingCache::default(),
12807 ));
12808 let current = signed_identity_announcement_fixture(origin.agent_id(), &machine_a, now);
12809 assert!(
12810 record_authenticated_machine_binding_from_message(&bindings, &message, ¤t, now,)
12811 .await
12812 );
12813
12814 let far_future = signed_identity_announcement_fixture(origin.agent_id(), &machine_b, u64::MAX);
12815 assert!(
12816 !record_authenticated_machine_binding_from_message(&bindings, &message, &far_future, now,)
12817 .await
12818 );
12819 assert_eq!(
12820 dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
12821 Some(machine_a.machine_id())
12822 );
12823
12824 let normal_move = signed_identity_announcement_fixture(origin.agent_id(), &machine_b, now + 1);
12825 assert!(
12826 record_authenticated_machine_binding_from_message(
12827 &bindings,
12828 &message,
12829 &normal_move,
12830 now + 1,
12831 )
12832 .await
12833 );
12834 assert_eq!(
12835 dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
12836 Some(machine_b.machine_id())
12837 );
12838}
12839
12840#[tokio::test]
12841async fn revocation_discovery_eviction_preserves_authenticated_binding() {
12842 let tempdir = tempfile::tempdir().expect("tempdir");
12843 let receiver = Agent::builder()
12844 .with_identity_dir(tempdir.path())
12845 .build()
12846 .await
12847 .expect("receiver agent");
12848 let origin = identity::AgentKeypair::generate().expect("origin keygen");
12849 let machine = identity::MachineKeypair::generate().expect("machine keygen");
12850 let now = 6_000;
12851 let announcement = signed_identity_announcement_fixture(origin.agent_id(), &machine, now);
12852 assert!(
12853 record_authenticated_machine_binding_from_message(
12854 &receiver.authenticated_machine_bindings,
12855 &verified_identity_origin_message(&origin),
12856 &announcement,
12857 now,
12858 )
12859 .await
12860 );
12861 let mut discovered = discovered_agent_fixture(0x66, now, &[], None);
12862 discovered.agent_id = origin.agent_id();
12863 discovered.machine_id = machine.machine_id();
12864 discovered.machine_public_key = machine.public_key().as_bytes().to_vec();
12865 receiver
12866 .insert_discovered_agent_for_testing(discovered)
12867 .await;
12868 assert!(receiver.cached_agent(&origin.agent_id()).await.is_some());
12869
12870 receiver
12871 .evict_revoked_subject(&revocation::RevokedSubject::Machine(machine.machine_id()))
12872 .await;
12873
12874 assert!(receiver.cached_agent(&origin.agent_id()).await.is_none());
12875 assert_eq!(
12876 dm_inbox::authenticated_machine_binding_for_testing(
12877 &receiver.authenticated_machine_bindings,
12878 &origin.agent_id(),
12879 )
12880 .await,
12881 Some(machine.machine_id())
12882 );
12883}
12884
12885#[tokio::test]
12886async fn upsert_discovered_agent_replaces_addresses_on_fresher_announcement() {
12887 let cache = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
12888 let id = identity::AgentId([7; 32]);
12889
12890 upsert_discovered_agent(
12891 &cache,
12892 discovered_agent_fixture(7, 100, &["10.0.0.1:5483", "8.8.8.8:5483"], None),
12893 )
12894 .await;
12895 upsert_discovered_agent(
12899 &cache,
12900 discovered_agent_fixture(7, 200, &["1.2.3.4:5483"], None),
12901 )
12902 .await;
12903
12904 let guard = cache.read().await;
12905 let entry = guard.get(&id).expect("entry present");
12906 assert_eq!(
12907 entry.addresses,
12908 vec!["1.2.3.4:5483".parse().expect("addr")],
12909 "fresher announcement must replace the address set, not union it"
12910 );
12911}
12912
12913#[tokio::test]
12914async fn upsert_discovered_agent_ignores_stale_announcement_addresses() {
12915 let cache = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
12916 let id = identity::AgentId([8; 32]);
12917
12918 upsert_discovered_agent(
12919 &cache,
12920 discovered_agent_fixture(8, 200, &["1.2.3.4:5483"], None),
12921 )
12922 .await;
12923 upsert_discovered_agent(
12926 &cache,
12927 discovered_agent_fixture(8, 100, &["10.0.0.9:5483"], None),
12928 )
12929 .await;
12930
12931 let guard = cache.read().await;
12932 let entry = guard.get(&id).expect("entry present");
12933 assert_eq!(
12934 entry.addresses,
12935 vec!["1.2.3.4:5483".parse().expect("addr")],
12936 "stale announcement must not add addresses"
12937 );
12938 assert_eq!(
12939 entry.announced_at, 200,
12940 "stale announcement must not regress announced_at"
12941 );
12942}
12943
12944#[tokio::test]
12945async fn upsert_discovered_agent_preserves_known_user_id() {
12946 let cache = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
12947 let id = identity::AgentId([9; 32]);
12948 let user = identity::UserId([9; 32]);
12949
12950 upsert_discovered_agent(
12951 &cache,
12952 discovered_agent_fixture(9, 100, &["1.2.3.4:5483"], Some(user)),
12953 )
12954 .await;
12955 upsert_discovered_agent(
12957 &cache,
12958 discovered_agent_fixture(9, 200, &["1.2.3.4:5483"], None),
12959 )
12960 .await;
12961
12962 let guard = cache.read().await;
12963 let entry = guard.get(&id).expect("entry present");
12964 assert_eq!(
12965 entry.user_id,
12966 Some(user),
12967 "a fresher anonymous announcement must not erase a disclosed user_id"
12968 );
12969}
12970
12971#[test]
12972fn sort_discovered_machine_sorts_fields() {
12973 let mut machine = DiscoveredMachine {
12974 machine_id: identity::MachineId([3u8; 32]),
12975 addresses: vec![
12976 "10.0.0.2:5483".parse::<std::net::SocketAddr>().unwrap(),
12977 "10.0.0.1:5483".parse::<std::net::SocketAddr>().unwrap(),
12978 ],
12979 announced_at: 100,
12980 last_seen: 100,
12981 machine_public_key: vec![],
12982 nat_type: None,
12983 can_receive_direct: None,
12984 is_relay: None,
12985 is_coordinator: None,
12986 reachable_via: vec![
12987 identity::MachineId([2u8; 32]),
12988 identity::MachineId([1u8; 32]),
12989 ],
12990 relay_candidates: vec![
12991 identity::MachineId([4u8; 32]),
12992 identity::MachineId([3u8; 32]),
12993 ],
12994 agent_ids: vec![identity::AgentId([2u8; 32]), identity::AgentId([1u8; 32])],
12995 user_ids: vec![identity::UserId([2u8; 32]), identity::UserId([1u8; 32])],
12996 };
12997 sort_discovered_machine(&mut machine);
12998 assert_eq!(
12999 machine.addresses[0],
13000 "10.0.0.1:5483".parse::<std::net::SocketAddr>().unwrap()
13001 );
13002 assert_eq!(
13003 machine.addresses[1],
13004 "10.0.0.2:5483".parse::<std::net::SocketAddr>().unwrap()
13005 );
13006 assert_eq!(machine.reachable_via[0], identity::MachineId([1u8; 32]));
13007 assert_eq!(machine.reachable_via[1], identity::MachineId([2u8; 32]));
13008 assert_eq!(machine.relay_candidates[0], identity::MachineId([3u8; 32]));
13009 assert_eq!(machine.relay_candidates[1], identity::MachineId([4u8; 32]));
13010 assert_eq!(machine.agent_ids[0], identity::AgentId([1u8; 32]));
13011 assert_eq!(machine.agent_ids[1], identity::AgentId([2u8; 32]));
13012 assert_eq!(machine.user_ids[0], identity::UserId([1u8; 32]));
13013 assert_eq!(machine.user_ids[1], identity::UserId([2u8; 32]));
13014}
13015
13016#[tokio::test]
13017async fn dm_inbox_capability_upgrade_visible_to_late_subscriber() {
13018 let dir = tempfile::tempdir().expect("tmpdir");
13026 let agent = Agent::builder()
13027 .with_machine_key(dir.path().join("machine.key"))
13028 .with_agent_key_path(dir.path().join("agent.key"))
13029 .with_peer_cache_dir(dir.path().join("peers"))
13030 .with_network_config(network::NetworkConfig::default())
13031 .build()
13032 .await
13033 .expect("agent");
13034
13035 let kem = std::sync::Arc::new(
13036 groups::kem_envelope::AgentKemKeypair::generate().expect("kem keypair"),
13037 );
13038 agent
13039 .start_dm_inbox(kem, dm_inbox::DmInboxConfig::default())
13040 .await
13041 .expect("start dm inbox");
13042
13043 let late_rx = agent.dm_capabilities_tx.subscribe();
13047 let caps = late_rx.borrow().clone();
13048 assert!(
13049 caps.gossip_inbox,
13050 "DM capability upgrade must be visible to subscribers that attach after start_dm_inbox (issue #101)"
13051 );
13052 assert!(
13053 !caps.kem_public_key.is_empty(),
13054 "upgraded capabilities must carry the KEM public key"
13055 );
13056 agent.stop_dm_inbox().await;
13057}
13058
13059#[test]
13060fn deserialize_identity_announcement_rejects_empty() {
13061 let result = deserialize_identity_announcement(&[]);
13062 assert!(result.is_err());
13063}
13064
13065#[test]
13066fn deserialize_machine_announcement_rejects_empty() {
13067 let result = deserialize_machine_announcement(&[]);
13068 assert!(result.is_err());
13069}
13070
13071#[test]
13072fn deserialize_identity_announcement_rejects_garbage() {
13073 let result = deserialize_identity_announcement(b"not-a-valid-bincode");
13074 assert!(result.is_err());
13075}
13076
13077#[test]
13078fn deserialize_machine_announcement_rejects_garbage() {
13079 let result = deserialize_machine_announcement(b"not-a-valid-bincode");
13080 assert!(result.is_err());
13081}