#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(missing_docs)]
pub mod error;
pub mod identity;
pub mod storage;
pub mod revocation;
pub mod bootstrap;
pub mod network;
pub mod streams;
pub mod forward;
pub mod contacts;
pub mod trust;
pub mod connectivity;
pub mod gossip;
pub mod crdt;
pub mod kv;
pub mod groups;
pub mod mls;
pub mod a2a;
pub mod direct;
pub mod dm;
pub mod dm_capability;
pub mod dm_capability_service;
pub mod dm_inbox;
pub mod dm_send;
pub mod peer_relay;
pub mod presence;
pub mod upgrade;
pub mod files;
pub mod connect;
pub mod exec;
pub mod constitution;
pub mod logging;
pub mod api;
pub mod cli;
pub mod server;
pub use gossip::{
GossipConfig, GossipRuntime, PubSubManager, PubSubMessage, PubSubStats, PubSubStatsSnapshot,
SigningContext, Subscription,
};
pub use direct::{DirectMessage, DirectMessageReceiver, DirectMessaging};
use saorsa_gossip_membership::Membership as _;
pub struct Agent {
identity: std::sync::Arc<identity::Identity>,
#[allow(dead_code)]
network: Option<std::sync::Arc<network::NetworkNode>>,
gossip_runtime: Option<std::sync::Arc<gossip::GossipRuntime>>,
bootstrap_cache: Option<std::sync::Arc<ant_quic::BootstrapCache>>,
gossip_cache_adapter: Option<saorsa_gossip_coordinator::GossipCacheAdapter>,
identity_discovery_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
>,
authenticated_machine_bindings: dm_inbox::AuthenticatedMachineBindings,
machine_discovery_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
>,
user_discovery_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::UserId, DiscoveredUser>>,
>,
identity_listener_started: std::sync::atomic::AtomicBool,
heartbeat_interval_secs: u64,
identity_ttl_secs: u64,
heartbeat_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
discovery_cache_reaper_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
rendezvous_advertised: std::sync::atomic::AtomicBool,
contact_store: std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>,
direct_messaging: std::sync::Arc<direct::DirectMessaging>,
network_event_listener_started: std::sync::atomic::AtomicBool,
direct_listener_started: std::sync::atomic::AtomicBool,
presence: Option<std::sync::Arc<presence::PresenceWrapper>>,
user_identity_consented: std::sync::Arc<std::sync::atomic::AtomicBool>,
capability_store: std::sync::Arc<dm_capability::CapabilityStore>,
dm_capabilities_tx: std::sync::Arc<tokio::sync::watch::Sender<dm::DmCapabilities>>,
dm_inflight_acks: std::sync::Arc<dm::InFlightAcks>,
recent_delivery_cache: std::sync::Arc<dm::RecentDeliveryCache>,
capability_advert_service:
tokio::sync::Mutex<Option<dm_capability_service::CapabilityAdvertService>>,
dm_inbox_service: tokio::sync::Mutex<Option<dm_inbox::DmInboxService>>,
revocation_set: std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>>,
identity_dir: Option<std::path::PathBuf>,
shutdown_token: tokio_util::sync::CancellationToken,
tracked_tasks: std::sync::Arc<std::sync::Mutex<TrackedTasks>>,
peer_relay: std::sync::Arc<peer_relay::PeerRelay>,
relay_candidates: std::sync::Arc<tokio::sync::RwLock<Vec<identity::AgentId>>>,
stream_accept: std::sync::Arc<streams::StreamAccept>,
}
struct TrackedTasks {
closed: bool,
handles: Vec<tokio::task::JoinHandle<()>>,
}
impl std::fmt::Debug for Agent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Agent")
.field("identity", &self.identity)
.field("network", &self.network.is_some())
.field("gossip_runtime", &self.gossip_runtime.is_some())
.field("bootstrap_cache", &self.bootstrap_cache.is_some())
.field("gossip_cache_adapter", &self.gossip_cache_adapter.is_some())
.finish()
}
}
impl Drop for Agent {
fn drop(&mut self) {
if let Ok(mut handle_guard) = self.heartbeat_handle.try_lock() {
if let Some(handle) = handle_guard.take() {
handle.abort();
}
}
if let Ok(mut reaper_guard) = self.discovery_cache_reaper_handle.try_lock() {
if let Some(handle) = reaper_guard.take() {
handle.abort();
}
}
}
}
#[derive(Debug, Clone)]
pub struct Message {
pub origin: String,
pub payload: Vec<u8>,
pub topic: String,
}
pub const IDENTITY_ANNOUNCE_TOPIC: &str = "x0x.identity.announce.v2";
pub const MACHINE_ANNOUNCE_TOPIC: &str = "x0x.machine.announce.v2";
pub const USER_ANNOUNCE_TOPIC: &str = "x0x.user.announce.v2";
pub const REVOCATION_TOPIC: &str = "x0x.revocation.v1";
#[must_use]
pub fn shard_topic_for_agent(agent_id: &identity::AgentId) -> String {
let shard = saorsa_gossip_rendezvous::calculate_shard(&agent_id.0);
format!("x0x.identity.shard.v2.{shard}")
}
#[must_use]
pub fn shard_topic_for_machine(machine_id: &identity::MachineId) -> String {
let shard = saorsa_gossip_rendezvous::calculate_shard(&machine_id.0);
format!("x0x.machine.shard.v2.{shard}")
}
#[must_use]
pub fn shard_topic_for_user(user_id: &identity::UserId) -> String {
let shard = saorsa_gossip_rendezvous::calculate_shard(&user_id.0);
format!("x0x.user.shard.v2.{shard}")
}
pub const RENDEZVOUS_SHARD_TOPIC_PREFIX: &str = "x0x.rendezvous.shard";
#[must_use]
pub fn rendezvous_shard_topic_for_agent(agent_id: &identity::AgentId) -> String {
let shard = saorsa_gossip_rendezvous::calculate_shard(&agent_id.0);
format!("{RENDEZVOUS_SHARD_TOPIC_PREFIX}.{shard}")
}
fn is_globally_routable(ip: std::net::IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(v4) => {
!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)
}
std::net::IpAddr::V6(v6) => {
let segs = v6.segments();
!v6.is_loopback() && !v6.is_unspecified() && (segs[0] & 0xffc0) != 0xfe80 && (segs[0] & 0xfe00) != 0xfc00 && (segs[0] & 0xfff0) != 0xfec0 }
}
}
pub fn is_publicly_advertisable(addr: std::net::SocketAddr) -> bool {
addr.port() > 0 && is_globally_routable(addr.ip())
}
fn filter_publicly_advertisable_addrs<I>(addresses: I) -> Vec<std::net::SocketAddr>
where
I: IntoIterator<Item = std::net::SocketAddr>,
{
addresses
.into_iter()
.filter(|addr| is_publicly_advertisable(*addr))
.collect()
}
fn is_local_discovery_addr(addr: std::net::SocketAddr) -> bool {
if addr.port() == 0 {
return false;
}
match addr.ip() {
std::net::IpAddr::V4(v4) => {
!v4.is_unspecified()
&& !v4.is_broadcast()
&& !v4.is_documentation()
&& !v4.is_link_local()
}
std::net::IpAddr::V6(v6) => {
let segs = v6.segments();
!v6.is_unspecified() && (segs[0] & 0xffc0) != 0xfe80 && (segs[0] & 0xfff0) != 0xfec0
}
}
}
fn filter_local_discovery_addrs<I>(addresses: I) -> Vec<std::net::SocketAddr>
where
I: IntoIterator<Item = std::net::SocketAddr>,
{
let mut filtered = Vec::new();
for addr in addresses {
if is_local_discovery_addr(addr) && !filtered.contains(&addr) {
filtered.push(addr);
}
}
filtered
}
fn filter_discovery_announcement_addrs<I>(
addresses: I,
allow_local_scope: bool,
) -> Vec<std::net::SocketAddr>
where
I: IntoIterator<Item = std::net::SocketAddr>,
{
if allow_local_scope {
filter_local_discovery_addrs(addresses)
} else {
filter_publicly_advertisable_addrs(addresses)
}
}
async fn register_announced_machine(
contact_store: &std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>,
own_agent_id: identity::AgentId,
announced_agent_id: identity::AgentId,
announced_machine_id: identity::MachineId,
) -> bool {
if announced_agent_id == own_agent_id {
return false;
}
let mut store = contact_store.write().await;
let record = contacts::MachineRecord::new(announced_machine_id, None);
store.add_machine(&announced_agent_id, record)
}
fn local_scoped_bootstrap_addr(addr: std::net::SocketAddr) -> bool {
if addr.port() == 0 {
return false;
}
match addr.ip() {
std::net::IpAddr::V4(v4) => {
v4.is_loopback() || v4.is_private() || is_cgnat_v4(v4) || v4.is_link_local()
}
std::net::IpAddr::V6(v6) => {
let segs = v6.segments();
v6.is_loopback() || (segs[0] & 0xfe00) == 0xfc00 || (segs[0] & 0xffc0) == 0xfe80
}
}
}
fn allow_local_discovery_addresses(config: &network::NetworkConfig) -> bool {
config.bootstrap_nodes.is_empty()
|| config
.bootstrap_nodes
.iter()
.copied()
.all(local_scoped_bootstrap_addr)
}
pub fn collect_local_interface_addrs(port: u16) -> Vec<std::net::SocketAddr> {
fn is_cgnat(v4: std::net::Ipv4Addr) -> bool {
v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64
}
fn addr_priority(ip: std::net::IpAddr) -> u8 {
match ip {
std::net::IpAddr::V4(v4) => {
if is_globally_routable(std::net::IpAddr::V4(v4)) {
0
} else if is_cgnat(v4) {
1
} else {
2
}
}
std::net::IpAddr::V6(v6) => {
if is_globally_routable(std::net::IpAddr::V6(v6)) {
3
} else {
4
}
}
}
}
let mut ranked = Vec::new();
let interfaces = match if_addrs::get_if_addrs() {
Ok(interfaces) => interfaces,
Err(_) => return Vec::new(),
};
for iface in interfaces {
let ip = iface.ip();
if ip.is_unspecified() || ip.is_loopback() {
continue;
}
let addr = match ip {
std::net::IpAddr::V4(v4) => {
if v4.is_link_local() {
continue;
}
std::net::SocketAddr::new(std::net::IpAddr::V4(v4), port)
}
std::net::IpAddr::V6(v6) => {
let segs = v6.segments();
let is_link_local = (segs[0] & 0xffc0) == 0xfe80;
if is_link_local {
continue;
}
std::net::SocketAddr::new(std::net::IpAddr::V6(v6), port)
}
};
if !ranked.iter().any(|(_, existing)| *existing == addr) {
ranked.push((addr_priority(addr.ip()), addr));
}
}
ranked.sort_by_key(|(priority, addr)| (*priority, addr.is_ipv6()));
ranked.into_iter().map(|(_, addr)| addr).collect()
}
fn is_cgnat_v4(v4: std::net::Ipv4Addr) -> bool {
v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64
}
fn same_v4_24(a: std::net::Ipv4Addr, b: std::net::Ipv4Addr) -> bool {
let a = a.octets();
let b = b.octets();
a[0] == b[0] && a[1] == b[1] && a[2] == b[2]
}
fn local_direct_probe_priority(
addr: std::net::SocketAddr,
local_v4s: &[std::net::Ipv4Addr],
) -> Option<u8> {
let std::net::IpAddr::V4(v4) = addr.ip() else {
return None;
};
if addr.port() == 0 || v4.is_loopback() || v4.is_link_local() || v4.is_unspecified() {
return None;
}
if local_v4s.iter().any(|local| same_v4_24(*local, v4)) {
return Some(0);
}
if v4.is_private() {
return Some(1);
}
if is_cgnat_v4(v4) {
return Some(2);
}
None
}
fn local_direct_probe_addrs_with_local_v4s(
addresses: &[std::net::SocketAddr],
local_v4s: &[std::net::Ipv4Addr],
) -> Vec<std::net::SocketAddr> {
let mut ranked = addresses
.iter()
.copied()
.filter_map(|addr| local_direct_probe_priority(addr, local_v4s).map(|rank| (rank, addr)))
.collect::<Vec<_>>();
ranked.sort_by_key(|(rank, addr)| (*rank, *addr));
ranked.dedup_by_key(|(_, addr)| *addr);
ranked.into_iter().map(|(_, addr)| addr).collect()
}
fn local_direct_probe_addrs(addresses: &[std::net::SocketAddr]) -> Vec<std::net::SocketAddr> {
let local_v4s = collect_local_interface_addrs(0)
.into_iter()
.filter_map(|addr| match addr.ip() {
std::net::IpAddr::V4(v4) => Some(v4),
std::net::IpAddr::V6(_) => None,
})
.collect::<Vec<_>>();
local_direct_probe_addrs_with_local_v4s(addresses, &local_v4s)
}
pub const IDENTITY_HEARTBEAT_INTERVAL_SECS: u64 = 300;
pub const IDENTITY_TTL_SECS: u64 = 900;
const DISCOVERY_REBROADCAST_STATE_CAP: usize = 1024;
const DISCOVERY_REBROADCAST_STATE_TTL: std::time::Duration = std::time::Duration::from_secs(3600);
const DISCOVERY_CACHE_REAPER_INTERVAL_SECS: u64 = 120;
fn discovery_record_is_live(_announced_at: u64, last_seen: u64, cutoff: u64) -> bool {
last_seen >= cutoff
}
fn should_rebroadcast_discovery_once<K>(
state: &mut std::collections::HashMap<K, std::time::Instant>,
key: K,
now: std::time::Instant,
) -> bool
where
K: Eq + std::hash::Hash,
{
match state.entry(key) {
std::collections::hash_map::Entry::Occupied(_) => false,
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(now);
if state.len() > DISCOVERY_REBROADCAST_STATE_CAP {
if let Some(cutoff) = now.checked_sub(DISCOVERY_REBROADCAST_STATE_TTL) {
state.retain(|_, seen_at| *seen_at >= cutoff);
}
}
true
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct IdentityAnnouncementUnsigned {
agent_id: identity::AgentId,
machine_id: identity::MachineId,
user_id: Option<identity::UserId>,
agent_certificate: Option<identity::AgentCertificate>,
machine_public_key: Vec<u8>,
addresses: Vec<std::net::SocketAddr>,
announced_at: u64,
nat_type: Option<String>,
can_receive_direct: Option<bool>,
is_relay: Option<bool>,
is_coordinator: Option<bool>,
reachable_via: Vec<identity::MachineId>,
relay_candidates: Vec<identity::MachineId>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct IdentityAnnouncement {
pub agent_id: identity::AgentId,
pub machine_id: identity::MachineId,
pub user_id: Option<identity::UserId>,
pub agent_certificate: Option<identity::AgentCertificate>,
pub machine_public_key: Vec<u8>,
pub machine_signature: Vec<u8>,
pub addresses: Vec<std::net::SocketAddr>,
pub announced_at: u64,
pub nat_type: Option<String>,
pub can_receive_direct: Option<bool>,
pub is_relay: Option<bool>,
pub is_coordinator: Option<bool>,
pub reachable_via: Vec<identity::MachineId>,
pub relay_candidates: Vec<identity::MachineId>,
pub agent_public_key: Vec<u8>,
}
impl IdentityAnnouncement {
fn to_unsigned(&self) -> IdentityAnnouncementUnsigned {
IdentityAnnouncementUnsigned {
agent_id: self.agent_id,
machine_id: self.machine_id,
user_id: self.user_id,
agent_certificate: self.agent_certificate.clone(),
machine_public_key: self.machine_public_key.clone(),
addresses: self.addresses.clone(),
announced_at: self.announced_at,
nat_type: self.nat_type.clone(),
can_receive_direct: self.can_receive_direct,
is_relay: self.is_relay,
is_coordinator: self.is_coordinator,
reachable_via: self.reachable_via.clone(),
relay_candidates: self.relay_candidates.clone(),
}
}
pub fn verify(&self) -> error::Result<()> {
let machine_pub =
ant_quic::MlDsaPublicKey::from_bytes(&self.machine_public_key).map_err(|_| {
error::IdentityError::CertificateVerification(
"invalid machine public key in announcement".to_string(),
)
})?;
let derived_machine_id = identity::MachineId::from_public_key(&machine_pub);
if derived_machine_id != self.machine_id {
return Err(error::IdentityError::CertificateVerification(
"machine_id does not match machine public key".to_string(),
));
}
let unsigned_bytes = bincode::serialize(&self.to_unsigned()).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize announcement for verification: {e}"
))
})?;
let signature = ant_quic::crypto::raw_public_keys::pqc::MlDsaSignature::from_bytes(
&self.machine_signature,
)
.map_err(|e| {
error::IdentityError::CertificateVerification(format!(
"invalid machine signature in announcement: {:?}",
e
))
})?;
ant_quic::crypto::raw_public_keys::pqc::verify_with_ml_dsa(
&machine_pub,
&unsigned_bytes,
&signature,
)
.map_err(|e| {
error::IdentityError::CertificateVerification(format!(
"machine signature verification failed: {:?}",
e
))
})?;
match (self.user_id, self.agent_certificate.as_ref()) {
(Some(user_id), Some(cert)) => {
cert.verify()?;
let cert_agent_id = cert.agent_id()?;
if cert_agent_id != self.agent_id {
return Err(error::IdentityError::CertificateVerification(
"agent certificate agent_id mismatch".to_string(),
));
}
let cert_user_id = cert.user_id()?;
if cert_user_id != user_id {
return Err(error::IdentityError::CertificateVerification(
"agent certificate user_id mismatch".to_string(),
));
}
Ok(())
}
(None, None) => Ok(()),
_ => Err(error::IdentityError::CertificateVerification(
"user identity disclosure requires matching certificate".to_string(),
)),
}
}
}
const IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS: u64 = dm::CLOCK_SKEW_TOLERANCE_MS / 1_000;
fn identity_announcement_timestamp_is_acceptable(announced_at: u64, now: u64) -> bool {
announced_at <= now.saturating_add(IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS)
}
fn identity_announcement_has_direct_agent_origin(
msg: &gossip::PubSubMessage,
announcement: &IdentityAnnouncement,
) -> bool {
if !msg.verified || msg.sender != Some(announcement.agent_id) {
return false;
}
let Some(sender_public_key) = msg.sender_public_key.as_deref() else {
return false;
};
let Ok(sender_public_key) = ant_quic::MlDsaPublicKey::from_bytes(sender_public_key) else {
return false;
};
identity::AgentId::from_public_key(&sender_public_key) == announcement.agent_id
}
async fn record_authenticated_machine_binding_from_message(
bindings: &dm_inbox::AuthenticatedMachineBindings,
msg: &gossip::PubSubMessage,
announcement: &IdentityAnnouncement,
now: u64,
) -> bool {
if !identity_announcement_timestamp_is_acceptable(announcement.announced_at, now) {
tracing::warn!(
agent = %hex::encode(announcement.agent_id.as_bytes()),
announced_at = announcement.announced_at,
now,
max_future_skew_secs = IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS,
"ignoring far-future identity announcement for authenticated machine binding"
);
return false;
}
if !identity_announcement_has_direct_agent_origin(msg, announcement) {
tracing::debug!(
agent = %hex::encode(announcement.agent_id.as_bytes()),
sender = ?msg.sender.map(|id| hex::encode(id.as_bytes())),
"identity announcement is not direct-origin authenticated; retained binding unchanged"
);
return false;
}
dm_inbox::record_authenticated_machine_binding(
bindings,
announcement.agent_id,
announcement.machine_id,
announcement.announced_at,
)
.await;
true
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct MachineAnnouncementUnsigned {
machine_id: identity::MachineId,
machine_public_key: Vec<u8>,
addresses: Vec<std::net::SocketAddr>,
announced_at: u64,
nat_type: Option<String>,
can_receive_direct: Option<bool>,
is_relay: Option<bool>,
is_coordinator: Option<bool>,
reachable_via: Vec<identity::MachineId>,
relay_candidates: Vec<identity::MachineId>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MachineAnnouncement {
pub machine_id: identity::MachineId,
pub machine_public_key: Vec<u8>,
pub machine_signature: Vec<u8>,
pub addresses: Vec<std::net::SocketAddr>,
pub announced_at: u64,
pub nat_type: Option<String>,
pub can_receive_direct: Option<bool>,
pub is_relay: Option<bool>,
pub is_coordinator: Option<bool>,
pub reachable_via: Vec<identity::MachineId>,
pub relay_candidates: Vec<identity::MachineId>,
}
impl MachineAnnouncement {
fn to_unsigned(&self) -> MachineAnnouncementUnsigned {
MachineAnnouncementUnsigned {
machine_id: self.machine_id,
machine_public_key: self.machine_public_key.clone(),
addresses: self.addresses.clone(),
announced_at: self.announced_at,
nat_type: self.nat_type.clone(),
can_receive_direct: self.can_receive_direct,
is_relay: self.is_relay,
is_coordinator: self.is_coordinator,
reachable_via: self.reachable_via.clone(),
relay_candidates: self.relay_candidates.clone(),
}
}
pub fn verify(&self) -> error::Result<()> {
let machine_pub =
ant_quic::MlDsaPublicKey::from_bytes(&self.machine_public_key).map_err(|_| {
error::IdentityError::CertificateVerification(
"invalid machine public key in machine announcement".to_string(),
)
})?;
let derived_machine_id = identity::MachineId::from_public_key(&machine_pub);
if derived_machine_id != self.machine_id {
return Err(error::IdentityError::CertificateVerification(
"machine_id does not match machine public key".to_string(),
));
}
let unsigned_bytes = bincode::serialize(&self.to_unsigned()).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize machine announcement for verification: {e}"
))
})?;
let signature = ant_quic::crypto::raw_public_keys::pqc::MlDsaSignature::from_bytes(
&self.machine_signature,
)
.map_err(|e| {
error::IdentityError::CertificateVerification(format!(
"invalid machine signature in machine announcement: {:?}",
e
))
})?;
ant_quic::crypto::raw_public_keys::pqc::verify_with_ml_dsa(
&machine_pub,
&unsigned_bytes,
&signature,
)
.map_err(|e| {
error::IdentityError::CertificateVerification(format!(
"machine announcement signature verification failed: {:?}",
e
))
})
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct UserAnnouncementUnsigned {
user_id: identity::UserId,
user_public_key: Vec<u8>,
agent_certificates: Vec<identity::AgentCertificate>,
announced_at: u64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UserAnnouncement {
pub user_id: identity::UserId,
pub user_public_key: Vec<u8>,
pub user_signature: Vec<u8>,
pub agent_certificates: Vec<identity::AgentCertificate>,
pub announced_at: u64,
}
impl UserAnnouncement {
fn to_unsigned(&self) -> UserAnnouncementUnsigned {
UserAnnouncementUnsigned {
user_id: self.user_id,
user_public_key: self.user_public_key.clone(),
agent_certificates: self.agent_certificates.clone(),
announced_at: self.announced_at,
}
}
pub fn sign(
user_kp: &identity::UserKeypair,
agent_certificates: Vec<identity::AgentCertificate>,
announced_at: u64,
) -> error::Result<Self> {
let user_id = user_kp.user_id();
for cert in &agent_certificates {
let cert_user = cert.user_id()?;
if cert_user != user_id {
return Err(error::IdentityError::CertificateVerification(
"user announcement contains certificate issued by a different user".to_string(),
));
}
}
let user_public_key = user_kp.public_key().as_bytes().to_vec();
let unsigned = UserAnnouncementUnsigned {
user_id,
user_public_key: user_public_key.clone(),
agent_certificates: agent_certificates.clone(),
announced_at,
};
let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize unsigned user announcement: {e}"
))
})?;
let user_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
user_kp.secret_key(),
&unsigned_bytes,
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to sign user announcement with user key: {e:?}"
)))
})?
.as_bytes()
.to_vec();
Ok(Self {
user_id,
user_public_key,
user_signature,
agent_certificates,
announced_at,
})
}
pub fn verify(&self) -> error::Result<()> {
let user_pub =
ant_quic::MlDsaPublicKey::from_bytes(&self.user_public_key).map_err(|_| {
error::IdentityError::CertificateVerification(
"invalid user public key in user announcement".to_string(),
)
})?;
let derived_user_id = identity::UserId::from_public_key(&user_pub);
if derived_user_id != self.user_id {
return Err(error::IdentityError::CertificateVerification(
"user_id does not match user public key in user announcement".to_string(),
));
}
let unsigned_bytes = bincode::serialize(&self.to_unsigned()).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize user announcement for verification: {e}"
))
})?;
let signature = ant_quic::crypto::raw_public_keys::pqc::MlDsaSignature::from_bytes(
&self.user_signature,
)
.map_err(|e| {
error::IdentityError::CertificateVerification(format!(
"invalid user signature in user announcement: {e:?}"
))
})?;
ant_quic::crypto::raw_public_keys::pqc::verify_with_ml_dsa(
&user_pub,
&unsigned_bytes,
&signature,
)
.map_err(|e| {
error::IdentityError::CertificateVerification(format!(
"user announcement signature verification failed: {e:?}"
))
})?;
for cert in &self.agent_certificates {
cert.verify()?;
let cert_user = cert.user_id()?;
if cert_user != self.user_id {
return Err(error::IdentityError::CertificateVerification(
"user announcement certificate user_id mismatch".to_string(),
));
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct DiscoveredUser {
pub user_id: identity::UserId,
pub user_public_key: Vec<u8>,
pub agent_certificates: Vec<identity::AgentCertificate>,
pub agent_ids: Vec<identity::AgentId>,
pub announced_at: u64,
pub last_seen: u64,
}
impl DiscoveredUser {
fn from_announcement(announcement: &UserAnnouncement, last_seen: u64) -> Self {
let agent_ids: Vec<identity::AgentId> = announcement
.agent_certificates
.iter()
.filter_map(|c| c.agent_id().ok())
.collect();
Self {
user_id: announcement.user_id,
user_public_key: announcement.user_public_key.clone(),
agent_certificates: announcement.agent_certificates.clone(),
agent_ids,
announced_at: announcement.announced_at,
last_seen,
}
}
}
#[derive(Debug, Clone)]
pub struct DiscoveredAgent {
pub agent_id: identity::AgentId,
pub machine_id: identity::MachineId,
pub user_id: Option<identity::UserId>,
pub addresses: Vec<std::net::SocketAddr>,
pub announced_at: u64,
pub last_seen: u64,
#[doc(hidden)]
pub machine_public_key: Vec<u8>,
pub nat_type: Option<String>,
pub can_receive_direct: Option<bool>,
pub is_relay: Option<bool>,
pub is_coordinator: Option<bool>,
pub reachable_via: Vec<identity::MachineId>,
pub relay_candidates: Vec<identity::MachineId>,
pub cert_not_after: Option<u64>,
pub agent_certificate: Option<identity::AgentCertificate>,
pub agent_public_key: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct DiscoveredMachine {
pub machine_id: identity::MachineId,
pub addresses: Vec<std::net::SocketAddr>,
pub announced_at: u64,
pub last_seen: u64,
pub machine_public_key: Vec<u8>,
pub nat_type: Option<String>,
pub can_receive_direct: Option<bool>,
pub is_relay: Option<bool>,
pub is_coordinator: Option<bool>,
pub reachable_via: Vec<identity::MachineId>,
pub relay_candidates: Vec<identity::MachineId>,
pub agent_ids: Vec<identity::AgentId>,
pub user_ids: Vec<identity::UserId>,
}
fn collect_subject_certs(
cache: &std::collections::HashMap<identity::AgentId, DiscoveredAgent>,
) -> std::collections::HashMap<identity::AgentId, identity::AgentCertificate> {
cache
.values()
.filter_map(|a| {
a.agent_certificate
.as_ref()
.map(|c| (a.agent_id, c.clone()))
})
.collect()
}
impl DiscoveredMachine {
fn from_machine_announcement(
announcement: &MachineAnnouncement,
addresses: Vec<std::net::SocketAddr>,
last_seen: u64,
) -> Self {
Self {
machine_id: announcement.machine_id,
addresses,
announced_at: announcement.announced_at,
last_seen,
machine_public_key: announcement.machine_public_key.clone(),
nat_type: announcement.nat_type.clone(),
can_receive_direct: announcement.can_receive_direct,
is_relay: announcement.is_relay,
is_coordinator: announcement.is_coordinator,
reachable_via: announcement.reachable_via.clone(),
relay_candidates: announcement.relay_candidates.clone(),
agent_ids: Vec::new(),
user_ids: Vec::new(),
}
}
fn from_discovered_agent(agent: &DiscoveredAgent) -> Self {
Self {
machine_id: agent.machine_id,
addresses: agent.addresses.clone(),
announced_at: agent.announced_at,
last_seen: agent.last_seen,
machine_public_key: agent.machine_public_key.clone(),
nat_type: agent.nat_type.clone(),
can_receive_direct: agent.can_receive_direct,
is_relay: agent.is_relay,
is_coordinator: agent.is_coordinator,
reachable_via: agent.reachable_via.clone(),
relay_candidates: agent.relay_candidates.clone(),
agent_ids: vec![agent.agent_id],
user_ids: agent.user_id.into_iter().collect(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct AnnouncementAssistSnapshot {
nat_type: Option<String>,
can_receive_direct: Option<bool>,
relay_capable: Option<bool>,
coordinator_capable: Option<bool>,
relay_active: Option<bool>,
coordinator_active: Option<bool>,
}
impl AnnouncementAssistSnapshot {
fn from_node_status(status: &ant_quic::NodeStatus) -> Self {
Self {
nat_type: Some(status.nat_type.to_string()),
can_receive_direct: Some(status.can_receive_direct),
relay_capable: Some(status.relay_service_enabled),
coordinator_capable: Some(status.coordinator_service_enabled),
relay_active: Some(status.is_relaying),
coordinator_active: Some(status.is_coordinating),
}
}
}
struct IdentityAnnouncementBuildOptions<'a> {
include_user_identity: bool,
human_consent: bool,
addresses: Vec<std::net::SocketAddr>,
assist_snapshot: Option<&'a AnnouncementAssistSnapshot>,
reachable_via: Vec<identity::MachineId>,
relay_candidates: Vec<identity::MachineId>,
allow_local_scope: bool,
}
fn push_unique<T: Copy + PartialEq>(items: &mut Vec<T>, item: T) {
if !items.contains(&item) {
items.push(item);
}
}
fn prioritize_discovery_addresses(addresses: &mut [std::net::SocketAddr]) {
addresses.sort_by_key(|addr| is_publicly_advertisable(*addr));
}
async fn upsert_discovered_agent(
cache: &std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
>,
mut incoming: DiscoveredAgent,
) {
prioritize_discovery_addresses(&mut incoming.addresses);
let mut cache = cache.write().await;
match cache.get_mut(&incoming.agent_id) {
Some(existing) => {
if incoming.announced_at >= existing.announced_at {
existing.announced_at = incoming.announced_at;
existing.addresses = incoming.addresses;
prioritize_discovery_addresses(&mut existing.addresses);
if incoming.machine_id.0 != [0u8; 32] {
existing.machine_id = incoming.machine_id;
}
if incoming.user_id.is_some() || existing.user_id.is_none() {
existing.user_id = incoming.user_id;
}
if !incoming.machine_public_key.is_empty() {
existing.machine_public_key = incoming.machine_public_key;
}
if incoming.nat_type.is_some() {
existing.nat_type = incoming.nat_type;
}
if incoming.can_receive_direct.is_some() {
existing.can_receive_direct = incoming.can_receive_direct;
}
if incoming.is_relay.is_some() {
existing.is_relay = incoming.is_relay;
}
if incoming.is_coordinator.is_some() {
existing.is_coordinator = incoming.is_coordinator;
}
existing.reachable_via = incoming.reachable_via;
existing.relay_candidates = incoming.relay_candidates;
if !incoming.agent_public_key.is_empty() {
existing.agent_public_key = incoming.agent_public_key;
}
}
existing.last_seen = incoming.last_seen;
}
None => {
cache.insert(incoming.agent_id, incoming);
}
}
}
fn sort_discovered_machine(machine: &mut DiscoveredMachine) {
machine.addresses.sort_by_key(|addr| addr.to_string());
machine.agent_ids.sort_by_key(|id| id.0);
machine.user_ids.sort_by_key(|id| id.0);
machine.reachable_via.sort_by_key(|id| id.0);
machine.relay_candidates.sort_by_key(|id| id.0);
}
async fn upsert_discovered_machine(
cache: &std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
>,
mut incoming: DiscoveredMachine,
) {
if incoming.machine_id.0 == [0u8; 32] {
return;
}
sort_discovered_machine(&mut incoming);
let mut cache = cache.write().await;
match cache.get_mut(&incoming.machine_id) {
Some(existing) => {
for addr in incoming.addresses {
if !existing.addresses.contains(&addr) {
existing.addresses.push(addr);
}
}
if incoming.announced_at >= existing.announced_at {
existing.announced_at = incoming.announced_at;
if !incoming.machine_public_key.is_empty() {
existing.machine_public_key = incoming.machine_public_key;
}
if incoming.nat_type.is_some() {
existing.nat_type = incoming.nat_type;
}
if incoming.can_receive_direct.is_some() {
existing.can_receive_direct = incoming.can_receive_direct;
}
if incoming.is_relay.is_some() {
existing.is_relay = incoming.is_relay;
}
if incoming.is_coordinator.is_some() {
existing.is_coordinator = incoming.is_coordinator;
}
existing.reachable_via = incoming.reachable_via;
existing.relay_candidates = incoming.relay_candidates;
}
existing.last_seen = existing.last_seen.max(incoming.last_seen);
for agent_id in incoming.agent_ids {
push_unique(&mut existing.agent_ids, agent_id);
}
for user_id in incoming.user_ids {
push_unique(&mut existing.user_ids, user_id);
}
sort_discovered_machine(existing);
}
None => {
cache.insert(incoming.machine_id, incoming);
}
}
}
async fn upsert_discovered_machine_from_agent(
cache: &std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
>,
agent: &DiscoveredAgent,
) {
if agent.machine_id.0 != [0u8; 32] {
upsert_discovered_machine(cache, DiscoveredMachine::from_discovered_agent(agent)).await;
}
}
const MAX_MACHINE_ANNOUNCEMENT_DECODE_BYTES: u64 = 64 * 1024;
const IDENTITY_ANNOUNCEMENT_V2_MAGIC: &[u8; 4] = b"X0A2";
fn serialize_identity_announcement(
announcement: &IdentityAnnouncement,
) -> Result<Vec<u8>, Box<bincode::ErrorKind>> {
use bincode::Options;
let body = bincode::DefaultOptions::new()
.with_fixint_encoding()
.serialize(announcement)?;
let mut out = Vec::with_capacity(IDENTITY_ANNOUNCEMENT_V2_MAGIC.len() + body.len());
out.extend_from_slice(IDENTITY_ANNOUNCEMENT_V2_MAGIC);
out.extend_from_slice(&body);
Ok(out)
}
#[derive(serde::Serialize, serde::Deserialize)]
struct IdentityAnnouncementLegacy {
agent_id: identity::AgentId,
machine_id: identity::MachineId,
user_id: Option<identity::UserId>,
agent_certificate: Option<identity::AgentCertificate>,
machine_public_key: Vec<u8>,
machine_signature: Vec<u8>,
addresses: Vec<std::net::SocketAddr>,
announced_at: u64,
nat_type: Option<String>,
can_receive_direct: Option<bool>,
is_relay: Option<bool>,
is_coordinator: Option<bool>,
reachable_via: Vec<identity::MachineId>,
relay_candidates: Vec<identity::MachineId>,
}
impl IdentityAnnouncementLegacy {
fn into_announcement(self) -> IdentityAnnouncement {
let agent_public_key = self
.agent_certificate
.as_ref()
.map(|c| c.agent_public_key().to_vec())
.unwrap_or_default();
IdentityAnnouncement {
agent_id: self.agent_id,
machine_id: self.machine_id,
user_id: self.user_id,
agent_certificate: self.agent_certificate,
machine_public_key: self.machine_public_key,
machine_signature: self.machine_signature,
addresses: self.addresses,
announced_at: self.announced_at,
nat_type: self.nat_type,
can_receive_direct: self.can_receive_direct,
is_relay: self.is_relay,
is_coordinator: self.is_coordinator,
reachable_via: self.reachable_via,
relay_candidates: self.relay_candidates,
agent_public_key,
}
}
}
fn deserialize_identity_announcement(
payload: &[u8],
) -> std::result::Result<IdentityAnnouncement, Box<bincode::ErrorKind>> {
use bincode::Options;
let opts = || {
bincode::DefaultOptions::new()
.with_fixint_encoding()
.with_limit(crate::network::MAX_MESSAGE_DESERIALIZE_SIZE)
.reject_trailing_bytes()
};
if payload.len() >= IDENTITY_ANNOUNCEMENT_V2_MAGIC.len()
&& &payload[..IDENTITY_ANNOUNCEMENT_V2_MAGIC.len()] == IDENTITY_ANNOUNCEMENT_V2_MAGIC
{
return opts().deserialize(&payload[IDENTITY_ANNOUNCEMENT_V2_MAGIC.len()..]);
}
let legacy: IdentityAnnouncementLegacy = opts().deserialize(payload)?;
Ok(legacy.into_announcement())
}
fn deserialize_user_announcement(
payload: &[u8],
) -> std::result::Result<UserAnnouncement, Box<bincode::ErrorKind>> {
use bincode::Options;
bincode::DefaultOptions::new()
.with_fixint_encoding()
.with_limit(crate::network::MAX_MESSAGE_DESERIALIZE_SIZE)
.reject_trailing_bytes()
.deserialize(payload)
}
fn deserialize_machine_announcement(
payload: &[u8],
) -> std::result::Result<MachineAnnouncement, Box<bincode::ErrorKind>> {
use bincode::Options;
bincode::DefaultOptions::new()
.with_fixint_encoding()
.with_limit(MAX_MACHINE_ANNOUNCEMENT_DECODE_BYTES)
.reject_trailing_bytes()
.deserialize(payload)
}
async fn collect_coordinator_hints(
network: &network::NetworkNode,
machine_cache: &std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
>,
own_machine_id: identity::MachineId,
) -> (Vec<identity::MachineId>, Vec<identity::MachineId>) {
const MAX_COORDINATOR_HINTS: usize = 8;
let connected = network.connected_peers().await;
if connected.is_empty() {
return (Vec::new(), Vec::new());
}
let cache = machine_cache.read().await;
let mut reachable_via: Vec<identity::MachineId> = Vec::new();
let mut relay_candidates: Vec<identity::MachineId> = Vec::new();
for peer_id in connected {
let mid = identity::MachineId(peer_id.0);
if mid == own_machine_id {
continue;
}
let Some(entry) = cache.get(&mid) else {
continue;
};
if entry.is_coordinator == Some(true)
&& !reachable_via.contains(&mid)
&& reachable_via.len() < MAX_COORDINATOR_HINTS
{
reachable_via.push(mid);
}
if entry.is_relay == Some(true)
&& !relay_candidates.contains(&mid)
&& relay_candidates.len() < MAX_COORDINATOR_HINTS
{
relay_candidates.push(mid);
}
}
reachable_via.sort_by_key(|id| id.0);
relay_candidates.sort_by_key(|id| id.0);
(reachable_via, relay_candidates)
}
fn build_machine_announcement_for_identity(
identity: &identity::Identity,
addresses: Vec<std::net::SocketAddr>,
announced_at: u64,
assist_snapshot: Option<&AnnouncementAssistSnapshot>,
reachable_via: Vec<identity::MachineId>,
relay_candidates: Vec<identity::MachineId>,
allow_local_scope: bool,
) -> error::Result<MachineAnnouncement> {
let addresses = filter_discovery_announcement_addrs(addresses, allow_local_scope);
let machine_public_key = identity.machine_keypair().public_key().as_bytes().to_vec();
let unsigned = MachineAnnouncementUnsigned {
machine_id: identity.machine_id(),
machine_public_key: machine_public_key.clone(),
addresses,
announced_at,
nat_type: assist_snapshot.and_then(|snapshot| snapshot.nat_type.clone()),
can_receive_direct: assist_snapshot.and_then(|snapshot| snapshot.can_receive_direct),
is_relay: assist_snapshot.and_then(|snapshot| snapshot.relay_capable),
is_coordinator: assist_snapshot.and_then(|snapshot| snapshot.coordinator_capable),
reachable_via,
relay_candidates,
};
let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize unsigned machine announcement: {e}"
))
})?;
let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
identity.machine_keypair().secret_key(),
&unsigned_bytes,
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to sign machine announcement with machine key: {:?}",
e
)))
})?
.as_bytes()
.to_vec();
Ok(MachineAnnouncement {
machine_id: unsigned.machine_id,
machine_public_key,
machine_signature,
addresses: unsigned.addresses,
announced_at: unsigned.announced_at,
nat_type: unsigned.nat_type,
can_receive_direct: unsigned.can_receive_direct,
is_relay: unsigned.is_relay,
is_coordinator: unsigned.is_coordinator,
reachable_via: unsigned.reachable_via,
relay_candidates: unsigned.relay_candidates,
})
}
#[derive(Debug)]
pub struct AgentBuilder {
machine_key_path: Option<std::path::PathBuf>,
agent_keypair: Option<identity::AgentKeypair>,
agent_key_path: Option<std::path::PathBuf>,
agent_cert_path: Option<std::path::PathBuf>,
user_keypair: Option<identity::UserKeypair>,
user_key_path: Option<std::path::PathBuf>,
#[allow(dead_code)]
network_config: Option<network::NetworkConfig>,
gossip_config: Option<gossip::GossipConfig>,
peer_cache_dir: Option<std::path::PathBuf>,
disable_peer_cache: bool,
heartbeat_interval_secs: Option<u64>,
identity_ttl_secs: Option<u64>,
presence_beacon_interval_secs: Option<u64>,
presence_event_poll_interval_secs: Option<u64>,
presence_offline_timeout_secs: Option<u64>,
contact_store_path: Option<std::path::PathBuf>,
identity_dir: Option<std::path::PathBuf>,
}
struct HeartbeatContext {
identity: std::sync::Arc<identity::Identity>,
runtime: std::sync::Arc<gossip::GossipRuntime>,
network: std::sync::Arc<network::NetworkNode>,
interval_secs: u64,
cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
>,
machine_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
>,
user_identity_consented: std::sync::Arc<std::sync::atomic::AtomicBool>,
allow_local_discovery_addrs: bool,
revocation_set: std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>>,
}
impl HeartbeatContext {
async fn announce(&self) -> error::Result<()> {
let machine_public_key = self
.identity
.machine_keypair()
.public_key()
.as_bytes()
.to_vec();
let announced_at = Agent::unix_timestamp_secs();
let mut addresses = match self.network.node_status().await {
Some(status) if !status.external_addrs.is_empty() => status.external_addrs,
_ => match self.network.routable_addr().await {
Some(addr) => vec![addr],
None => Vec::new(),
},
};
let bind_port = self
.network
.bound_addr()
.await
.map(|a| a.port())
.unwrap_or(5483);
if let Ok(sock) = std::net::UdpSocket::bind("[::]:0") {
if sock.connect("[2001:4860:4860::8888]:80").is_ok() {
if let Ok(local) = sock.local_addr() {
if let std::net::IpAddr::V6(v6) = local.ip() {
let segs = v6.segments();
let is_global = (segs[0] & 0xffc0) != 0xfe80
&& (segs[0] & 0xff00) != 0xfd00
&& !v6.is_loopback();
if is_global {
let v6_addr =
std::net::SocketAddr::new(std::net::IpAddr::V6(v6), bind_port);
if !addresses.contains(&v6_addr) {
addresses.push(v6_addr);
}
}
}
}
}
}
for addr in collect_local_interface_addrs(bind_port) {
if !addresses.contains(&addr) {
addresses.push(addr);
}
}
addresses =
filter_discovery_announcement_addrs(addresses, self.allow_local_discovery_addrs);
let assist_snapshot = self
.network
.node_status()
.await
.map(|status| AnnouncementAssistSnapshot::from_node_status(&status))
.unwrap_or_default();
let nat_type = assist_snapshot.nat_type.clone();
let can_receive_direct = assist_snapshot.can_receive_direct;
let relay_capable = assist_snapshot.relay_capable;
let coordinator_capable = assist_snapshot.coordinator_capable;
let (reachable_via, relay_candidates) = if can_receive_direct == Some(true) {
(Vec::new(), Vec::new())
} else {
collect_coordinator_hints(
self.network.as_ref(),
&self.machine_cache,
self.identity.machine_id(),
)
.await
};
let include_user = self
.user_identity_consented
.load(std::sync::atomic::Ordering::Acquire);
let (user_id, agent_certificate) = if include_user {
(
self.identity
.user_keypair()
.map(identity::UserKeypair::user_id),
self.identity.agent_certificate().cloned(),
)
} else {
(None, None)
};
let unsigned = IdentityAnnouncementUnsigned {
agent_id: self.identity.agent_id(),
machine_id: self.identity.machine_id(),
user_id,
agent_certificate,
machine_public_key: machine_public_key.clone(),
addresses,
announced_at,
nat_type: nat_type.clone(),
can_receive_direct,
is_relay: relay_capable,
is_coordinator: coordinator_capable,
reachable_via: reachable_via.clone(),
relay_candidates: relay_candidates.clone(),
};
let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
error::IdentityError::Serialization(format!(
"heartbeat: failed to serialize announcement: {e}"
))
})?;
let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
self.identity.machine_keypair().secret_key(),
&unsigned_bytes,
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"heartbeat: failed to sign announcement: {:?}",
e
)))
})?
.as_bytes()
.to_vec();
let agent_public_key = self
.identity
.agent_keypair()
.public_key()
.as_bytes()
.to_vec();
let announcement = IdentityAnnouncement {
agent_id: unsigned.agent_id,
machine_id: unsigned.machine_id,
user_id: unsigned.user_id,
agent_certificate: unsigned.agent_certificate,
machine_public_key: machine_public_key.clone(),
machine_signature,
addresses: unsigned.addresses,
announced_at,
nat_type,
can_receive_direct,
is_relay: relay_capable,
is_coordinator: coordinator_capable,
reachable_via: reachable_via.clone(),
relay_candidates: relay_candidates.clone(),
agent_public_key,
};
tracing::debug!(
target: "x0x::discovery",
announcement_kind = "heartbeat",
machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
addr_total = announcement.addresses.len(),
nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
can_receive_direct = ?announcement.can_receive_direct,
relay_capable = ?announcement.is_relay,
coordinator_capable = ?announcement.is_coordinator,
relay_active = ?assist_snapshot.relay_active,
coordinator_active = ?assist_snapshot.coordinator_active,
reachable_via_count = announcement.reachable_via.len(),
relay_candidate_count = announcement.relay_candidates.len(),
"publishing identity announcement"
);
let machine_announcement = build_machine_announcement_for_identity(
&self.identity,
announcement.addresses.clone(),
announced_at,
Some(&assist_snapshot),
reachable_via.clone(),
relay_candidates.clone(),
self.allow_local_discovery_addrs,
)?;
tracing::debug!(
target: "x0x::discovery",
announcement_kind = "machine_heartbeat",
machine_prefix = %network::hex_prefix(&machine_announcement.machine_id.0, 4),
addr_total = machine_announcement.addresses.len(),
nat_type = machine_announcement.nat_type.as_deref().unwrap_or("unknown"),
can_receive_direct = ?machine_announcement.can_receive_direct,
relay_capable = ?machine_announcement.is_relay,
coordinator_capable = ?machine_announcement.is_coordinator,
"publishing machine announcement"
);
let machine_encoded = bincode::serialize(&machine_announcement).map_err(|e| {
error::IdentityError::Serialization(format!(
"heartbeat: failed to serialize machine announcement: {e}"
))
})?;
let machine_payload = bytes::Bytes::from(machine_encoded);
self.runtime
.pubsub()
.publish(
shard_topic_for_machine(&machine_announcement.machine_id),
machine_payload.clone(),
)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"heartbeat: machine shard publish failed: {e}"
)))
})?;
self.runtime
.pubsub()
.publish(MACHINE_ANNOUNCE_TOPIC.to_string(), machine_payload)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"heartbeat: machine publish failed: {e}"
)))
})?;
let encoded = serialize_identity_announcement(&announcement).map_err(|e| {
error::IdentityError::Serialization(format!(
"heartbeat: failed to serialize announcement: {e}"
))
})?;
self.runtime
.pubsub()
.publish(
IDENTITY_ANNOUNCE_TOPIC.to_string(),
bytes::Bytes::from(encoded),
)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"heartbeat: publish failed: {e}"
)))
})?;
let now = Agent::unix_timestamp_secs();
upsert_discovered_machine(
&self.machine_cache,
DiscoveredMachine::from_machine_announcement(
&machine_announcement,
machine_announcement.addresses.clone(),
now,
),
)
.await;
let discovered_agent = DiscoveredAgent {
agent_id: announcement.agent_id,
machine_id: announcement.machine_id,
user_id: announcement.user_id,
addresses: announcement.addresses,
announced_at: announcement.announced_at,
last_seen: now,
machine_public_key: machine_public_key.clone(),
nat_type: announcement.nat_type.clone(),
can_receive_direct: announcement.can_receive_direct,
is_relay: announcement.is_relay,
is_coordinator: announcement.is_coordinator,
reachable_via: announcement.reachable_via.clone(),
relay_candidates: announcement.relay_candidates.clone(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: announcement.agent_public_key.clone(),
};
upsert_discovered_machine_from_agent(&self.machine_cache, &discovered_agent).await;
upsert_discovered_agent(&self.cache, discovered_agent).await;
let records = self.revocation_set.read().await.all_records();
if !records.is_empty() {
match bincode::serialize(&records) {
Ok(bytes) => {
if let Err(e) = self
.runtime
.pubsub()
.publish(REVOCATION_TOPIC.to_string(), bytes::Bytes::from(bytes))
.await
{
tracing::debug!("heartbeat: revocation re-broadcast failed: {e}");
}
}
Err(e) => {
tracing::debug!("heartbeat: failed to serialize revocation set: {e}");
}
}
}
Ok(())
}
}
impl Agent {
pub async fn new() -> error::Result<Self> {
Agent::builder().build().await
}
pub fn builder() -> AgentBuilder {
AgentBuilder {
machine_key_path: None,
agent_keypair: None,
agent_key_path: None,
agent_cert_path: None,
user_keypair: None,
user_key_path: None,
network_config: None,
gossip_config: None,
peer_cache_dir: None,
disable_peer_cache: false,
heartbeat_interval_secs: None,
identity_ttl_secs: None,
presence_beacon_interval_secs: None,
presence_event_poll_interval_secs: None,
presence_offline_timeout_secs: None,
contact_store_path: None,
identity_dir: None,
}
}
#[inline]
#[must_use]
pub fn identity(&self) -> &identity::Identity {
&self.identity
}
#[inline]
#[must_use]
pub fn machine_id(&self) -> identity::MachineId {
self.identity.machine_id()
}
#[inline]
#[must_use]
pub fn agent_id(&self) -> identity::AgentId {
self.identity.agent_id()
}
#[inline]
#[must_use]
pub fn user_id(&self) -> Option<identity::UserId> {
self.identity.user_id()
}
#[inline]
#[must_use]
pub fn agent_certificate(&self) -> Option<&identity::AgentCertificate> {
self.identity.agent_certificate()
}
#[must_use]
pub fn network(&self) -> Option<&std::sync::Arc<network::NetworkNode>> {
self.network.as_ref()
}
pub fn gossip_cache_adapter(&self) -> Option<&saorsa_gossip_coordinator::GossipCacheAdapter> {
self.gossip_cache_adapter.as_ref()
}
#[must_use]
pub fn gossip_stats(&self) -> Option<gossip::PubSubStatsSnapshot> {
self.gossip_runtime.as_ref().map(|rt| rt.pubsub().stats())
}
#[must_use]
pub fn gossip_dispatch_stats(&self) -> Option<gossip::GossipDispatchStatsSnapshot> {
self.gossip_runtime.as_ref().map(|rt| rt.dispatch_stats())
}
#[must_use]
pub fn gossip_pubsub_stage_stats(&self) -> Option<gossip::PubSubStageStatsSnapshot> {
self.gossip_runtime
.as_ref()
.map(|rt| rt.pubsub().stage_stats())
}
#[must_use]
pub fn recv_pump_diagnostics(&self) -> Option<network::RecvPumpDiagnosticsSnapshot> {
self.network.as_ref().map(|net| net.recv_pump_diagnostics())
}
#[must_use]
pub fn presence_system(&self) -> Option<&std::sync::Arc<presence::PresenceWrapper>> {
self.presence.as_ref()
}
#[must_use]
pub fn contacts(&self) -> &std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>> {
&self.contact_store
}
pub async fn reachability(
&self,
agent_id: &identity::AgentId,
) -> Option<connectivity::ReachabilityInfo> {
let cache = self.identity_discovery_cache.read().await;
cache
.get(agent_id)
.map(connectivity::ReachabilityInfo::from_discovered)
}
async fn seed_transport_peer_hints_for_target(
&self,
network: &network::NetworkNode,
target: &DiscoveredAgent,
) -> error::Result<()> {
#[derive(Default)]
struct HelperHintEntry {
addrs: Vec<std::net::SocketAddr>,
caps: ant_quic::bootstrap_cache::PeerCapabilities,
sources: std::collections::BTreeSet<&'static str>,
}
fn merge_helper_hint(
hints: &mut std::collections::HashMap<ant_quic::PeerId, HelperHintEntry>,
peer_id: ant_quic::PeerId,
source: &'static str,
addrs: impl IntoIterator<Item = std::net::SocketAddr>,
supports_coordination: bool,
supports_relay: bool,
) {
let entry = hints.entry(peer_id).or_default();
entry.sources.insert(source);
for addr in addrs {
if !entry.addrs.contains(&addr) {
entry.addrs.push(addr);
}
}
if supports_coordination {
entry.caps.supports_coordination = true;
}
if supports_relay {
entry.caps.supports_relay = true;
}
}
let target_agent_prefix = network::hex_prefix(&target.agent_id.0, 4);
let target_machine_prefix = network::hex_prefix(&target.machine_id.0, 4);
let target_peer_id = ant_quic::PeerId(target.machine_id.0);
if target.machine_id.0 != [0u8; 32] {
network
.upsert_peer_hints(target_peer_id, target.addresses.clone(), None)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to upsert target peer hints: {e}"
)))
})?;
tracing::debug!(
target: "x0x::connect",
stage = "seed_target_hints",
%target_agent_prefix,
%target_machine_prefix,
target_addr_count = target.addresses.len(),
"upserted direct target hints"
);
}
let mut helper_hints: std::collections::HashMap<ant_quic::PeerId, HelperHintEntry> =
std::collections::HashMap::new();
if let Some(ref cache) = self.bootstrap_cache {
for peer in cache.select_coordinators(6).await {
merge_helper_hint(
&mut helper_hints,
peer.peer_id,
"bootstrap_cache:coordinator",
peer.preferred_addresses(),
true,
false,
);
}
for peer in cache.select_relay_peers(6).await {
merge_helper_hint(
&mut helper_hints,
peer.peer_id,
"bootstrap_cache:relay",
peer.preferred_addresses(),
false,
true,
);
}
}
if let Some(ref adapter) = self.gossip_cache_adapter {
let mut adverts = adapter.get_all_adverts();
adverts.sort_by_key(|a| std::cmp::Reverse(a.score));
for advert in adverts.into_iter().take(12) {
let advert_peer_id = ant_quic::PeerId(*advert.peer.as_bytes());
if advert_peer_id == target_peer_id {
continue;
}
merge_helper_hint(
&mut helper_hints,
advert_peer_id,
"gossip_cache_advert",
advert
.addr_hints
.into_iter()
.map(|hint| hint.addr)
.filter(|addr| is_publicly_advertisable(*addr)),
advert.roles.coordinator || advert.roles.rendezvous,
advert.roles.relay,
);
}
}
let discovered: Vec<DiscoveredMachine> = {
let cache = self.machine_discovery_cache.read().await;
cache.values().cloned().collect()
};
for candidate in discovered {
if candidate.machine_id == target.machine_id || candidate.machine_id.0 == [0u8; 32] {
continue;
}
merge_helper_hint(
&mut helper_hints,
ant_quic::PeerId(candidate.machine_id.0),
"machine_discovery_cache",
candidate.addresses.iter().copied(),
candidate.is_coordinator == Some(true),
candidate.is_relay == Some(true),
);
}
let helper_candidate_count = helper_hints.len();
let helper_addr_total: usize = helper_hints.values().map(|entry| entry.addrs.len()).sum();
tracing::info!(
target: "x0x::connect",
stage = "seed_target_hints",
%target_agent_prefix,
%target_machine_prefix,
target_addr_count = target.addresses.len(),
helper_candidate_count,
helper_addr_total,
"prepared helper hints for peer-authenticated dial"
);
for (peer_id, entry) in &helper_hints {
tracing::debug!(
target: "x0x::connect",
stage = "seed_target_hints",
helper_peer_prefix = %network::hex_prefix(&peer_id.0, 4),
helper_addr_count = entry.addrs.len(),
supports_coordination = entry.caps.supports_coordination,
supports_relay = entry.caps.supports_relay,
sources = %entry.sources.iter().copied().collect::<Vec<_>>().join(","),
"helper candidate discovered"
);
}
for (peer_id, entry) in helper_hints {
let HelperHintEntry {
mut addrs,
caps,
sources,
} = entry;
addrs.retain(|addr| !target.addresses.contains(addr));
if addrs.is_empty() && !caps.supports_coordination && !caps.supports_relay {
tracing::debug!(
target: "x0x::connect",
stage = "seed_target_hints",
helper_peer_prefix = %network::hex_prefix(&peer_id.0, 4),
sources = %sources.iter().copied().collect::<Vec<_>>().join(","),
"skipping helper with no remaining addresses or assist capability"
);
continue;
}
tracing::debug!(
target: "x0x::connect",
stage = "seed_target_hints",
helper_peer_prefix = %network::hex_prefix(&peer_id.0, 4),
helper_addr_count = addrs.len(),
supports_coordination = caps.supports_coordination,
supports_relay = caps.supports_relay,
sources = %sources.iter().copied().collect::<Vec<_>>().join(","),
"upserting helper peer hints"
);
network
.upsert_peer_hints(peer_id, addrs, Some(caps))
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to upsert helper peer hints: {e}"
)))
})?;
}
Ok(())
}
pub async fn connect_to_agent(
&self,
agent_id: &identity::AgentId,
) -> error::Result<connectivity::ConnectOutcome> {
let call_start = std::time::Instant::now();
let agent_prefix = network::hex_prefix(&agent_id.0, 4);
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
"begin"
);
let discovered = {
let cache = self.identity_discovery_cache.read().await;
cache.get(agent_id).cloned()
};
let agent = match discovered {
Some(a) => a,
None => {
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
outcome = "not_found",
dur_ms = call_start.elapsed().as_millis() as u64,
"agent not in discovery cache"
);
return Ok(connectivity::ConnectOutcome::NotFound);
}
};
{
let revoked = self.revocation_set.read().await;
if revoked.is_agent_revoked(agent_id) || revoked.is_machine_revoked(&agent.machine_id) {
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
outcome = "revoked",
dur_ms = call_start.elapsed().as_millis() as u64,
"connect target is revoked — refusing before any dial"
);
return Ok(connectivity::ConnectOutcome::NotFound);
}
}
let info = connectivity::ReachabilityInfo::from_discovered(&agent);
let v4_addrs = info.addresses.iter().filter(|a| a.is_ipv4()).count();
let v6_addrs = info.addresses.len() - v4_addrs;
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
machine_prefix = %network::hex_prefix(&agent.machine_id.0, 4),
addr_total = info.addresses.len(),
v4_addrs,
v6_addrs,
can_receive_direct = ?info.can_receive_direct,
should_attempt_direct = info.should_attempt_direct(),
needs_coordination = info.needs_coordination(),
"reachability classified"
);
let Some(ref network) = self.network else {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_agent",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
outcome = "unreachable_no_network",
"network layer not initialised"
);
return Ok(connectivity::ConnectOutcome::Unreachable);
};
let connected_machine_id = if agent.machine_id.0 != [0u8; 32]
&& network
.is_connected(&ant_quic::PeerId(agent.machine_id.0))
.await
{
Some(agent.machine_id)
} else {
match self.direct_messaging.get_machine_id(agent_id).await {
Some(machine_id) if network.is_connected(&ant_quic::PeerId(machine_id.0)).await => {
Some(machine_id)
}
_ => None,
}
};
if let Some(machine_id) = connected_machine_id {
if machine_id != agent.machine_id {
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = machine_id;
}
}
self.direct_messaging
.mark_connected(agent.agent_id, machine_id)
.await;
let dur_ms = call_start.elapsed().as_millis() as u64;
return if let Some(addr) = info.addresses.first() {
let family = if addr.is_ipv4() { "v4" } else { "v6" };
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "already_connected",
outcome = "direct",
selected_addr = %addr,
family,
dur_ms,
"reusing existing connection"
);
Ok(connectivity::ConnectOutcome::Direct(*addr))
} else {
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "already_connected",
outcome = "already_connected",
dur_ms,
"reusing existing connection without known addr"
);
Ok(connectivity::ConnectOutcome::AlreadyConnected)
};
}
if info.addresses.is_empty() {
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
outcome = "unreachable",
reason = "no_addresses",
dur_ms = call_start.elapsed().as_millis() as u64,
"no known addresses for agent"
);
return Ok(connectivity::ConnectOutcome::Unreachable);
}
let dial_timeout = std::time::Duration::from_secs(8);
let local_probe_timeout = std::time::Duration::from_secs(3);
let direct_probe_addrs = local_direct_probe_addrs(&info.addresses);
let peer_id_hint =
(agent.machine_id.0 != [0u8; 32]).then_some(ant_quic::PeerId(agent.machine_id.0));
for addr in &direct_probe_addrs {
if let Some(peer_id_hint) = peer_id_hint {
match tokio::time::timeout(
local_probe_timeout,
network.connect_peer_with_addrs(peer_id_hint, vec![*addr]),
)
.await
{
Ok(Ok((selected_addr, connected_peer_id)))
if connected_peer_id == peer_id_hint =>
{
let real_machine_id = identity::MachineId(connected_peer_id.0);
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(connected_peer_id, vec![selected_addr], None)
.await;
}
{
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = real_machine_id;
}
}
self.direct_messaging
.mark_connected(agent.agent_id, real_machine_id)
.await;
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "local_direct_first",
outcome = "direct",
selected_addr = %selected_addr,
family = "v4",
dur_ms = call_start.elapsed().as_millis() as u64,
"local peer-authenticated dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Direct(selected_addr));
}
Ok(Ok((selected_addr, connected_peer_id))) => {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_agent",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
strategy = "local_direct_first",
requested_addr = %crate::logging::LogHexId::addr(&addr.to_string()),
selected_addr = %crate::logging::LogHexId::addr(&selected_addr.to_string()),
connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
"local peer-authenticated dial reached unexpected peer"
);
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "local_direct_first",
%addr,
error = %e,
"local peer-authenticated dial failed; trying verified raw-address fallback"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "local_direct_first",
%addr,
timeout_s = local_probe_timeout.as_secs(),
"local peer-authenticated dial timed out; trying verified raw-address fallback"
);
}
}
match tokio::time::timeout(local_probe_timeout, network.connect_addr(*addr)).await {
Ok(Ok(connected_peer_id)) if connected_peer_id == peer_id_hint => {
let real_machine_id = identity::MachineId(connected_peer_id.0);
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(connected_peer_id, vec![*addr], None)
.await;
}
{
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = real_machine_id;
}
}
self.direct_messaging
.mark_connected(agent.agent_id, real_machine_id)
.await;
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "local_direct_raw_fallback",
outcome = "direct",
selected_addr = %addr,
family = "v4",
dur_ms = call_start.elapsed().as_millis() as u64,
"verified local raw-address fallback succeeded"
);
return Ok(connectivity::ConnectOutcome::Direct(*addr));
}
Ok(Ok(connected_peer_id)) => {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_agent",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
strategy = "local_direct_raw_fallback",
addr = %crate::logging::LogHexId::addr(&addr.to_string()),
connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
"verified local raw-address fallback reached unexpected peer"
);
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "local_direct_raw_fallback",
%addr,
error = %e,
"verified local raw-address fallback failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "local_direct_raw_fallback",
%addr,
timeout_s = local_probe_timeout.as_secs(),
"verified local raw-address fallback timed out"
);
}
}
} else {
match tokio::time::timeout(local_probe_timeout, network.connect_addr(*addr)).await {
Ok(Ok(connected_peer_id)) => {
let real_machine_id = identity::MachineId(connected_peer_id.0);
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(connected_peer_id, vec![*addr], None)
.await;
}
{
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = real_machine_id;
}
}
self.direct_messaging
.mark_connected(agent.agent_id, real_machine_id)
.await;
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "local_direct_first",
outcome = "direct",
selected_addr = %addr,
family = "v4",
dur_ms = call_start.elapsed().as_millis() as u64,
"local direct dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Direct(*addr));
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "local_direct_first",
%addr,
error = %e,
"local direct dial failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "local_direct_first",
%addr,
timeout_s = local_probe_timeout.as_secs(),
"local direct dial timed out"
);
}
}
}
}
if agent.machine_id.0 != [0u8; 32] {
let peer_id_hint = ant_quic::PeerId(agent.machine_id.0);
self.seed_transport_peer_hints_for_target(network, &agent)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to seed transport peer hints: {e}"
)))
})?;
match tokio::time::timeout(
dial_timeout,
network.connect_peer_with_addrs(peer_id_hint, info.addresses.clone()),
)
.await
{
Ok(Ok((addr, verified_peer_id))) => {
let verified_machine_id = identity::MachineId(verified_peer_id.0);
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(verified_peer_id, vec![addr], None)
.await;
bc.record_success(&verified_peer_id, 0).await;
}
{
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = verified_machine_id;
}
}
self.direct_messaging
.mark_connected(agent.agent_id, verified_machine_id)
.await;
let family = if addr.is_ipv4() { "v4" } else { "v6" };
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "hinted_peer",
outcome = "coordinated",
selected_addr = %addr,
family,
dur_ms = call_start.elapsed().as_millis() as u64,
"hinted peer dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Coordinated(addr));
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "hinted_peer",
error = %e,
"hinted peer dial failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "hinted_peer",
timeout_s = dial_timeout.as_secs(),
"hinted peer dial timed out"
);
}
}
}
if info.should_attempt_direct() {
for addr in &info.addresses {
if direct_probe_addrs.contains(addr) {
continue;
}
match tokio::time::timeout(dial_timeout, network.connect_addr(*addr)).await {
Ok(Ok(connected_peer_id)) => {
let real_machine_id = identity::MachineId(connected_peer_id.0);
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(connected_peer_id, vec![*addr], None)
.await;
}
{
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = real_machine_id;
}
}
self.direct_messaging
.mark_connected(agent.agent_id, real_machine_id)
.await;
let family = if addr.is_ipv4() { "v4" } else { "v6" };
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "direct_per_addr",
outcome = "direct",
selected_addr = %addr,
family,
dur_ms = call_start.elapsed().as_millis() as u64,
"direct dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Direct(*addr));
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "direct_per_addr",
%addr,
error = %e,
"direct dial failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "direct_per_addr",
%addr,
timeout_s = dial_timeout.as_secs(),
"direct dial timed out"
);
}
}
}
}
if info.needs_coordination() || !info.should_attempt_direct() {
for coord in &info.reachable_via {
if let Some(coord_machine) = self
.machine_discovery_cache
.read()
.await
.get(coord)
.cloned()
{
let coord_peer = ant_quic::PeerId(coord_machine.machine_id.0);
let coord_addrs = coord_machine.addresses.clone();
if !coord_addrs.is_empty() {
if let Err(e) = network
.upsert_peer_hints(coord_peer, coord_addrs, None)
.await
{
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
coord_prefix = %network::hex_prefix(&coord.0, 4),
error = %e,
"failed to seed coordinator hints from reachable_via"
);
}
}
}
}
let peer_id_hint = ant_quic::PeerId(agent.machine_id.0);
let hint_was_zeroed = agent.machine_id.0 == [0u8; 32];
self.seed_transport_peer_hints_for_target(network, &agent)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to seed transport peer hints: {e}"
)))
})?;
let coordinated_result = tokio::time::timeout(
dial_timeout,
network.connect_peer_with_addrs(peer_id_hint, info.addresses.clone()),
)
.await;
match coordinated_result {
Ok(Ok((addr, verified_peer_id))) => {
let verified_machine_id = identity::MachineId(verified_peer_id.0);
if !hint_was_zeroed {
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(verified_peer_id, vec![addr], None)
.await;
bc.record_success(&verified_peer_id, 0).await;
}
{
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = verified_machine_id;
}
}
}
if !hint_was_zeroed {
self.direct_messaging
.mark_connected(agent.agent_id, verified_machine_id)
.await;
}
let family = if addr.is_ipv4() { "v4" } else { "v6" };
tracing::info!(
target: "x0x::connect",
stage = "connect_to_agent",
%agent_prefix,
strategy = "coordinated_fallback",
outcome = "coordinated",
selected_addr = %addr,
family,
hint_was_zeroed,
dur_ms = call_start.elapsed().as_millis() as u64,
"coordinated dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Coordinated(addr));
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "coordinated_fallback",
error = %e,
"coordinated dial failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
%agent_prefix,
strategy = "coordinated_fallback",
timeout_s = dial_timeout.as_secs(),
"coordinated dial timed out"
);
}
}
}
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_agent",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
outcome = "unreachable",
reason = "all_strategies_exhausted",
dur_ms = call_start.elapsed().as_millis() as u64,
v4_addrs,
v6_addrs,
"all connection strategies exhausted"
);
Ok(connectivity::ConnectOutcome::Unreachable)
}
pub async fn connect_to_machine(
&self,
machine_id: &identity::MachineId,
) -> error::Result<connectivity::ConnectOutcome> {
let call_start = std::time::Instant::now();
let machine_prefix = network::hex_prefix(&machine_id.0, 4);
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
"begin"
);
let machine = {
let cache = self.machine_discovery_cache.read().await;
cache.get(machine_id).cloned()
};
let Some(machine) = machine else {
tracing::info!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
outcome = "not_found",
dur_ms = call_start.elapsed().as_millis() as u64,
"machine not in discovery cache"
);
return Ok(connectivity::ConnectOutcome::NotFound);
};
let Some(ref network) = self.network else {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_machine",
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
outcome = "unreachable_no_network",
"network layer not initialised"
);
return Ok(connectivity::ConnectOutcome::Unreachable);
};
let peer_id = ant_quic::PeerId(machine.machine_id.0);
if network.is_connected(&peer_id).await {
for agent_id in &machine.agent_ids {
self.direct_messaging
.mark_connected(*agent_id, machine.machine_id)
.await;
}
return if let Some(addr) = machine.addresses.first() {
Ok(connectivity::ConnectOutcome::Direct(*addr))
} else {
Ok(connectivity::ConnectOutcome::AlreadyConnected)
};
}
let info = connectivity::ReachabilityInfo::from_discovered_machine(&machine);
let v4_addrs = info.addresses.iter().filter(|a| a.is_ipv4()).count();
let v6_addrs = info.addresses.len() - v4_addrs;
tracing::info!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
addr_total = info.addresses.len(),
v4_addrs,
v6_addrs,
can_receive_direct = ?info.can_receive_direct,
should_attempt_direct = info.should_attempt_direct(),
needs_coordination = info.needs_coordination(),
"machine reachability classified"
);
if info.addresses.is_empty() {
return Ok(connectivity::ConnectOutcome::Unreachable);
}
let dial_timeout = std::time::Duration::from_secs(8);
let local_probe_timeout = std::time::Duration::from_secs(3);
let direct_probe_addrs = local_direct_probe_addrs(&info.addresses);
for addr in &direct_probe_addrs {
match tokio::time::timeout(
local_probe_timeout,
network.connect_peer_with_addrs(peer_id, vec![*addr]),
)
.await
{
Ok(Ok((selected_addr, connected_peer_id))) if connected_peer_id == peer_id => {
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(connected_peer_id, vec![selected_addr], None)
.await;
}
for agent_id in &machine.agent_ids {
self.direct_messaging
.mark_connected(*agent_id, machine.machine_id)
.await;
}
tracing::info!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "local_direct_first",
outcome = "direct",
selected_addr = %selected_addr,
dur_ms = call_start.elapsed().as_millis() as u64,
"machine local direct dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Direct(selected_addr));
}
Ok(Ok((selected_addr, connected_peer_id))) => {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_machine",
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
requested_addr = %crate::logging::LogHexId::addr(&addr.to_string()),
selected_addr = %crate::logging::LogHexId::addr(&selected_addr.to_string()),
connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
"machine local direct dial reached unexpected peer"
);
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "local_direct_first",
%addr,
error = %e,
"machine local direct dial failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "local_direct_first",
%addr,
timeout_s = local_probe_timeout.as_secs(),
"machine local direct dial timed out"
);
}
}
match tokio::time::timeout(local_probe_timeout, network.connect_addr(*addr)).await {
Ok(Ok(connected_peer_id)) if connected_peer_id == peer_id => {
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(connected_peer_id, vec![*addr], None)
.await;
}
for agent_id in &machine.agent_ids {
self.direct_messaging
.mark_connected(*agent_id, machine.machine_id)
.await;
}
tracing::info!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "local_direct_raw_fallback",
outcome = "direct",
selected_addr = %addr,
dur_ms = call_start.elapsed().as_millis() as u64,
"verified machine local raw-address fallback succeeded"
);
return Ok(connectivity::ConnectOutcome::Direct(*addr));
}
Ok(Ok(connected_peer_id)) => {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_machine",
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
strategy = "local_direct_raw_fallback",
addr = %crate::logging::LogHexId::addr(&addr.to_string()),
connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
"verified machine local raw-address fallback reached unexpected peer"
);
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "local_direct_raw_fallback",
%addr,
error = %e,
"verified machine local raw-address fallback failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "local_direct_raw_fallback",
%addr,
timeout_s = local_probe_timeout.as_secs(),
"verified machine local raw-address fallback timed out"
);
}
}
}
network
.upsert_peer_hints(peer_id, info.addresses.clone(), None)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to upsert machine peer hints: {e}"
)))
})?;
match tokio::time::timeout(
dial_timeout,
network.connect_peer_with_addrs(peer_id, info.addresses.clone()),
)
.await
{
Ok(Ok((addr, verified_peer_id))) if verified_peer_id == peer_id => {
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(verified_peer_id, vec![addr], None)
.await;
bc.record_success(&verified_peer_id, 0).await;
}
for agent_id in &machine.agent_ids {
self.direct_messaging
.mark_connected(*agent_id, machine.machine_id)
.await;
}
tracing::info!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "hinted_peer",
outcome = "coordinated",
selected_addr = %addr,
dur_ms = call_start.elapsed().as_millis() as u64,
"machine peer-authenticated dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Coordinated(addr));
}
Ok(Ok((addr, verified_peer_id))) => {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_machine",
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
selected_addr = %crate::logging::LogHexId::addr(&addr.to_string()),
verified_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&verified_peer_id.0, 4)),
"machine dial reached unexpected peer"
);
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
error = %e,
"machine peer-authenticated dial failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
timeout_s = dial_timeout.as_secs(),
"machine peer-authenticated dial timed out"
);
}
}
if info.should_attempt_direct() {
for addr in &info.addresses {
if direct_probe_addrs.contains(addr) {
continue;
}
match tokio::time::timeout(dial_timeout, network.connect_addr(*addr)).await {
Ok(Ok(connected_peer_id)) if connected_peer_id == peer_id => {
if let Some(ref bc) = self.bootstrap_cache {
bc.add_from_connection(connected_peer_id, vec![*addr], None)
.await;
}
for agent_id in &machine.agent_ids {
self.direct_messaging
.mark_connected(*agent_id, machine.machine_id)
.await;
}
tracing::info!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
strategy = "direct_per_addr",
outcome = "direct",
selected_addr = %addr,
dur_ms = call_start.elapsed().as_millis() as u64,
"machine direct dial succeeded"
);
return Ok(connectivity::ConnectOutcome::Direct(*addr));
}
Ok(Ok(connected_peer_id)) => {
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_machine",
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
addr = %crate::logging::LogHexId::addr(&addr.to_string()),
connected_machine_prefix = %crate::logging::LogHexId::new("machine", &network::hex_prefix(&connected_peer_id.0, 4)),
"machine direct dial reached unexpected peer"
);
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
%addr,
error = %e,
"machine direct dial failed"
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
stage = "connect_to_machine",
%machine_prefix,
%addr,
timeout_s = dial_timeout.as_secs(),
"machine direct dial timed out"
);
}
}
}
}
tracing::warn!(
target: "x0x::connect",
stage = "connect_to_machine",
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
outcome = "unreachable",
reason = "all_strategies_exhausted",
dur_ms = call_start.elapsed().as_millis() as u64,
v4_addrs,
v6_addrs,
"all machine connection strategies exhausted"
);
Ok(connectivity::ConnectOutcome::Unreachable)
}
fn spawn_tracked<F>(&self, fut: F)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
let mut guard = match self.tracked_tasks.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
if guard.closed {
return;
}
guard.handles.push(tokio::spawn(fut));
}
pub fn begin_shutdown(&self) {
self.shutdown_token.cancel();
let mut guard = match self.tracked_tasks.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.closed = true;
}
pub async fn shutdown(&self) {
self.shutdown_token.cancel();
self.stop_identity_heartbeat().await;
self.stop_discovery_cache_reaper().await;
self.stop_dm_inbox().await;
{
let service = {
let mut guard = self.capability_advert_service.lock().await;
guard.take()
};
if let Some(service) = service {
service.abort();
}
}
let handles = {
let mut guard = match self.tracked_tasks.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.closed = true;
std::mem::take(&mut guard.handles)
};
if !handles.is_empty() {
let abort_handles: Vec<tokio::task::AbortHandle> =
handles.iter().map(|h| h.abort_handle()).collect();
let mut join = futures::future::join_all(handles);
tokio::select! {
_results = &mut join => {}
_ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {
tracing::warn!(
"Agent background tasks did not stop within grace; aborting stragglers"
);
for handle in &abort_handles {
handle.abort();
}
let _results: Vec<Result<(), tokio::task::JoinError>> = join.await;
}
}
}
if let Some(ref pw) = self.presence {
pw.shutdown().await;
tracing::info!("Presence system shut down");
}
if let Some(ref cache) = self.bootstrap_cache {
if let Err(e) = cache.save().await {
tracing::warn!("Failed to save bootstrap cache on shutdown: {e}");
} else {
tracing::info!("Bootstrap cache saved on shutdown");
}
}
if let Some(ref runtime) = self.gossip_runtime {
if let Err(e) = runtime.shutdown().await {
tracing::warn!("Gossip runtime shutdown error: {e}");
} else {
tracing::info!("Gossip runtime shut down");
}
}
if let Some(ref network) = self.network {
network.shutdown().await;
tracing::info!("Network node shut down");
}
}
async fn stop_identity_heartbeat(&self) {
let handle = {
let mut handle_guard = self.heartbeat_handle.lock().await;
handle_guard.take()
};
if let Some(handle) = handle {
handle.abort();
match handle.await {
Ok(()) => tracing::debug!("Identity heartbeat task stopped"),
Err(e) if e.is_cancelled() => {
tracing::debug!("Identity heartbeat task aborted")
}
Err(e) => tracing::warn!("Identity heartbeat task failed during shutdown: {e}"),
}
}
}
async fn stop_discovery_cache_reaper(&self) {
let handle = {
let mut handle_guard = self.discovery_cache_reaper_handle.lock().await;
handle_guard.take()
};
if let Some(handle) = handle {
handle.abort();
match handle.await {
Ok(()) => tracing::debug!("Discovery cache reaper stopped"),
Err(e) if e.is_cancelled() => {
tracing::debug!("Discovery cache reaper aborted")
}
Err(e) => tracing::warn!("Discovery cache reaper failed during shutdown: {e}"),
}
}
}
async fn discovery_cache_reaper_loop(
identity_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
>,
machine_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
>,
user_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::UserId, DiscoveredUser>>,
>,
ttl_secs: u64,
interval: std::time::Duration,
) {
loop {
tokio::time::sleep(interval).await;
let cutoff = Self::unix_timestamp_secs().saturating_sub(ttl_secs);
{
let mut c = identity_cache.write().await;
c.retain(|_, a| a.last_seen >= cutoff);
}
{
let mut c = machine_cache.write().await;
c.retain(|_, m| m.last_seen >= cutoff);
}
{
let mut c = user_cache.write().await;
c.retain(|_, u| u.last_seen >= cutoff);
}
}
}
async fn start_discovery_cache_reaper(&self) -> error::Result<()> {
let mut guard = self.discovery_cache_reaper_handle.lock().await;
if self.shutdown_token.is_cancelled() {
return Ok(());
}
if guard.is_some() {
return Ok(());
}
let identity = std::sync::Arc::clone(&self.identity_discovery_cache);
let machine = std::sync::Arc::clone(&self.machine_discovery_cache);
let user = std::sync::Arc::clone(&self.user_discovery_cache);
let ttl = self.identity_ttl_secs;
let interval = std::time::Duration::from_secs(DISCOVERY_CACHE_REAPER_INTERVAL_SECS);
let handle = tokio::spawn(Self::discovery_cache_reaper_loop(
identity, machine, user, ttl, interval,
));
*guard = Some(handle);
Ok(())
}
pub async fn send_direct(
&self,
to: &identity::AgentId,
payload: Vec<u8>,
) -> Result<dm::DmReceipt, dm::DmError> {
self.send_direct_with_config(to, payload, dm::DmSendConfig::default())
.await
}
async fn dm_peer_rtt_ms(&self, agent_id: &identity::AgentId) -> Option<u32> {
let registry_machine_id = self.direct_messaging.get_machine_id(agent_id).await;
let cached_machine_id = {
let cache = self.identity_discovery_cache.read().await;
cache
.get(agent_id)
.map(|entry| entry.machine_id)
.filter(|machine_id| machine_id.0 != [0_u8; 32])
};
let machine_id = registry_machine_id.or(cached_machine_id)?;
let peer = self
.bootstrap_cache
.as_ref()?
.get(&ant_quic::PeerId(machine_id.0))
.await?;
(peer.stats.avg_rtt_ms > 0).then_some(peer.stats.avg_rtt_ms)
}
async fn dm_lifecycle_hint(
&self,
agent_id: &identity::AgentId,
) -> Option<dm_send::DmLifecycleHint> {
let registry_machine_id = self.direct_messaging.get_machine_id(agent_id).await;
let cached_machine_id = {
let cache = self.identity_discovery_cache.read().await;
cache
.get(agent_id)
.map(|entry| entry.machine_id)
.filter(|machine_id| machine_id.0 != [0_u8; 32])
};
let machine_id = registry_machine_id.or(cached_machine_id)?;
Some(dm_send::DmLifecycleHint {
recipient_machine_id: machine_id,
replaced_rx: self.direct_messaging.subscribe_lifecycle_replaced(),
})
}
async fn dm_peer_likely_offline(
&self,
agent_id: &identity::AgentId,
) -> Option<(f64, Option<u64>)> {
if self.is_agent_connected(agent_id).await {
return None;
}
let last_seen = {
let cache = self.identity_discovery_cache.read().await;
cache.get(agent_id).map(|entry| entry.last_seen)
}?;
let now_secs = Self::unix_timestamp_secs();
let age_secs = now_secs.saturating_sub(last_seen);
let heartbeat = self.heartbeat_interval_secs.max(1);
let phi = age_secs as f64 / heartbeat as f64;
(phi > 8.0).then_some((phi, Some(age_secs.saturating_mul(1000))))
}
pub async fn send_direct_with_config(
&self,
to: &identity::AgentId,
payload: Vec<u8>,
config: dm::DmSendConfig,
) -> Result<dm::DmReceipt, dm::DmError> {
if *to == self.identity.agent_id() {
self.direct_messaging.record_outgoing_started(*to, None);
if payload.len() > direct::MAX_DIRECT_PAYLOAD_SIZE {
self.direct_messaging.record_outgoing_failed(*to);
return Err(dm::DmError::PayloadTooLarge {
len: payload.len(),
max: direct::MAX_DIRECT_PAYLOAD_SIZE,
});
}
let delivered = self
.direct_messaging
.handle_loopback(
self.identity.machine_id(),
self.identity.agent_id(),
payload,
)
.await;
let receipt = dm_send::loopback_receipt();
self.direct_messaging
.record_outgoing_succeeded(*to, receipt.path);
tracing::debug!(
target: "dm.trace",
stage = "outbound_send_returned_ok",
request_id = %hex::encode(receipt.request_id),
sender = %hex::encode(self.identity.agent_id().as_bytes()),
recipient = %hex::encode(to.as_bytes()),
path = "loopback",
delivered_subscribers = delivered,
);
return Ok(receipt);
}
let advert_cap = self.capability_store.lookup(to);
let advert_gossip_ready = advert_cap
.as_ref()
.is_some_and(|caps| caps.gossip_inbox && !caps.kem_public_key.is_empty());
let (cap, cap_source) = if advert_gossip_ready {
(advert_cap, "advert_cache")
} else {
let contact_cap = {
let contacts = self.contact_store.read().await;
contacts.get(to).and_then(|contact| {
contact
.dm_capabilities
.as_ref()
.filter(|caps| caps.gossip_inbox && !caps.kem_public_key.is_empty())
.cloned()
})
};
match contact_cap {
Some(cap) if advert_cap.is_some() => {
(Some(cap), "contact_card_after_unusable_advert")
}
Some(cap) => (Some(cap), "contact_card"),
None if advert_cap.is_some() => (advert_cap, "advert_cache_unusable"),
None => (None, "none"),
}
};
let gossip_ok = cap
.as_ref()
.map(|c| c.gossip_inbox && !c.kem_public_key.is_empty())
.unwrap_or(false);
tracing::debug!(
target: "dm.trace",
stage = "capability_lookup",
recipient = %hex::encode(to.as_bytes()),
hit = cap.is_some(),
gossip_ok,
source = cap_source,
capability_store_entries = self.capability_store.len(),
);
let relay_seed: Option<(Vec<u8>, Vec<u8>)> = if self.peer_relay.policy().enabled {
cap.as_ref()
.filter(|c| !c.kem_public_key.is_empty())
.map(|c| (payload.clone(), c.kem_public_key.clone()))
} else {
None
};
let rtt_hint_ms = self.dm_peer_rtt_ms(to).await;
let mut config = config;
if !gossip_ok && config.timeout_per_attempt == dm::dm_attempt_timeout(None) {
config.timeout_per_attempt = dm::dm_attempt_timeout(rtt_hint_ms);
}
self.direct_messaging
.record_outgoing_started(*to, rtt_hint_ms);
if let Some((phi, last_seen_ms_ago)) = self.dm_peer_likely_offline(to).await {
self.direct_messaging.record_outgoing_failed(*to);
return Err(dm::DmError::PeerLikelyOffline {
phi,
last_seen_ms_ago,
});
}
let mut preferred_raw_err = None;
let prefer_newest_grace = std::time::Duration::from_millis(config.prefer_newest_grace_ms);
let preferred_raw_receipt = if config.prefer_raw_quic_if_connected && !config.require_gossip
{
match self
.send_direct_raw_quic(
to,
&payload,
config.raw_quic_receive_ack_timeout,
prefer_newest_grace,
)
.await
{
Ok(path) => Some(dm_send::raw_quic_receipt_for_path(path)),
Err(e) => {
tracing::debug!(
target: "x0x::direct",
recipient = %hex::encode(to.as_bytes()),
error = %e,
"preferred raw-QUIC path unavailable; falling back to capability-aware send"
);
preferred_raw_err = Some(e);
None
}
}
} else {
None
};
let result = if let Some(receipt) = preferred_raw_receipt {
Ok(receipt)
} else if preferred_raw_err.as_ref().is_some_and(|err| {
config.stop_fallback_on_raw_error
|| Self::raw_quic_error_should_stop_fallback(err, gossip_ok)
}) {
match preferred_raw_err.take() {
Some(e) => Err(Self::map_raw_quic_dm_error(e)),
None => Err(dm::DmError::NoConnectivity(
"raw-QUIC send failed before gossip fallback".to_string(),
)),
}
} else if gossip_ok {
match self.gossip_runtime.as_ref() {
Some(runtime) => {
let signing =
gossip::SigningContext::from_keypair(self.identity.agent_keypair());
let kem_pub = cap
.as_ref()
.map(|c| c.kem_public_key.clone())
.unwrap_or_default();
let lifecycle_hint = self.dm_lifecycle_hint(to).await;
dm_send::send_via_gossip(
dm_send::DmSendContext {
pubsub: std::sync::Arc::clone(runtime.pubsub()),
signing: &signing,
self_agent_id: self.identity.agent_id(),
self_machine_id: self.identity.machine_id(),
inflight: std::sync::Arc::clone(&self.dm_inflight_acks),
},
*to,
&kem_pub,
payload,
&config,
lifecycle_hint,
)
.await
}
None => Err(dm::DmError::LocalGossipUnavailable(
"send_direct: no gossip runtime configured".to_string(),
)),
}
} else if config.require_gossip {
Err(dm::DmError::RecipientKeyUnavailable(format!(
"recipient {} has no gossip DM capability advert",
hex::encode(to.as_bytes())
)))
} else {
match preferred_raw_err {
Some(e) => Err(Self::map_raw_quic_dm_error(e)),
None => self
.send_direct_raw_quic(
to,
&payload,
config.raw_quic_receive_ack_timeout,
prefer_newest_grace,
)
.await
.map(dm_send::raw_quic_receipt_for_path)
.map_err(Self::map_raw_quic_dm_error),
}
};
match result {
Ok(receipt) => {
self.direct_messaging
.record_outgoing_succeeded(*to, receipt.path);
self.peer_relay.record_direct_success(to);
Ok(receipt)
}
Err(direct_err) => {
self.direct_messaging.record_outgoing_failed(*to);
self.peer_relay.record_direct_failure(to);
if let Some((saved_payload, kem_pub)) = relay_seed {
if self.peer_relay.needs_relay(to) {
match self.try_relay_fallback(to, saved_payload, &kem_pub).await {
Ok(relay_receipt) => {
self.direct_messaging
.record_outgoing_succeeded(*to, relay_receipt.path);
return Ok(relay_receipt);
}
Err(relay_err) => {
tracing::debug!(
target: "x0x::relay",
recipient = %hex::encode(to.as_bytes()),
direct_err = %direct_err,
relay_err = %relay_err,
"X0X-0070b relay fallback failed; surfacing original direct error"
);
}
}
}
}
Err(direct_err)
}
}
}
async fn try_relay_fallback(
&self,
to: &identity::AgentId,
payload: Vec<u8>,
recipient_kem_public_key: &[u8],
) -> Result<dm::DmReceipt, dm::DmError> {
let sender = self.identity.agent_id();
let candidates = self.relay_candidates.read().await.clone();
let Some(relay_agent) = self.peer_relay.select_relay(&candidates, to, &sender) else {
return Err(dm::DmError::NoRelayCandidate);
};
let relay_machine_id = {
let cache = self.identity_discovery_cache.read().await;
cache.get(&relay_agent).map(|e| e.machine_id)
};
let Some(relay_machine_id) = relay_machine_id else {
return Err(dm::DmError::NoRelayCandidate);
};
let now = dm::now_unix_ms();
let expires = now.saturating_add(dm_send::DEFAULT_ENVELOPE_LIFETIME_MS);
let request_id = dm_send::fresh_request_id();
let signing = gossip::SigningContext::from_keypair(self.identity.agent_keypair());
let envelope = dm::EnvelopeBuilder::build_payload_envelope(
request_id,
&sender,
&self.identity.machine_id(),
to,
recipient_kem_public_key,
now,
expires,
payload,
|bytes| signing.sign(bytes).map_err(|e| e.to_string()),
)?;
let (sender_pub_bytes, sender_sec_bytes) = self.identity.agent_keypair().to_bytes();
let sender_secret = ant_quic::MlDsaSecretKey::from_bytes(&sender_sec_bytes)
.map_err(|e| dm::DmError::RelayBuildFailed(format!("agent secret key: {e:?}")))?;
let relayed = self
.peer_relay
.build_relayed_dm(to, &sender, sender_pub_bytes, now, envelope, |bytes| {
ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(&sender_secret, bytes)
.map(|s| s.as_bytes().to_vec())
.map_err(|e| format!("{e:?}"))
})
.map_err(dm::DmError::RelayBuildFailed)?;
let wire = postcard::to_allocvec(&relayed).map_err(|e| {
dm::DmError::EnvelopeConstruction(format!("relayed envelope postcard: {e}"))
})?;
let network = self
.network
.as_ref()
.ok_or_else(|| dm::DmError::NoConnectivity("no network for relay send".to_string()))?;
let relay_peer_id = ant_quic::PeerId(relay_machine_id.0);
network
.send_direct_typed(
&relay_peer_id,
sender.as_bytes(),
network::RELAYED_DM_STREAM_TYPE,
&wire,
)
.await
.map_err(|e| dm::DmError::PublishFailed(format!("relay send: {e}")))?;
Ok(dm::DmReceipt {
request_id,
accepted_at: std::time::Instant::now(),
retries_used: 0,
path: dm::DmPath::Relayed { via: relay_agent },
})
}
#[must_use]
pub fn peer_relay(&self) -> &peer_relay::PeerRelay {
&self.peer_relay
}
pub async fn relay_candidates(&self) -> Vec<identity::AgentId> {
self.relay_candidates.read().await.clone()
}
async fn send_direct_raw_quic(
&self,
agent_id: &identity::AgentId,
payload: &[u8],
receive_ack_timeout: Option<std::time::Duration>,
prefer_newest_grace: std::time::Duration,
) -> error::NetworkResult<dm::DmPath> {
let send_start = std::time::Instant::now();
let agent_prefix = network::hex_prefix(&agent_id.0, 4);
let self_prefix = network::hex_prefix(&self.identity.agent_id().0, 4);
let bytes = payload.len();
let digest = direct::dm_payload_digest_hex(payload);
let target_path_label = if receive_ack_timeout.is_some() {
"raw_quic_acked"
} else {
"raw_quic"
};
let network = self.network.as_ref().ok_or_else(|| {
tracing::warn!(
target: "x0x::direct",
stage = "send",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
outcome = "err_no_network",
"network not initialised"
);
error::NetworkError::NodeCreation("network not initialized".to_string())
})?;
let cached_machine_id = {
let cache = self.identity_discovery_cache.read().await;
cache
.get(agent_id)
.map(|d| d.machine_id)
.filter(|m| m.0 != [0u8; 32]) };
let registry_machine_id = self.direct_messaging.get_machine_id(agent_id).await;
let (machine_id, resolution) = match (cached_machine_id, registry_machine_id) {
(Some(id), _) if network.is_connected(&ant_quic::PeerId(id.0)).await => {
(id, "cached_connected")
}
(_, Some(id)) if network.is_connected(&ant_quic::PeerId(id.0)).await => {
if cached_machine_id != Some(id) {
let mut cache = self.identity_discovery_cache.write().await;
if let Some(entry) = cache.get_mut(agent_id) {
entry.machine_id = id;
}
}
(id, "registry_connected")
}
(Some(id), None) => (id, "cached_not_connected"),
(Some(id), Some(_)) => (id, "cached_both_disconnected"),
(None, Some(id)) => (id, "registry_not_connected"),
(None, None) => {
tracing::debug!(
target: "x0x::direct",
stage = "send",
%agent_prefix,
resolution = "last_resort_connect",
"no machine_id known; triggering connect_to_agent"
);
let _ = self.connect_to_agent(agent_id).await;
let id = self
.direct_messaging
.get_machine_id(agent_id)
.await
.ok_or_else(|| {
tracing::warn!(
target: "x0x::direct",
stage = "send",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
outcome = "err_agent_not_found",
dur_ms = send_start.elapsed().as_millis() as u64,
"no machine_id after connect_to_agent"
);
error::NetworkError::AgentNotFound(agent_id.0)
})?;
(id, "post_connect")
}
};
let ant_peer_id = ant_quic::PeerId(machine_id.0);
let machine_prefix = network::hex_prefix(&machine_id.0, 4);
let mut connected = network.is_connected(&ant_peer_id).await;
let mut repair_outcome: Option<&'static str> = None;
if !connected && resolution != "post_connect" {
const REPAIR_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
let outcome = match tokio::time::timeout(
REPAIR_TIMEOUT,
network.ensure_peer_send_ready(&ant_peer_id),
)
.await
{
Ok(Ok(())) => "repaired",
Ok(Err(_)) => "repair_failed",
Err(_) => "repair_timeout",
};
repair_outcome = Some(outcome);
tracing::debug!(
target: "x0x::direct",
stage = "send",
%agent_prefix,
%machine_prefix,
resolution,
outcome,
"send-readiness repair on disconnected peer"
);
connected = network.is_connected(&ant_peer_id).await;
}
if connected {
if let Some(reason) = self.direct_messaging.lifecycle_block_reason(&machine_id) {
tracing::warn!(
target: "x0x::direct",
stage = "send",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
resolution,
?repair_outcome,
reason = %reason,
"ignoring stale lifecycle block because ant-quic reports a live connection"
);
self.direct_messaging
.record_lifecycle_established(machine_id, None);
}
} else {
if let Some(reason) = self.direct_messaging.lifecycle_block_reason(&machine_id) {
tracing::warn!(
target: "x0x::direct",
stage = "send",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
resolution,
?repair_outcome,
outcome = "err_peer_disconnected",
reason = %reason,
dur_ms = send_start.elapsed().as_millis() as u64,
"lifecycle watcher says peer is disconnected"
);
return Err(error::NetworkError::ConnectionFailed(format!(
"peer disconnected: {reason}"
)));
}
tracing::warn!(
target: "x0x::direct",
stage = "send",
agent_prefix = %crate::logging::LogHexId::agent(&agent_prefix),
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
resolution,
?repair_outcome,
outcome = "err_not_connected",
bytes,
dur_ms = send_start.elapsed().as_millis() as u64,
"machine_id resolved but peer not currently connected after repair attempt"
);
return Err(error::NetworkError::AgentNotConnected(agent_id.0));
}
tracing::debug!(
target: "dm.trace",
stage = "path_chosen",
sender = %hex::encode(self.identity.agent_id().as_bytes()),
recipient = %hex::encode(agent_id.as_bytes()),
machine_id = %hex::encode(machine_id.as_bytes()),
path = target_path_label,
bytes,
digest = %digest,
);
let send_result = if let Some(timeout) = receive_ack_timeout {
let wire = direct::DirectMessaging::encode_message(&self.identity.agent_id(), payload)?;
tracing::debug!(
target: "dm.trace",
stage = "wire_encoded",
sender = %hex::encode(self.identity.agent_id().as_bytes()),
recipient = %hex::encode(agent_id.as_bytes()),
path = "raw_quic_acked",
bytes = wire.len(),
payload_bytes = bytes,
digest = %digest,
);
self.send_ack_racing_replaced(
network.as_ref(),
ant_peer_id,
machine_id,
&wire,
timeout,
prefer_newest_grace,
agent_id,
)
.await
} else {
tracing::debug!(
target: "dm.trace",
stage = "wire_encoded",
sender = %hex::encode(self.identity.agent_id().as_bytes()),
recipient = %hex::encode(agent_id.as_bytes()),
path = "raw_quic",
bytes,
digest = %digest,
);
network
.send_direct(&ant_peer_id, &self.identity.agent_id().0, payload)
.await
.map(|()| dm::DmPath::RawQuic)
};
match send_result {
Ok(path) => {
let path_label = match path {
dm::DmPath::Loopback => "loopback",
dm::DmPath::RawQuic => "raw_quic",
dm::DmPath::RawQuicAcked => "raw_quic_acked",
dm::DmPath::GossipInbox => "gossip_inbox",
dm::DmPath::Relayed { .. } => "relayed",
};
tracing::debug!(
target: "dm.trace",
stage = "outbound_send_returned_ok",
sender = %hex::encode(self.identity.agent_id().as_bytes()),
recipient = %hex::encode(agent_id.as_bytes()),
machine_id = %hex::encode(machine_id.as_bytes()),
path = path_label,
bytes,
digest = %digest,
dur_ms = send_start.elapsed().as_millis() as u64,
);
tracing::info!(
target: "x0x::direct",
stage = "send",
from = %self_prefix,
to = %agent_prefix,
%machine_prefix,
resolution,
bytes,
dur_ms = send_start.elapsed().as_millis() as u64,
outcome = "ok",
path = ?path,
"direct message sent"
);
Ok(path)
}
Err(e) => {
tracing::warn!(
target: "x0x::direct",
stage = "send",
from = %self_prefix,
to = %agent_prefix,
machine_prefix = %crate::logging::LogHexId::new("machine", &machine_prefix),
resolution,
bytes,
dur_ms = send_start.elapsed().as_millis() as u64,
outcome = "err_transport",
error = %e,
"transport send_direct failed"
);
if receive_ack_timeout.is_some() && network.is_connected(&ant_peer_id).await {
match network.disconnect(&ant_peer_id).await {
Ok(()) => tracing::info!(
target: "x0x::direct",
stage = "send",
to = %agent_prefix,
%machine_prefix,
"tore down zombie connection after acked send failure; retry will redial"
),
Err(de) => tracing::debug!(
target: "x0x::direct",
stage = "send",
to = %agent_prefix,
%machine_prefix,
error = %de,
"failed to tear down zombie connection after acked send failure"
),
}
}
Err(e)
}
}
}
#[allow(clippy::too_many_arguments)]
async fn send_ack_racing_replaced(
&self,
network: &network::NetworkNode,
ant_peer_id: ant_quic::PeerId,
machine_id: identity::MachineId,
wire: &[u8],
timeout: std::time::Duration,
prefer_newest_grace: std::time::Duration,
agent_id: &identity::AgentId,
) -> error::NetworkResult<dm::DmPath> {
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::broadcast::error::TryRecvError as BroadcastTryRecvError;
let mut replaced_rx = self.direct_messaging.subscribe_lifecycle_replaced();
let pre_send_generation = self.direct_messaging.current_generation(&machine_id);
let agent_prefix = network::hex_prefix(&agent_id.0, 4);
let machine_prefix = network::hex_prefix(&machine_id.0, 4);
let self_prefix = network::hex_prefix(&self.identity.agent_id().0, 4);
let ack_race_test_hook = self.direct_messaging.raw_quic_ack_race_test_hook();
let send_fut = async {
if let Some(hook) = ack_race_test_hook.as_ref() {
hook.notify_first_attempt_started();
}
let result = network
.send_with_receive_ack(ant_peer_id, wire, timeout)
.await;
if let Some(hook) = ack_race_test_hook.as_ref() {
hook.hold_first_attempt_result().await;
}
result
};
tokio::pin!(send_fut);
let superseded_to: u64;
loop {
tokio::select! {
biased;
send_result = &mut send_fut => {
match send_result {
Some(Ok(())) => return Ok(dm::DmPath::RawQuicAcked),
Some(Err(e)) => {
let mut queued_supersede: Option<u64> = None;
loop {
match replaced_rx.try_recv() {
Ok((m, gen)) if m == machine_id => {
queued_supersede = Some(gen);
}
Ok(_) => continue,
Err(BroadcastTryRecvError::Empty)
| Err(BroadcastTryRecvError::Closed)
| Err(BroadcastTryRecvError::Lagged(_)) => break,
}
}
if let Some(gen) = queued_supersede {
superseded_to = gen;
break;
}
let reason = format!("send_with_receive_ack failed: {e}");
return if Self::raw_quic_ack_receive_backpressured(&reason) {
Err(error::NetworkError::RemoteReceiveBackpressured(reason))
} else {
Err(error::NetworkError::ConnectionFailed(reason))
};
}
None => return Err(error::NetworkError::NodeCreation(
"network node not initialized".to_string(),
)),
}
}
replaced = replaced_rx.recv() => {
match replaced {
Ok((m, gen)) if m == machine_id => {
superseded_to = gen;
break;
}
Ok(_) => continue,
Err(RecvError::Lagged(_)) => {
let mut found: Option<u64> = None;
loop {
match replaced_rx.try_recv() {
Ok((m, gen)) if m == machine_id => {
found = Some(gen);
break;
}
Ok(_) => continue,
Err(BroadcastTryRecvError::Empty)
| Err(BroadcastTryRecvError::Closed)
| Err(BroadcastTryRecvError::Lagged(_)) => break,
}
}
if let Some(gen) = found {
superseded_to = gen;
break;
}
replaced_rx = self
.direct_messaging
.subscribe_lifecycle_replaced();
continue;
}
Err(RecvError::Closed) => {
let result = (&mut send_fut).await;
return match result {
Some(Ok(())) => Ok(dm::DmPath::RawQuicAcked),
Some(Err(e)) => {
let reason =
format!("send_with_receive_ack failed: {e}");
if Self::raw_quic_ack_receive_backpressured(&reason) {
Err(error::NetworkError::RemoteReceiveBackpressured(reason))
} else {
Err(error::NetworkError::ConnectionFailed(reason))
}
}
None => Err(error::NetworkError::NodeCreation(
"network node not initialized".to_string(),
)),
};
}
}
}
}
}
let new_generation = superseded_to;
if let Some(hook) = ack_race_test_hook.as_ref() {
hook.notify_replaced_short_circuit();
}
tracing::debug!(
target: "dm.trace",
stage = "raw_quic_ack_replaced_short_circuit",
from = %self_prefix,
to = %agent_prefix,
%machine_prefix,
pre_send_generation = ?pre_send_generation,
new_generation,
grace_ms = prefer_newest_grace.as_millis() as u64,
"X0X-0053: same-peer Replaced fired during in-flight send_with_receive_ack; abandoning and reissuing",
);
if !prefer_newest_grace.is_zero() {
let grace_deadline = tokio::time::Instant::now() + prefer_newest_grace;
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(20);
while tokio::time::Instant::now() < grace_deadline {
if network.is_connected(&ant_peer_id).await {
break;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
}
if !network.is_connected(&ant_peer_id).await {
return Err(error::NetworkError::AgentNotConnected(agent_id.0));
}
match network
.send_with_receive_ack(ant_peer_id, wire, timeout)
.await
{
Some(Ok(())) => Ok(dm::DmPath::RawQuicAcked),
Some(Err(e)) => {
let reason = format!("send_with_receive_ack failed: {e}");
if Self::raw_quic_ack_receive_backpressured(&reason) {
Err(error::NetworkError::RemoteReceiveBackpressured(reason))
} else {
Err(error::NetworkError::ConnectionFailed(reason))
}
}
None => Err(error::NetworkError::NodeCreation(
"network node not initialized".to_string(),
)),
}
}
fn raw_quic_error_should_stop_fallback(
err: &error::NetworkError,
gossip_available: bool,
) -> bool {
match err {
error::NetworkError::PayloadTooLarge { .. } | error::NetworkError::NodeCreation(_) => {
true
}
error::NetworkError::ConnectionFailed(reason)
if reason.starts_with("peer disconnected:")
|| reason.starts_with("send_with_receive_ack failed:") =>
{
!gossip_available
}
error::NetworkError::RemoteReceiveBackpressured(_) => !gossip_available,
error::NetworkError::ConnectionClosed(_)
| error::NetworkError::ConnectionReset(_)
| error::NetworkError::NotConnected(_) => !gossip_available,
_ => false,
}
}
fn raw_quic_ack_receive_backpressured(reason: &str) -> bool {
reason.contains("Remote receive pipeline rejected payload: Backpressured")
}
fn map_raw_quic_dm_error(err: error::NetworkError) -> dm::DmError {
match err {
error::NetworkError::AgentNotFound(_) => {
dm::DmError::RecipientKeyUnavailable(err.to_string())
}
error::NetworkError::AgentNotConnected(_)
| error::NetworkError::NotConnected(_)
| error::NetworkError::ConnectionClosed(_)
| error::NetworkError::ConnectionReset(_) => dm::DmError::PeerDisconnected {
reason: err.to_string(),
},
error::NetworkError::ConnectionFailed(reason)
if reason.starts_with("peer disconnected:")
|| reason.starts_with("send_with_receive_ack failed:") =>
{
dm::DmError::PeerDisconnected { reason }
}
error::NetworkError::RemoteReceiveBackpressured(reason) => {
dm::DmError::ReceiverBackpressured { reason }
}
error::NetworkError::PayloadTooLarge { size, max } => {
dm::DmError::PayloadTooLarge { len: size, max }
}
error::NetworkError::NodeCreation(reason) => dm::DmError::NoConnectivity(reason),
other => dm::DmError::PublishFailed(other.to_string()),
}
}
pub async fn recv_direct(&self) -> Option<direct::DirectMessage> {
self.recv_direct_inner().await
}
pub async fn recv_direct_annotated(&self) -> Option<direct::DirectMessage> {
self.recv_direct_inner().await
}
async fn recv_direct_inner(&self) -> Option<direct::DirectMessage> {
self.direct_messaging.recv().await
}
pub fn subscribe_direct(&self) -> direct::DirectMessageReceiver {
self.direct_messaging.subscribe()
}
pub fn direct_messaging(&self) -> &std::sync::Arc<direct::DirectMessaging> {
&self.direct_messaging
}
pub async fn is_agent_connected(&self, agent_id: &identity::AgentId) -> bool {
let Some(network) = &self.network else {
return false;
};
let machine_id = {
let cache = self.identity_discovery_cache.read().await;
cache.get(agent_id).map(|d| d.machine_id)
};
match machine_id {
Some(mid) => {
let ant_peer_id = ant_quic::PeerId(mid.0);
network.is_connected(&ant_peer_id).await
}
None => false,
}
}
pub async fn connected_agents(&self) -> Vec<identity::AgentId> {
let Some(network) = &self.network else {
return Vec::new();
};
let connected_peers = network.connected_peers().await;
let cache = self.identity_discovery_cache.read().await;
cache
.values()
.filter(|agent| {
let ant_peer_id = ant_quic::PeerId(agent.machine_id.0);
connected_peers.contains(&ant_peer_id)
})
.map(|agent| agent.agent_id)
.collect()
}
pub fn set_contacts(&self, store: std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>) {
if let Some(runtime) = &self.gossip_runtime {
let pubsub = runtime.pubsub();
pubsub.set_contacts(store);
pubsub.set_revocation_set(self.revocation_set());
}
}
pub async fn announce_identity(
&self,
include_user_identity: bool,
human_consent: bool,
) -> error::Result<()> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
self.start_identity_listener().await?;
let network_status = if let Some(network) = self.network.as_ref() {
network.node_status().await
} else {
None
};
let assist_snapshot = network_status
.as_ref()
.map(AnnouncementAssistSnapshot::from_node_status)
.unwrap_or_default();
let mut addresses = if let Some(network) = self.network.as_ref() {
match network_status.as_ref() {
Some(status) if !status.external_addrs.is_empty() => status.external_addrs.clone(),
_ => match network.routable_addr().await {
Some(addr) => vec![addr],
None => self.announcement_addresses(),
},
}
} else {
self.announcement_addresses()
};
let bind_port = if let Some(network) = self.network.as_ref() {
network.bound_addr().await.map(|a| a.port()).unwrap_or(5483)
} else {
5483
};
if let Ok(sock) = std::net::UdpSocket::bind("[::]:0") {
if sock.connect("[2001:4860:4860::8888]:80").is_ok() {
if let Ok(local) = sock.local_addr() {
if let std::net::IpAddr::V6(v6) = local.ip() {
let segs = v6.segments();
let is_global = (segs[0] & 0xffc0) != 0xfe80
&& (segs[0] & 0xff00) != 0xfd00
&& !v6.is_loopback();
if is_global {
let v6_addr =
std::net::SocketAddr::new(std::net::IpAddr::V6(v6), bind_port);
if !addresses.contains(&v6_addr) {
addresses.push(v6_addr);
}
}
}
}
}
}
for addr in collect_local_interface_addrs(bind_port) {
if !addresses.contains(&addr) {
addresses.push(addr);
}
}
let allow_local_scope = self
.network
.as_ref()
.is_some_and(|network| allow_local_discovery_addresses(network.config()));
addresses = filter_discovery_announcement_addrs(addresses, allow_local_scope);
let (reachable_via, relay_candidates) = if assist_snapshot.can_receive_direct == Some(true)
{
(Vec::new(), Vec::new())
} else if let Some(network) = self.network.as_ref() {
collect_coordinator_hints(
network.as_ref(),
&self.machine_discovery_cache,
self.machine_id(),
)
.await
} else {
(Vec::new(), Vec::new())
};
let announcement =
self.build_identity_announcement_with_addrs(IdentityAnnouncementBuildOptions {
include_user_identity,
human_consent,
addresses,
assist_snapshot: Some(&assist_snapshot),
reachable_via,
relay_candidates,
allow_local_scope,
})?;
tracing::debug!(
target: "x0x::discovery",
announcement_kind = "explicit",
machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
addr_total = announcement.addresses.len(),
nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
can_receive_direct = ?announcement.can_receive_direct,
relay_capable = ?announcement.is_relay,
coordinator_capable = ?announcement.is_coordinator,
relay_active = ?assist_snapshot.relay_active,
coordinator_active = ?assist_snapshot.coordinator_active,
"publishing identity announcement"
);
let machine_announcement = build_machine_announcement_for_identity(
&self.identity,
announcement.addresses.clone(),
announcement.announced_at,
Some(&assist_snapshot),
announcement.reachable_via.clone(),
announcement.relay_candidates.clone(),
allow_local_scope,
)?;
tracing::debug!(
target: "x0x::discovery",
announcement_kind = "machine_explicit",
machine_prefix = %network::hex_prefix(&machine_announcement.machine_id.0, 4),
addr_total = machine_announcement.addresses.len(),
nat_type = machine_announcement.nat_type.as_deref().unwrap_or("unknown"),
can_receive_direct = ?machine_announcement.can_receive_direct,
relay_capable = ?machine_announcement.is_relay,
coordinator_capable = ?machine_announcement.is_coordinator,
reachable_via_count = machine_announcement.reachable_via.len(),
relay_candidate_count = machine_announcement.relay_candidates.len(),
"publishing machine announcement"
);
let machine_payload =
bytes::Bytes::from(bincode::serialize(&machine_announcement).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize machine announcement: {e}"
))
})?);
runtime
.pubsub()
.publish(
shard_topic_for_machine(&machine_announcement.machine_id),
machine_payload.clone(),
)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to publish machine announcement to shard topic: {e}"
)))
})?;
runtime
.pubsub()
.publish(MACHINE_ANNOUNCE_TOPIC.to_string(), machine_payload)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to publish machine announcement: {e}"
)))
})?;
let encoded = serialize_identity_announcement(&announcement).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize identity announcement: {e}"
))
})?;
let payload = bytes::Bytes::from(encoded);
let shard_topic = shard_topic_for_agent(&announcement.agent_id);
runtime
.pubsub()
.publish(shard_topic, payload.clone())
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to publish identity announcement to shard topic: {e}"
)))
})?;
runtime
.pubsub()
.publish(IDENTITY_ANNOUNCE_TOPIC.to_string(), payload)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to publish identity announcement: {e}"
)))
})?;
let now = Self::unix_timestamp_secs();
upsert_discovered_machine(
&self.machine_discovery_cache,
DiscoveredMachine::from_machine_announcement(
&machine_announcement,
machine_announcement.addresses.clone(),
now,
),
)
.await;
let discovered_agent = DiscoveredAgent {
agent_id: announcement.agent_id,
machine_id: announcement.machine_id,
user_id: announcement.user_id,
addresses: announcement.addresses.clone(),
announced_at: announcement.announced_at,
last_seen: now,
machine_public_key: announcement.machine_public_key.clone(),
nat_type: announcement.nat_type.clone(),
can_receive_direct: announcement.can_receive_direct,
is_relay: announcement.is_relay,
is_coordinator: announcement.is_coordinator,
reachable_via: announcement.reachable_via.clone(),
relay_candidates: announcement.relay_candidates.clone(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: announcement.agent_public_key.clone(),
};
upsert_discovered_machine_from_agent(&self.machine_discovery_cache, &discovered_agent)
.await;
upsert_discovered_agent(&self.identity_discovery_cache, discovered_agent).await;
if include_user_identity && human_consent {
self.user_identity_consented
.store(true, std::sync::atomic::Ordering::Release);
}
Ok(())
}
pub async fn discovered_agents(&self) -> error::Result<Vec<DiscoveredAgent>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
let mut agents: Vec<_> = self
.identity_discovery_cache
.read()
.await
.values()
.filter(|a| discovery_record_is_live(a.announced_at, a.last_seen, cutoff))
.cloned()
.collect();
agents.sort_by_key(|a| a.agent_id.0);
Ok(agents)
}
pub async fn online_agents(&self) -> error::Result<Vec<DiscoveredAgent>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
let cache = self.identity_discovery_cache.read().await;
let mut seen = std::collections::HashSet::new();
let mut agents = Vec::new();
for agent in cache
.values()
.filter(|agent| discovery_record_is_live(agent.announced_at, agent.last_seen, cutoff))
{
if seen.insert(agent.agent_id) {
agents.push(agent.clone());
}
}
if let Some(ref pw) = self.presence {
let records = pw
.manager()
.get_group_presence(crate::presence::global_presence_topic())
.await;
for (peer_id, record) in records {
if let Some(agent) =
crate::presence::presence_record_to_discovered_agent(peer_id, &record, &cache)
{
if seen.insert(agent.agent_id) {
agents.push(agent);
}
}
}
}
agents.sort_by_key(|a| a.agent_id.0);
Ok(agents)
}
pub async fn discovered_agents_unfiltered(&self) -> error::Result<Vec<DiscoveredAgent>> {
self.start_identity_listener().await?;
let mut agents: Vec<_> = self
.identity_discovery_cache
.read()
.await
.values()
.cloned()
.collect();
agents.sort_by_key(|a| a.agent_id.0);
Ok(agents)
}
pub async fn discovered_agent(
&self,
agent_id: identity::AgentId,
) -> error::Result<Option<DiscoveredAgent>> {
self.start_identity_listener().await?;
Ok(self
.identity_discovery_cache
.read()
.await
.get(&agent_id)
.cloned())
}
pub async fn discovered_machines(&self) -> error::Result<Vec<DiscoveredMachine>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
let mut machines: Vec<_> = self
.machine_discovery_cache
.read()
.await
.values()
.filter(|m| discovery_record_is_live(m.announced_at, m.last_seen, cutoff))
.cloned()
.collect();
machines.sort_by_key(|m| m.machine_id.0);
Ok(machines)
}
pub async fn discovered_machines_unfiltered(&self) -> error::Result<Vec<DiscoveredMachine>> {
self.start_identity_listener().await?;
let mut machines: Vec<_> = self
.machine_discovery_cache
.read()
.await
.values()
.cloned()
.collect();
machines.sort_by_key(|m| m.machine_id.0);
Ok(machines)
}
pub async fn discovered_machine(
&self,
machine_id: identity::MachineId,
) -> error::Result<Option<DiscoveredMachine>> {
self.start_identity_listener().await?;
Ok(self
.machine_discovery_cache
.read()
.await
.get(&machine_id)
.cloned())
}
pub async fn machine_for_agent(
&self,
agent_id: identity::AgentId,
) -> error::Result<Option<DiscoveredMachine>> {
self.start_identity_listener().await?;
let machine_id = {
let agents = self.identity_discovery_cache.read().await;
agents.get(&agent_id).map(|agent| agent.machine_id)
};
let Some(machine_id) = machine_id else {
return Ok(None);
};
Ok(self
.machine_discovery_cache
.read()
.await
.get(&machine_id)
.cloned())
}
pub async fn find_machines_by_user(
&self,
user_id: identity::UserId,
) -> error::Result<Vec<DiscoveredMachine>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
let mut machines: Vec<_> = self
.machine_discovery_cache
.read()
.await
.values()
.filter(|m| {
discovery_record_is_live(m.announced_at, m.last_seen, cutoff)
&& m.user_ids.contains(&user_id)
})
.cloned()
.collect();
machines.sort_by_key(|m| m.machine_id.0);
Ok(machines)
}
pub async fn announce_user_identity(&self, human_consent: bool) -> error::Result<()> {
if !human_consent {
return Err(error::IdentityError::Storage(std::io::Error::other(
"user announcement requires explicit human consent — set human_consent: true",
)));
}
let user_kp = self.identity.user_keypair().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"user announcement requested but no user identity is configured",
))
})?;
let own_cert = self.identity.agent_certificate().cloned().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"user announcement requested but agent certificate is missing",
))
})?;
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
self.start_identity_listener().await?;
let announced_at = Self::unix_timestamp_secs();
let announcement = UserAnnouncement::sign(user_kp, vec![own_cert], announced_at)?;
let payload = bytes::Bytes::from(bincode::serialize(&announcement).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize user announcement: {e}"
))
})?);
runtime
.pubsub()
.publish(shard_topic_for_user(&announcement.user_id), payload.clone())
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to publish user announcement to shard topic: {e}"
)))
})?;
runtime
.pubsub()
.publish(USER_ANNOUNCE_TOPIC.to_string(), payload)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to publish user announcement: {e}"
)))
})?;
let now = Self::unix_timestamp_secs();
let incoming = DiscoveredUser::from_announcement(&announcement, now);
self.user_discovery_cache
.write()
.await
.insert(incoming.user_id, incoming);
Ok(())
}
pub async fn discovered_user(
&self,
user_id: identity::UserId,
) -> error::Result<Option<DiscoveredUser>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
Ok(self
.user_discovery_cache
.read()
.await
.get(&user_id)
.filter(|u| discovery_record_is_live(u.announced_at, u.last_seen, cutoff))
.cloned())
}
pub async fn discovered_users(&self) -> error::Result<Vec<DiscoveredUser>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
let mut users: Vec<_> = self
.user_discovery_cache
.read()
.await
.values()
.filter(|u| discovery_record_is_live(u.announced_at, u.last_seen, cutoff))
.cloned()
.collect();
users.sort_by_key(|u| u.user_id.0);
Ok(users)
}
#[must_use]
pub async fn discovery_cache_entry_counts(&self) -> (usize, usize, usize) {
let id = self.identity_discovery_cache.read().await.len();
let mach = self.machine_discovery_cache.read().await.len();
let usr = self.user_discovery_cache.read().await.len();
(id, mach, usr)
}
async fn start_identity_listener(&self) -> error::Result<()> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
if self
.identity_listener_started
.swap(true, std::sync::atomic::Ordering::AcqRel)
{
return Ok(());
}
let mut sub_legacy = runtime
.pubsub()
.subscribe(IDENTITY_ANNOUNCE_TOPIC.to_string())
.await;
let own_shard_topic = shard_topic_for_agent(&self.agent_id());
let mut sub_shard = runtime.pubsub().subscribe(own_shard_topic).await;
let mut sub_machine_legacy = runtime
.pubsub()
.subscribe(MACHINE_ANNOUNCE_TOPIC.to_string())
.await;
let own_machine_shard_topic = shard_topic_for_machine(&self.machine_id());
let mut sub_machine_shard = runtime.pubsub().subscribe(own_machine_shard_topic).await;
let mut sub_user_legacy = runtime
.pubsub()
.subscribe(USER_ANNOUNCE_TOPIC.to_string())
.await;
let mut sub_user_shard = match self.user_id() {
Some(uid) => Some(runtime.pubsub().subscribe(shard_topic_for_user(&uid)).await),
None => None,
};
let cache = std::sync::Arc::clone(&self.identity_discovery_cache);
let authenticated_machine_bindings =
std::sync::Arc::clone(&self.authenticated_machine_bindings);
let machine_cache = std::sync::Arc::clone(&self.machine_discovery_cache);
let user_cache = std::sync::Arc::clone(&self.user_discovery_cache);
let bootstrap_cache = self.bootstrap_cache.clone();
let contact_store = std::sync::Arc::clone(&self.contact_store);
let direct_messaging = std::sync::Arc::clone(&self.direct_messaging);
let network = self.network.as_ref().map(std::sync::Arc::clone);
let allow_local_scope = network
.as_ref()
.is_some_and(|network| allow_local_discovery_addresses(network.config()));
let own_agent_id = self.agent_id();
let own_machine_id = self.machine_id();
let own_user_id = self.user_id();
let rebroadcast_pubsub = std::sync::Arc::clone(runtime.pubsub());
let token = self.shutdown_token.clone();
let mut sub_revocation = runtime
.pubsub()
.subscribe(REVOCATION_TOPIC.to_string())
.await;
let revocation_set = std::sync::Arc::clone(&self.revocation_set);
let identity_dir_for_listener = self.identity_dir.clone();
let contact_store_for_evict = std::sync::Arc::clone(&self.contact_store);
self.spawn_tracked(async move {
enum DiscoveryMessage {
Identity(crate::gossip::PubSubMessage),
Machine(crate::gossip::PubSubMessage),
User(crate::gossip::PubSubMessage),
Revocation(crate::gossip::PubSubMessage),
}
let mut auto_connect_attempts =
std::collections::HashMap::<identity::AgentId, std::time::Instant>::new();
let mut rebroadcast_state: std::collections::HashMap<
(identity::AgentId, u64),
std::time::Instant,
> = std::collections::HashMap::new();
let mut machine_rebroadcast_state: std::collections::HashMap<
(identity::MachineId, u64),
std::time::Instant,
> = std::collections::HashMap::new();
let mut seen_identity_payloads: std::collections::HashMap<
blake3::Hash,
std::time::Instant,
> = std::collections::HashMap::new();
let mut seen_machine_payloads: std::collections::HashMap<
blake3::Hash,
std::time::Instant,
> = std::collections::HashMap::new();
let mut user_rebroadcast_state: std::collections::HashMap<
(identity::UserId, u64),
std::time::Instant,
> = std::collections::HashMap::new();
const VERIFIED_PAYLOAD_TTL: std::time::Duration = std::time::Duration::from_secs(60);
let has_recent_verified_payload =
|seen: &std::collections::HashMap<blake3::Hash, std::time::Instant>,
payload: &[u8]| {
let key = blake3::hash(payload);
matches!(seen.get(&key), Some(last) if last.elapsed() < VERIFIED_PAYLOAD_TTL)
};
let remember_verified_payload =
|seen: &mut std::collections::HashMap<blake3::Hash, std::time::Instant>,
payload: &[u8]| {
let now = std::time::Instant::now();
seen.insert(blake3::hash(payload), now);
if seen.len() > 4096 {
let cutoff = now - VERIFIED_PAYLOAD_TTL;
seen.retain(|_, t| *t >= cutoff);
}
};
loop {
let msg = tokio::select! {
Some(m) = sub_legacy.recv() => DiscoveryMessage::Identity(m),
Some(m) = sub_shard.recv() => DiscoveryMessage::Identity(m),
Some(m) = sub_machine_legacy.recv() => DiscoveryMessage::Machine(m),
Some(m) = sub_machine_shard.recv() => DiscoveryMessage::Machine(m),
Some(m) = sub_user_legacy.recv() => DiscoveryMessage::User(m),
Some(m) = async {
match sub_user_shard.as_mut() {
Some(s) => s.recv().await,
None => std::future::pending().await,
}
} => DiscoveryMessage::User(m),
Some(m) = sub_revocation.recv() => DiscoveryMessage::Revocation(m),
_ = token.cancelled() => break,
else => break,
};
let msg = match msg {
DiscoveryMessage::Machine(msg) => {
let raw_payload = msg.payload.clone();
let already_verified =
has_recent_verified_payload(&seen_machine_payloads, &raw_payload);
let announcement = match deserialize_machine_announcement(&raw_payload) {
Ok(a) => a,
Err(e) => {
tracing::debug!(
"Ignoring invalid machine announcement payload: {}",
e
);
continue;
}
};
if !already_verified {
if let Err(e) = announcement.verify() {
tracing::warn!("Ignoring unverifiable machine announcement: {}", e);
continue;
}
remember_verified_payload(&mut seen_machine_payloads, &raw_payload);
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let bootstrap_addresses = filter_publicly_advertisable_addrs(
announcement.addresses.iter().copied(),
);
if !bootstrap_addresses.is_empty() {
if let Some(ref bc) = &bootstrap_cache {
let peer_id = ant_quic::PeerId(announcement.machine_id.0);
bc.add_from_connection(peer_id, bootstrap_addresses.clone(), None)
.await;
}
}
let discovery_addresses = filter_discovery_announcement_addrs(
announcement.addresses.iter().copied(),
allow_local_scope,
);
let filtered_addr_count = discovery_addresses.len();
upsert_discovered_machine(
&machine_cache,
DiscoveredMachine::from_machine_announcement(
&announcement,
discovery_addresses,
now,
),
)
.await;
tracing::debug!(
target: "x0x::discovery",
announcement_kind = "machine_received",
machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
addr_total = announcement.addresses.len(),
filtered_addr_count,
nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
can_receive_direct = ?announcement.can_receive_direct,
relay_capable = ?announcement.is_relay,
coordinator_capable = ?announcement.is_coordinator,
"cached verified machine announcement"
);
if announcement.machine_id != own_machine_id {
let key = (announcement.machine_id, announcement.announced_at);
if should_rebroadcast_discovery_once(
&mut machine_rebroadcast_state,
key,
std::time::Instant::now(),
) {
let pubsub = std::sync::Arc::clone(&rebroadcast_pubsub);
tokio::spawn(async move {
if let Err(e) = pubsub
.publish(MACHINE_ANNOUNCE_TOPIC.to_string(), raw_payload)
.await
{
tracing::debug!(
"machine announcement re-broadcast failed: {e}"
);
}
});
}
}
continue;
}
DiscoveryMessage::User(msg) => {
let raw_payload = msg.payload.clone();
let announcement = match deserialize_user_announcement(&raw_payload) {
Ok(a) => a,
Err(e) => {
tracing::debug!(
"Ignoring invalid user announcement payload: {}",
e
);
continue;
}
};
if let Err(e) = announcement.verify() {
tracing::warn!("Ignoring unverifiable user announcement: {}", e);
continue;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let incoming = DiscoveredUser::from_announcement(&announcement, now);
{
let mut cache = user_cache.write().await;
match cache.get_mut(&incoming.user_id) {
Some(existing) if incoming.announced_at < existing.announced_at => {
}
Some(existing) => {
existing.user_public_key = incoming.user_public_key;
existing.agent_certificates = incoming.agent_certificates;
existing.agent_ids = incoming.agent_ids;
existing.announced_at = incoming.announced_at;
existing.last_seen = now;
}
None => {
cache.insert(incoming.user_id, incoming);
}
}
}
tracing::debug!(
target: "x0x::discovery",
announcement_kind = "user_received",
user_prefix = %network::hex_prefix(&announcement.user_id.0, 4),
agent_count = announcement.agent_certificates.len(),
"cached verified user announcement"
);
if Some(announcement.user_id) != own_user_id {
let key = (announcement.user_id, announcement.announced_at);
if should_rebroadcast_discovery_once(
&mut user_rebroadcast_state,
key,
std::time::Instant::now(),
) {
let pubsub = std::sync::Arc::clone(&rebroadcast_pubsub);
tokio::spawn(async move {
if let Err(e) = pubsub
.publish(USER_ANNOUNCE_TOPIC.to_string(), raw_payload)
.await
{
tracing::debug!(
"user announcement re-broadcast failed: {e}"
);
}
});
}
}
continue;
}
DiscoveryMessage::Identity(msg) => msg,
DiscoveryMessage::Revocation(msg) => {
const MAX_REVOCATION_PAYLOAD_BYTES: usize = 2 * 1024 * 1024;
if msg.payload.len() > MAX_REVOCATION_PAYLOAD_BYTES {
tracing::debug!(
"ignoring oversized revocation payload ({} bytes)",
msg.payload.len()
);
continue;
}
let records: Vec<revocation::RevocationRecord> =
match bincode::deserialize(&msg.payload) {
Ok(r) => r,
Err(e) => {
tracing::debug!("ignoring invalid revocation payload: {e}");
continue;
}
};
let mut newly_inserted = Vec::new();
let subject_certs = collect_subject_certs(&*cache.read().await);
{
let mut set = revocation_set.write().await;
for record in records {
if set.contains_hash(&record.record_hash()) {
continue; }
let subject_cert = match &record.subject {
revocation::RevokedSubject::Agent(agent_id) => {
subject_certs.get(agent_id)
}
_ => None,
};
match set.verify_and_insert(record.clone(), subject_cert) {
Ok(true) => newly_inserted.push(record),
Ok(false) => {} Err(e) => {
tracing::debug!(
"revocation record rejected: {e}"
);
}
}
}
}
if !newly_inserted.is_empty() {
let persisted_bytes = revocation_set.read().await.to_bytes();
let id_dir = identity_dir_for_listener.clone();
tokio::spawn(async move {
match persisted_bytes {
Ok(bytes) => {
if let Err(e) = storage::save_revocation_set_bytes(
bytes,
id_dir.as_deref(),
)
.await
{
tracing::warn!(
"failed to persist revocation set: {e}"
);
}
}
Err(e) => tracing::warn!(
"failed to encode revocation set for persistence: {e}"
),
}
});
for record in newly_inserted {
match &record.subject {
revocation::RevokedSubject::Agent(agent_id) => {
if let Some(entry) = cache.write().await.remove(agent_id) {
machine_cache.write().await.remove(&entry.machine_id);
}
let mut cs = contact_store_for_evict.write().await;
cs.set_trust(agent_id, contacts::TrustLevel::Blocked);
tracing::info!(
agent = %hex::encode(agent_id.as_bytes()),
"evicted revoked agent (received via gossip)"
);
}
revocation::RevokedSubject::Machine(machine_id) => {
machine_cache.write().await.remove(machine_id);
cache.write().await.retain(|_, a| a.machine_id != *machine_id);
tracing::info!(
machine = %hex::encode(machine_id.as_bytes()),
"evicted revoked machine (received via gossip)"
);
}
}
}
}
continue;
}
};
let raw_payload = msg.payload.clone();
let already_verified =
has_recent_verified_payload(&seen_identity_payloads, &raw_payload);
let announcement = match deserialize_identity_announcement(&raw_payload) {
Ok(a) => a,
Err(e) => {
tracing::debug!("Ignoring invalid identity announcement payload: {}", e);
continue;
}
};
if !already_verified {
if let Err(e) = announcement.verify() {
tracing::warn!("Ignoring unverifiable identity announcement: {}", e);
continue;
}
remember_verified_payload(&mut seen_identity_payloads, &raw_payload);
}
let now = Agent::unix_timestamp_secs();
if !identity_announcement_timestamp_is_acceptable(announcement.announced_at, now) {
tracing::warn!(
agent = %hex::encode(announcement.agent_id.as_bytes()),
announced_at = announcement.announced_at,
now,
max_future_skew_secs = IDENTITY_ANNOUNCEMENT_CLOCK_SKEW_SECS,
"ignoring far-future identity announcement"
);
continue;
}
{
let store = contact_store.read().await;
let evaluator = trust::TrustEvaluator::new(&store);
let decision = evaluator.evaluate(&trust::TrustContext {
agent_id: &announcement.agent_id,
machine_id: &announcement.machine_id,
});
match decision {
trust::TrustDecision::RejectBlocked => {
tracing::debug!(
"Dropping identity announcement from blocked agent {:?}",
hex::encode(&announcement.agent_id.0[..8]),
);
continue;
}
trust::TrustDecision::RejectMachineMismatch => {
tracing::warn!(
"Dropping identity announcement from agent {}: machine {} not in pinned list",
crate::logging::LogAgentId::from(&announcement.agent_id),
crate::logging::LogMachineId::from(&announcement.machine_id),
);
continue;
}
_ => {}
}
}
{
let revoked = revocation_set.read().await;
if revoked.is_agent_revoked(&announcement.agent_id) {
tracing::debug!(
"Dropping identity announcement from revoked agent {:?}",
hex::encode(&announcement.agent_id.0[..8]),
);
continue;
}
if revoked.is_machine_revoked(&announcement.machine_id) {
tracing::debug!(
"Dropping identity announcement from revoked machine {:?}",
hex::encode(&announcement.machine_id.0[..8]),
);
continue;
}
}
if let Some(cert) = &announcement.agent_certificate {
if identity::is_expired(cert.not_after(), Agent::unix_timestamp_secs()) {
tracing::debug!(
"Dropping identity announcement with expired cert from agent {:?}",
hex::encode(&announcement.agent_id.0[..8]),
);
continue;
}
}
register_announced_machine(
&contact_store,
own_agent_id,
announcement.agent_id,
announcement.machine_id,
)
.await;
let bootstrap_addresses =
filter_publicly_advertisable_addrs(announcement.addresses.iter().copied());
let discovery_addresses = filter_discovery_announcement_addrs(
announcement.addresses.iter().copied(),
allow_local_scope,
);
let filtered_addr_count = discovery_addresses.len();
let auto_connect_addresses = discovery_addresses.clone();
{
if !bootstrap_addresses.is_empty() {
if let Some(ref bc) = &bootstrap_cache {
let peer_id = ant_quic::PeerId(announcement.machine_id.0);
bc.add_from_connection(peer_id, bootstrap_addresses.clone(), None)
.await;
tracing::debug!(
"Added {} public addresses to bootstrap cache for agent {:?} (machine {:?})",
bootstrap_addresses.len(),
announcement.agent_id,
hex::encode(&announcement.machine_id.0[..8]),
);
}
}
}
let cert_not_after = announcement
.agent_certificate
.as_ref()
.and_then(|c| c.not_after());
let discovered_agent = DiscoveredAgent {
agent_id: announcement.agent_id,
machine_id: announcement.machine_id,
user_id: announcement.user_id,
addresses: discovery_addresses,
announced_at: announcement.announced_at,
last_seen: now,
machine_public_key: announcement.machine_public_key.clone(),
nat_type: announcement.nat_type.clone(),
can_receive_direct: announcement.can_receive_direct,
is_relay: announcement.is_relay,
is_coordinator: announcement.is_coordinator,
reachable_via: announcement.reachable_via.clone(),
relay_candidates: announcement.relay_candidates.clone(),
cert_not_after,
agent_certificate: announcement.agent_certificate.clone(),
agent_public_key: announcement.agent_public_key.clone(),
};
record_authenticated_machine_binding_from_message(
&authenticated_machine_bindings,
&msg,
&announcement,
now,
)
.await;
upsert_discovered_machine_from_agent(&machine_cache, &discovered_agent).await;
upsert_discovered_agent(&cache, discovered_agent).await;
tracing::debug!(
target: "x0x::discovery",
announcement_kind = "received",
agent_prefix = %network::hex_prefix(&announcement.agent_id.0, 4),
machine_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
addr_total = announcement.addresses.len(),
filtered_addr_count,
nat_type = announcement.nat_type.as_deref().unwrap_or("unknown"),
can_receive_direct = ?announcement.can_receive_direct,
relay_capable = ?announcement.is_relay,
coordinator_capable = ?announcement.is_coordinator,
reachable_via_count = announcement.reachable_via.len(),
relay_candidate_count = announcement.relay_candidates.len(),
"cached verified identity announcement"
);
direct_messaging
.register_agent(announcement.agent_id, announcement.machine_id)
.await;
if announcement.agent_id != own_agent_id {
let key = (announcement.agent_id, announcement.announced_at);
if should_rebroadcast_discovery_once(
&mut rebroadcast_state,
key,
std::time::Instant::now(),
) {
let pubsub = std::sync::Arc::clone(&rebroadcast_pubsub);
let payload = raw_payload.clone();
tokio::spawn(async move {
if let Err(e) = pubsub
.publish(IDENTITY_ANNOUNCE_TOPIC.to_string(), payload)
.await
{
tracing::debug!("identity announcement re-broadcast failed: {e}");
}
});
}
}
if let Some(ref net) = &network {
let ant_peer_id = ant_quic::PeerId(announcement.machine_id.0);
if net.is_connected(&ant_peer_id).await {
direct_messaging
.mark_connected(announcement.agent_id, announcement.machine_id)
.await;
}
}
if announcement.agent_id != own_agent_id
&& !auto_connect_addresses.is_empty()
&& announcement_auto_connect_retry_allowed(
auto_connect_attempts.get(&announcement.agent_id).copied(),
std::time::Instant::now(),
)
{
if let Some(net) = &network {
let ant_peer = ant_quic::PeerId(announcement.machine_id.0);
let suppressed = net.is_reconnect_suppressed(announcement.machine_id.0);
let connected = net.is_connected(&ant_peer).await;
if announcement_should_auto_connect(suppressed, connected) {
auto_connect_attempts
.insert(announcement.agent_id, std::time::Instant::now());
let net = std::sync::Arc::clone(net);
let addresses = auto_connect_addresses.clone();
tokio::spawn(async move {
for addr in &addresses {
match net.connect_addr(*addr).await {
Ok(_) => {
tracing::info!(
"Auto-connected to discovered agent at {addr}",
);
return;
}
Err(e) => {
tracing::debug!("Auto-connect to {addr} failed: {e}",);
}
}
}
tracing::debug!(
"Auto-connect exhausted all {} addresses for discovered agent",
addresses.len(),
);
});
} else if suppressed {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %network::hex_prefix(&announcement.machine_id.0, 4),
"skipping announcement auto-connect for reconnect-suppressed peer",
);
}
}
}
}
});
Ok(())
}
fn unix_timestamp_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
fn announcement_addresses(&self) -> Vec<std::net::SocketAddr> {
match self.network.as_ref().and_then(|n| n.local_addr()) {
Some(addr) if addr.port() > 0 => filter_discovery_announcement_addrs(
collect_local_interface_addrs(addr.port()),
self.network
.as_ref()
.is_some_and(|network| allow_local_discovery_addresses(network.config())),
),
_ => Vec::new(),
}
}
fn build_identity_announcement(
&self,
include_user_identity: bool,
human_consent: bool,
) -> error::Result<IdentityAnnouncement> {
self.build_identity_announcement_with_addrs(IdentityAnnouncementBuildOptions {
include_user_identity,
human_consent,
addresses: self.announcement_addresses(),
assist_snapshot: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
allow_local_scope: false,
})
}
fn build_identity_announcement_with_addrs(
&self,
options: IdentityAnnouncementBuildOptions<'_>,
) -> error::Result<IdentityAnnouncement> {
let IdentityAnnouncementBuildOptions {
include_user_identity,
human_consent,
addresses,
assist_snapshot,
reachable_via,
relay_candidates,
allow_local_scope,
} = options;
if include_user_identity && !human_consent {
return Err(error::IdentityError::Storage(std::io::Error::other(
"human identity disclosure requires explicit human consent — set human_consent: true in the request body",
)));
}
let addresses = filter_discovery_announcement_addrs(addresses, allow_local_scope);
let (user_id, agent_certificate) = if include_user_identity {
let user_id = self.user_id().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"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",
))
})?;
let cert = self.agent_certificate().cloned().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"human identity disclosure requested but agent certificate is missing",
))
})?;
(Some(user_id), Some(cert))
} else {
(None, None)
};
let machine_public_key = self
.identity
.machine_keypair()
.public_key()
.as_bytes()
.to_vec();
let unsigned = IdentityAnnouncementUnsigned {
agent_id: self.agent_id(),
machine_id: self.machine_id(),
user_id,
agent_certificate: agent_certificate.clone(),
machine_public_key: machine_public_key.clone(),
addresses,
announced_at: Self::unix_timestamp_secs(),
nat_type: assist_snapshot.and_then(|snapshot| snapshot.nat_type.clone()),
can_receive_direct: assist_snapshot.and_then(|snapshot| snapshot.can_receive_direct),
is_relay: assist_snapshot.and_then(|snapshot| snapshot.relay_capable),
is_coordinator: assist_snapshot.and_then(|snapshot| snapshot.coordinator_capable),
reachable_via,
relay_candidates,
};
let unsigned_bytes = bincode::serialize(&unsigned).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize unsigned identity announcement: {e}"
))
})?;
let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
self.identity.machine_keypair().secret_key(),
&unsigned_bytes,
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to sign identity announcement with machine key: {:?}",
e
)))
})?
.as_bytes()
.to_vec();
Ok(IdentityAnnouncement {
agent_id: unsigned.agent_id,
machine_id: unsigned.machine_id,
user_id: unsigned.user_id,
agent_certificate: unsigned.agent_certificate,
machine_public_key,
machine_signature,
addresses: unsigned.addresses,
announced_at: unsigned.announced_at,
nat_type: unsigned.nat_type,
can_receive_direct: unsigned.can_receive_direct,
is_relay: unsigned.is_relay,
is_coordinator: unsigned.is_coordinator,
reachable_via: unsigned.reachable_via,
relay_candidates: unsigned.relay_candidates,
agent_public_key: self
.identity
.agent_keypair()
.public_key()
.as_bytes()
.to_vec(),
})
}
pub async fn join_network(&self) -> error::Result<()> {
let Some(network) = self.network.as_ref() else {
tracing::debug!("join_network called but no network configured");
return Ok(());
};
if let Some(ref runtime) = self.gossip_runtime {
runtime.start().await.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to start gossip runtime: {e}"
)))
})?;
tracing::info!("Gossip runtime started");
}
if self.shutdown_token.is_cancelled() {
tracing::info!("join_network aborted: shutdown already in progress");
return Ok(());
}
self.start_identity_listener().await?;
self.start_network_event_listener();
self.start_direct_listener();
self.start_stream_accept_loop();
let bootstrap_nodes = network.config().bootstrap_nodes.clone();
let min_connected = 3;
let mut all_connected: Vec<std::net::SocketAddr> = Vec::new();
if let Some(ref cache) = self.bootstrap_cache {
let coordinators = cache.select_coordinators(6).await;
let coordinator_addrs: Vec<std::net::SocketAddr> = coordinators
.iter()
.flat_map(|peer| peer.preferred_addresses())
.collect();
if !coordinator_addrs.is_empty() {
tracing::info!(
"Phase 0: Trying {} addresses from {} cached coordinators",
coordinator_addrs.len(),
coordinators.len()
);
let (succeeded, _failed) = self
.connect_peers_parallel_tracked(network, &coordinator_addrs)
.await;
all_connected.extend(&succeeded);
tracing::info!(
"Phase 0: {}/{} coordinator addresses connected",
succeeded.len(),
coordinator_addrs.len()
);
}
}
if all_connected.len() < min_connected {
if let Some(ref cache) = self.bootstrap_cache {
const PHASE1_PEER_CANDIDATES: usize = 12;
let cached_peers = cache.select_peers(PHASE1_PEER_CANDIDATES).await;
if !cached_peers.is_empty() {
tracing::info!("Phase 1: Trying {} cached peers", cached_peers.len());
let (succeeded, _failed) = self
.connect_cached_peers_parallel_tracked(network, &cached_peers)
.await;
all_connected.extend(&succeeded);
tracing::info!(
"Phase 1: {}/{} cached peers connected",
succeeded.len(),
cached_peers.len()
);
}
}
}
if all_connected.len() < min_connected && !bootstrap_nodes.is_empty() {
let remaining: Vec<std::net::SocketAddr> = bootstrap_nodes
.iter()
.filter(|addr| !all_connected.contains(addr))
.copied()
.collect();
let (succeeded, mut failed) = self
.connect_peers_parallel_tracked(network, &remaining)
.await;
all_connected.extend(&succeeded);
tracing::info!(
"Phase 2 round 1: {}/{} bootstrap peers connected",
succeeded.len(),
remaining.len()
);
for round in 2..=3 {
if failed.is_empty() {
break;
}
let delay = std::time::Duration::from_secs(if round == 2 { 10 } else { 15 });
tracing::info!(
"Retrying {} failed peers in {}s (round {})",
failed.len(),
delay.as_secs(),
round
);
tokio::time::sleep(delay).await;
let (succeeded, still_failed) =
self.connect_peers_parallel_tracked(network, &failed).await;
all_connected.extend(&succeeded);
failed = still_failed;
tracing::info!(
"Phase 2 round {}: {} total peers connected",
round,
all_connected.len()
);
}
if !failed.is_empty() {
tracing::warn!(
"Could not connect to {} bootstrap peers: {:?}",
failed.len(),
failed
);
}
}
tracing::info!(
"Network join complete. Connected to {} peers.",
all_connected.len()
);
if let Some(ref runtime) = self.gossip_runtime {
let seeds: Vec<String> = all_connected.iter().map(|addr| addr.to_string()).collect();
if !seeds.is_empty() {
if let Err(e) = runtime.membership().join(seeds).await {
tracing::warn!("HyParView membership join failed: {e}");
}
}
}
if let Some(ref pw) = self.presence {
if let Some(ref runtime) = self.gossip_runtime {
let active = runtime.membership().active_view();
let active_view_count = active.len();
let mut broadcast_peers = active;
let mut connected_peer_count = 0usize;
if let Some(ref net) = self.network {
let connected = net.connected_peers().await;
connected_peer_count = connected.len();
broadcast_peers.extend(
connected
.into_iter()
.map(|peer| saorsa_gossip_types::PeerId::new(peer.0)),
);
}
pw.manager().replace_broadcast_peers(broadcast_peers).await;
let broadcast_peer_count = pw.manager().broadcast_peer_count().await;
tracing::info!(
active_view_count,
connected_peer_count,
broadcast_peer_count,
"Presence seeded broadcast peers"
);
let pw_clone = pw.clone();
let runtime_clone = runtime.clone();
let network_clone = self.network.as_ref().map(std::sync::Arc::clone);
let token = self.shutdown_token.clone();
self.spawn_tracked(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
interval.tick().await; loop {
tokio::select! {
_ = interval.tick() => {}
_ = token.cancelled() => break,
}
let active = runtime_clone.membership().active_view();
let active_view_count = active.len();
let mut broadcast_peers = active;
let mut connected_peer_count = 0usize;
if let Some(ref net) = network_clone {
let connected = net.connected_peers().await;
connected_peer_count = connected.len();
broadcast_peers.extend(
connected
.into_iter()
.map(|peer| saorsa_gossip_types::PeerId::new(peer.0)),
);
}
pw_clone
.manager()
.replace_broadcast_peers(broadcast_peers)
.await;
let broadcast_peer_count = pw_clone.manager().broadcast_peer_count().await;
tracing::info!(
active_view_count,
connected_peer_count,
broadcast_peer_count,
"Presence broadcast peer refresh"
);
}
});
}
if let Some(ref net) = self.network {
if let Some(status) = net.node_status().await {
let hints: Vec<String> = status
.external_addrs
.iter()
.filter(|a| is_publicly_advertisable(**a))
.map(|a| a.to_string())
.collect();
pw.manager().set_addr_hints(hints).await;
}
}
if !self.shutdown_token.is_cancelled() {
if pw.config().enable_beacons {
if let Err(e) = pw
.manager()
.start_beacons(pw.config().beacon_interval_secs)
.await
{
tracing::warn!("Failed to start presence beacons: {e}");
} else {
tracing::info!(
"Presence beacons started (interval={}s)",
pw.config().beacon_interval_secs
);
}
}
pw.start_event_loop(std::sync::Arc::clone(&self.identity_discovery_cache))
.await;
tracing::debug!("Presence event loop started");
}
}
if let Err(e) = self.announce_identity(false, false).await {
tracing::warn!("Initial identity announcement failed: {}", e);
}
if let Err(e) = self.start_identity_heartbeat().await {
tracing::warn!("Failed to start identity heartbeat: {e}");
}
if let Err(e) = self.start_discovery_cache_reaper().await {
tracing::warn!("Failed to start discovery cache reaper: {e}");
}
if let (Some(ref runtime), Some(ref network)) = (&self.gossip_runtime, &self.network) {
let ctx = HeartbeatContext {
identity: std::sync::Arc::clone(&self.identity),
runtime: std::sync::Arc::clone(runtime),
network: std::sync::Arc::clone(network),
interval_secs: self.heartbeat_interval_secs,
cache: std::sync::Arc::clone(&self.identity_discovery_cache),
machine_cache: std::sync::Arc::clone(&self.machine_discovery_cache),
user_identity_consented: std::sync::Arc::clone(&self.user_identity_consented),
allow_local_discovery_addrs: allow_local_discovery_addresses(network.config()),
revocation_set: std::sync::Arc::clone(&self.revocation_set),
};
self.spawn_tracked(async move {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
if let Err(e) = ctx.announce().await {
tracing::warn!("Delayed identity re-announcement failed: {e}");
} else {
tracing::info!(
"Delayed identity re-announcement sent (gossip mesh stabilized)"
);
}
});
}
if let Err(e) = self.start_capability_advert_service().await {
tracing::warn!("failed to start capability advert service: {e}");
}
Ok(())
}
#[must_use]
pub fn capability_store(&self) -> std::sync::Arc<dm_capability::CapabilityStore> {
std::sync::Arc::clone(&self.capability_store)
}
pub async fn start_capability_advert_service(&self) -> error::Result<()> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"cannot start capability advert service: no gossip runtime configured",
))
})?;
let signing = std::sync::Arc::new(gossip::SigningContext::from_keypair(
self.identity.agent_keypair(),
));
let caps_rx = self.dm_capabilities_tx.subscribe();
let service = dm_capability_service::CapabilityAdvertService::spawn_default(
std::sync::Arc::clone(runtime.pubsub()),
signing,
self.identity.agent_id(),
self.identity.machine_id(),
caps_rx,
std::sync::Arc::clone(&self.capability_store),
)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"capability advert service spawn failed: {e}"
)))
})?;
let mut guard = self.capability_advert_service.lock().await;
if self.shutdown_token.is_cancelled() {
service.abort();
return Ok(());
}
if let Some(prev) = guard.take() {
prev.abort();
}
*guard = Some(service);
tracing::info!("Capability advert service started");
Ok(())
}
#[must_use]
pub fn dm_inflight_acks(&self) -> std::sync::Arc<dm::InFlightAcks> {
std::sync::Arc::clone(&self.dm_inflight_acks)
}
#[must_use]
pub fn recent_delivery_cache(&self) -> std::sync::Arc<dm::RecentDeliveryCache> {
std::sync::Arc::clone(&self.recent_delivery_cache)
}
pub async fn start_dm_inbox(
&self,
kem_keypair: std::sync::Arc<groups::kem_envelope::AgentKemKeypair>,
config: dm_inbox::DmInboxConfig,
) -> error::Result<()> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"cannot start DM inbox: no gossip runtime configured",
))
})?;
let signing = std::sync::Arc::new(gossip::SigningContext::from_keypair(
self.identity.agent_keypair(),
));
let service = dm_inbox::DmInboxService::spawn(
std::sync::Arc::clone(runtime.pubsub()),
signing,
self.identity.agent_id(),
self.identity.machine_id(),
std::sync::Arc::clone(&kem_keypair),
std::sync::Arc::clone(&self.direct_messaging),
std::sync::Arc::clone(&self.contact_store),
std::sync::Arc::clone(&self.dm_inflight_acks),
std::sync::Arc::clone(&self.recent_delivery_cache),
config,
std::sync::Arc::clone(&self.revocation_set),
std::sync::Arc::clone(&self.authenticated_machine_bindings),
)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"DM inbox spawn failed: {e}"
)))
})?;
let mut guard = self.dm_inbox_service.lock().await;
if self.shutdown_token.is_cancelled() {
service.abort();
return Ok(());
}
if let Some(prev) = guard.take() {
prev.abort();
}
*guard = Some(service);
let upgraded =
dm::DmCapabilities::pending().with_kem_public_key(kem_keypair.public_bytes.clone());
self.dm_capabilities_tx.send_replace(upgraded);
tracing::info!("DM inbox service started");
Ok(())
}
pub async fn stop_dm_inbox(&self) {
let mut guard = self.dm_inbox_service.lock().await;
if let Some(service) = guard.take() {
service.abort();
}
}
async fn connect_cached_peers_parallel_tracked(
&self,
network: &std::sync::Arc<network::NetworkNode>,
peers: &[ant_quic::CachedPeer],
) -> (Vec<std::net::SocketAddr>, Vec<ant_quic::PeerId>) {
use tokio::time::{timeout, Duration};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
let handles: Vec<_> = peers
.iter()
.map(|peer| {
let net = network.clone();
let peer_id = peer.peer_id;
tokio::spawn(async move {
tracing::debug!("Connecting to cached peer: {:?}", peer_id);
match timeout(CONNECT_TIMEOUT, net.connect_cached_peer(peer_id)).await {
Ok(Ok(addr)) => {
tracing::info!("Connected to cached peer {:?} at {}", peer_id, addr);
Ok(addr)
}
Ok(Err(e)) => {
tracing::warn!("Failed to connect to cached peer {:?}: {}", peer_id, e);
Err(peer_id)
}
Err(_) => {
tracing::warn!(
"Connection to cached peer {:?} timed out after {}s",
peer_id,
CONNECT_TIMEOUT.as_secs()
);
Err(peer_id)
}
}
})
})
.collect();
let mut succeeded = Vec::new();
let mut failed = Vec::new();
for handle in handles {
match handle.await {
Ok(Ok(addr)) => succeeded.push(addr),
Ok(Err(peer_id)) => failed.push(peer_id),
Err(e) => tracing::error!("Connection task panicked: {}", e),
}
}
(succeeded, failed)
}
async fn connect_peers_parallel_tracked(
&self,
network: &std::sync::Arc<network::NetworkNode>,
addrs: &[std::net::SocketAddr],
) -> (Vec<std::net::SocketAddr>, Vec<std::net::SocketAddr>) {
use tokio::time::{timeout, Duration};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
let handles: Vec<_> = addrs
.iter()
.map(|addr| {
let net = network.clone();
let addr = *addr;
tokio::spawn(async move {
tracing::debug!("Connecting to peer: {}", addr);
match timeout(CONNECT_TIMEOUT, net.connect_addr(addr)).await {
Ok(Ok(_)) => {
tracing::info!("Connected to peer: {}", addr);
Ok(addr)
}
Ok(Err(e)) => {
tracing::warn!("Failed to connect to {}: {}", addr, e);
Err(addr)
}
Err(_) => {
tracing::warn!(
"Connection to {} timed out after {}s",
addr,
CONNECT_TIMEOUT.as_secs()
);
Err(addr)
}
}
})
})
.collect();
let mut succeeded = Vec::new();
let mut failed = Vec::new();
for handle in handles {
match handle.await {
Ok(Ok(addr)) => succeeded.push(addr),
Ok(Err(addr)) => failed.push(addr),
Err(e) => tracing::error!("Connection task panicked: {}", e),
}
}
(succeeded, failed)
}
pub async fn subscribe(&self, topic: &str) -> error::Result<Subscription> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
Ok(runtime.pubsub().subscribe(topic.to_string()).await)
}
pub async fn publish(&self, topic: &str, payload: Vec<u8>) -> error::Result<()> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
runtime
.pubsub()
.publish(topic.to_string(), bytes::Bytes::from(payload))
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"publish failed: {}",
e
)))
})
}
pub async fn peers(&self) -> error::Result<Vec<saorsa_gossip_types::PeerId>> {
let network = self.network.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"network not initialized - configure agent with network first",
))
})?;
let ant_peers = network.connected_peers().await;
Ok(ant_peers
.into_iter()
.map(|p| saorsa_gossip_types::PeerId::new(p.0))
.collect())
}
pub async fn presence(&self) -> error::Result<Vec<identity::AgentId>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
let mut agents: Vec<_> = self
.identity_discovery_cache
.read()
.await
.values()
.filter(|a| discovery_record_is_live(a.announced_at, a.last_seen, cutoff))
.map(|a| a.agent_id)
.collect();
agents.sort_by_key(|a| a.0);
Ok(agents)
}
pub async fn subscribe_presence(
&self,
) -> error::NetworkResult<tokio::sync::broadcast::Receiver<presence::PresenceEvent>> {
let pw = self.presence.as_ref().ok_or_else(|| {
error::NetworkError::NodeError("presence system not initialized".to_string())
})?;
pw.start_event_loop(std::sync::Arc::clone(&self.identity_discovery_cache))
.await;
Ok(pw.subscribe_events())
}
pub async fn cached_agent(&self, id: &identity::AgentId) -> Option<DiscoveredAgent> {
self.identity_discovery_cache.read().await.get(id).cloned()
}
pub async fn is_agent_machine_verified(
&self,
agent_id: &identity::AgentId,
machine_id: &identity::MachineId,
) -> bool {
{
let revoked = self.revocation_set.read().await;
if revoked.is_agent_revoked(agent_id) || revoked.is_machine_revoked(machine_id) {
return false;
}
}
let cache = self.identity_discovery_cache.read().await;
let Some(entry) = cache.get(agent_id) else {
return false;
};
if entry.machine_id != *machine_id {
return false;
}
if identity::is_expired(entry.cert_not_after, Self::unix_timestamp_secs()) {
return false;
}
true
}
pub fn revocation_set(&self) -> std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>> {
std::sync::Arc::clone(&self.revocation_set)
}
pub(crate) fn identity_discovery_cache(
&self,
) -> std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
> {
std::sync::Arc::clone(&self.identity_discovery_cache)
}
pub(crate) fn contact_store(
&self,
) -> std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>> {
std::sync::Arc::clone(&self.contact_store)
}
pub async fn revocation_records(&self) -> Vec<revocation::RevocationRecord> {
self.revocation_set.read().await.all_records()
}
pub async fn revoke(
&self,
issuer_keypair: &identity::AgentKeypair,
subject: revocation::RevokedSubject,
reason: Option<String>,
subject_cert: Option<&identity::AgentCertificate>,
) -> error::Result<revocation::RevocationRecord> {
let now = Self::unix_timestamp_secs();
let record = revocation::RevocationRecord::sign(
subject,
issuer_keypair.public_key(),
issuer_keypair.secret_key(),
now,
reason,
)?;
self.apply_and_publish_revocation(record.clone(), subject_cert)
.await?;
Ok(record)
}
async fn apply_and_publish_revocation(
&self,
record: revocation::RevocationRecord,
subject_cert: Option<&identity::AgentCertificate>,
) -> error::Result<()> {
{
let mut set = self.revocation_set.write().await;
if let Err(e) = set.verify_and_insert(record.clone(), subject_cert) {
return Err(error::IdentityError::CertificateVerification(format!(
"revocation rejected: {e}"
)));
}
}
storage::save_revocation_set(
&*self.revocation_set.read().await,
self.identity_dir.as_deref(),
)
.await?;
self.evict_revoked_subject(&record.subject).await;
if let Some(rt) = &self.gossip_runtime {
let records = self.revocation_set.read().await.all_records();
match bincode::serialize(&records) {
Ok(bytes) if !bytes.is_empty() => {
let _ = rt
.pubsub()
.publish(REVOCATION_TOPIC.to_string(), bytes::Bytes::from(bytes))
.await;
}
_ => {}
}
}
Ok(())
}
async fn evict_revoked_subject(&self, subject: &revocation::RevokedSubject) {
match subject {
revocation::RevokedSubject::Agent(agent_id) => {
let revoked_machine = {
let mut cache = self.identity_discovery_cache.write().await;
let m = cache.remove(agent_id).map(|entry| entry.machine_id);
drop(cache);
m
};
if let Some(machine_id) = revoked_machine {
let mut mcache = self.machine_discovery_cache.write().await;
mcache.remove(&machine_id);
drop(mcache);
if let Some(net) = &self.network {
let _ = net
.disconnect_with_reason(
&ant_quic::PeerId(machine_id.0),
network::DisconnectReason::Revocation,
)
.await;
}
}
{
let mut cs = self.contact_store.write().await;
cs.set_trust(agent_id, contacts::TrustLevel::Blocked);
}
tracing::info!(
agent = %hex::encode(agent_id.as_bytes()),
"evicted revoked agent from discovery cache"
);
}
revocation::RevokedSubject::Machine(machine_id) => {
let mut mcache = self.machine_discovery_cache.write().await;
mcache.remove(machine_id);
drop(mcache);
let mut cache = self.identity_discovery_cache.write().await;
cache.retain(|_, agent| agent.machine_id != *machine_id);
drop(cache);
if let Some(net) = &self.network {
let _ = net
.disconnect_with_reason(
&ant_quic::PeerId(machine_id.0),
network::DisconnectReason::Revocation,
)
.await;
}
tracing::info!(
machine = %hex::encode(machine_id.as_bytes()),
"evicted revoked machine from discovery cache"
);
}
}
}
pub async fn discover_agents_foaf(
&self,
ttl: u8,
timeout_ms: u64,
) -> error::NetworkResult<Vec<DiscoveredAgent>> {
let pw = self.presence.as_ref().ok_or_else(|| {
error::NetworkError::NodeError("presence system not initialized".to_string())
})?;
let topic = presence::global_presence_topic();
let raw_results: Vec<(
saorsa_gossip_types::PeerId,
saorsa_gossip_types::PresenceRecord,
)> = pw
.manager()
.initiate_foaf_query(topic, ttl, timeout_ms)
.await
.map_err(|e| error::NetworkError::NodeError(e.to_string()))?;
let cache = self.identity_discovery_cache.read().await;
let mut seen: std::collections::HashSet<identity::AgentId> =
std::collections::HashSet::new();
let mut agents: Vec<DiscoveredAgent> = Vec::with_capacity(raw_results.len());
for (peer_id, record) in &raw_results {
if let Some(agent) =
presence::presence_record_to_discovered_agent(*peer_id, record, &cache)
{
if seen.insert(agent.agent_id) {
agents.push(agent);
}
}
}
Ok(agents)
}
pub async fn discover_agent_by_id(
&self,
target_id: identity::AgentId,
ttl: u8,
timeout_ms: u64,
) -> error::NetworkResult<Option<DiscoveredAgent>> {
{
let cache = self.identity_discovery_cache.read().await;
if let Some(agent) = cache.get(&target_id) {
return Ok(Some(agent.clone()));
}
}
let agents = self.discover_agents_foaf(ttl, timeout_ms).await?;
Ok(agents.into_iter().find(|a| a.agent_id == target_id))
}
pub async fn find_agent(
&self,
agent_id: identity::AgentId,
) -> error::Result<Option<Vec<std::net::SocketAddr>>> {
self.start_identity_listener().await?;
if let Some(addrs) = self
.identity_discovery_cache
.read()
.await
.get(&agent_id)
.map(|e| e.addresses.clone())
{
return Ok(Some(addrs));
}
let runtime = match self.gossip_runtime.as_ref() {
Some(r) => r,
None => return Ok(None),
};
let shard_topic = shard_topic_for_agent(&agent_id);
let mut sub = runtime.pubsub().subscribe(shard_topic).await;
let cache = std::sync::Arc::clone(&self.identity_discovery_cache);
let machine_cache = std::sync::Arc::clone(&self.machine_discovery_cache);
let allow_local_scope = self
.network
.as_ref()
.is_some_and(|network| allow_local_discovery_addresses(network.config()));
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
if tokio::time::Instant::now() >= deadline {
break;
}
let timeout = tokio::time::sleep_until(deadline);
tokio::select! {
Some(msg) = sub.recv() => {
if let Ok(ann) = deserialize_identity_announcement(&msg.payload) {
if ann.verify().is_ok() && ann.agent_id == agent_id {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let filtered = filter_discovery_announcement_addrs(
ann.addresses.iter().copied(),
allow_local_scope,
);
let addrs = filtered.clone();
let discovered_agent = DiscoveredAgent {
agent_id: ann.agent_id,
machine_id: ann.machine_id,
user_id: ann.user_id,
addresses: filtered,
announced_at: ann.announced_at,
last_seen: now,
machine_public_key: ann.machine_public_key.clone(),
nat_type: ann.nat_type.clone(),
can_receive_direct: ann.can_receive_direct,
is_relay: ann.is_relay,
is_coordinator: ann.is_coordinator,
reachable_via: ann.reachable_via.clone(),
relay_candidates: ann.relay_candidates.clone(),
cert_not_after: ann
.agent_certificate
.as_ref()
.and_then(|c| c.not_after()),
agent_certificate: ann.agent_certificate.clone(),
agent_public_key: ann.agent_public_key.clone(),
};
upsert_discovered_machine_from_agent(&machine_cache, &discovered_agent)
.await;
upsert_discovered_agent(&cache, discovered_agent).await;
return Ok(Some(addrs));
}
}
}
_ = timeout => break,
}
}
if let Some(addrs) = self.find_agent_rendezvous(agent_id, 5).await? {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
upsert_discovered_agent(
&cache,
DiscoveredAgent {
agent_id,
machine_id: identity::MachineId([0u8; 32]),
user_id: None,
addresses: addrs.clone(),
announced_at: now,
last_seen: now,
machine_public_key: Vec::new(),
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: Vec::new(),
},
)
.await;
return Ok(Some(addrs));
}
Ok(None)
}
pub async fn find_machine(
&self,
machine_id: identity::MachineId,
timeout_secs: u64,
) -> error::Result<Option<DiscoveredMachine>> {
self.start_identity_listener().await?;
if let Some(machine) = self
.machine_discovery_cache
.read()
.await
.get(&machine_id)
.cloned()
{
return Ok(Some(machine));
}
let runtime = match self.gossip_runtime.as_ref() {
Some(r) => r,
None => return Ok(None),
};
let shard_topic = shard_topic_for_machine(&machine_id);
let mut sub = runtime.pubsub().subscribe(shard_topic).await;
let machine_cache = std::sync::Arc::clone(&self.machine_discovery_cache);
let allow_local_scope = self
.network
.as_ref()
.is_some_and(|network| allow_local_discovery_addresses(network.config()));
let deadline =
tokio::time::Instant::now() + std::time::Duration::from_secs(timeout_secs.clamp(1, 60));
loop {
if tokio::time::Instant::now() >= deadline {
break;
}
let timeout = tokio::time::sleep_until(deadline);
tokio::select! {
Some(msg) = sub.recv() => {
if let Ok(ann) = deserialize_machine_announcement(&msg.payload) {
if ann.verify().is_ok() && ann.machine_id == machine_id {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let filtered = filter_discovery_announcement_addrs(
ann.addresses.iter().copied(),
allow_local_scope,
);
let discovered = DiscoveredMachine::from_machine_announcement(
&ann,
filtered,
now,
);
upsert_discovered_machine(&machine_cache, discovered).await;
return Ok(machine_cache.read().await.get(&machine_id).cloned());
}
}
}
_ = timeout => break,
}
}
Ok(None)
}
pub async fn find_agents_by_user(
&self,
user_id: identity::UserId,
) -> error::Result<Vec<DiscoveredAgent>> {
self.start_identity_listener().await?;
let cutoff = Self::unix_timestamp_secs().saturating_sub(self.identity_ttl_secs);
Ok(self
.identity_discovery_cache
.read()
.await
.values()
.filter(|a| {
discovery_record_is_live(a.announced_at, a.last_seen, cutoff)
&& a.user_id == Some(user_id)
})
.cloned()
.collect())
}
#[must_use]
pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
self.network.as_ref().and_then(|n| n.local_addr())
}
pub async fn bound_addr(&self) -> Option<std::net::SocketAddr> {
if let Some(ref network) = self.network {
let addr = network.bound_addr().await;
match (addr, self.local_addr()) {
(Some(bound), Some(config)) if config.is_ipv4() && bound.is_ipv6() => {
Some(std::net::SocketAddr::new(config.ip(), bound.port()))
}
(Some(bound), _) => Some(bound),
_ => None,
}
} else {
None
}
}
pub fn build_announcement(
&self,
include_user: bool,
consent: bool,
) -> error::Result<IdentityAnnouncement> {
self.build_identity_announcement(include_user, consent)
}
pub fn build_machine_announcement(&self) -> error::Result<MachineAnnouncement> {
build_machine_announcement_for_identity(
&self.identity,
self.announcement_addresses(),
Self::unix_timestamp_secs(),
None,
Vec::new(),
Vec::new(),
self.network
.as_ref()
.is_some_and(|network| allow_local_discovery_addresses(network.config())),
)
}
fn start_network_event_listener(&self) {
if self
.network_event_listener_started
.swap(true, std::sync::atomic::Ordering::AcqRel)
{
return;
}
let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
return;
};
let cache = std::sync::Arc::clone(&self.identity_discovery_cache);
let dm = std::sync::Arc::clone(&self.direct_messaging);
let lifecycle_network = std::sync::Arc::clone(&network);
let lifecycle_dm = std::sync::Arc::clone(&dm);
let event_token = self.shutdown_token.clone();
let lifecycle_token = self.shutdown_token.clone();
let machine_cache = std::sync::Arc::clone(&self.machine_discovery_cache);
let active_reconnects: ReconnectTracker =
std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
let lifecycle_machine_cache = std::sync::Arc::clone(&machine_cache);
let lifecycle_reconnects = std::sync::Arc::clone(&active_reconnects);
self.spawn_tracked(async move {
let mut rx = network.subscribe();
tracing::info!("Network event reconciliation listener started");
loop {
let event = tokio::select! {
_ = event_token.cancelled() => break,
recv = rx.recv() => match recv {
Ok(event) => event,
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
tracing::warn!("Network event listener lagged by {skipped} events");
continue;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
},
};
match event {
network::NetworkEvent::PeerConnected { peer_id, .. } => {
let machine_id = identity::MachineId(peer_id);
let cached_agent_id = {
let cache = cache.read().await;
cache
.values()
.find(|entry| entry.machine_id == machine_id)
.map(|entry| entry.agent_id)
};
let agent_id = match cached_agent_id {
Some(agent_id) => Some(agent_id),
None => dm.lookup_agent(&machine_id).await,
};
if let Some(agent_id) = agent_id {
dm.mark_connected(agent_id, machine_id).await;
}
if let Ok(mut tracker) = active_reconnects.lock() {
if let Some(handle) = tracker.remove(&peer_id) {
handle.abort();
}
}
}
network::NetworkEvent::PeerDisconnected { peer_id, reason } => {
let machine_id = identity::MachineId(peer_id);
let cached_agent_id = {
let cache = cache.read().await;
cache
.values()
.find(|entry| entry.machine_id == machine_id)
.map(|entry| entry.agent_id)
};
let agent_id = match cached_agent_id {
Some(agent_id) => Some(agent_id),
None => dm.lookup_agent(&machine_id).await,
};
if let Some(agent_id) = agent_id {
dm.mark_disconnected(&agent_id).await;
}
if reason.reconnect_eligible() {
schedule_reconnect(
std::sync::Arc::clone(&network),
std::sync::Arc::clone(&machine_cache),
event_token.clone(),
peer_id,
std::sync::Arc::clone(&active_reconnects),
);
}
}
_ => {}
}
}
});
self.spawn_tracked(async move {
let Some(mut rx) = lifecycle_network.subscribe_all_peer_events().await else {
tracing::debug!(
"Peer lifecycle listener unavailable: network node not initialised"
);
return;
};
tracing::info!("Peer lifecycle watcher started for direct messaging");
loop {
let (peer_id, event) = tokio::select! {
_ = lifecycle_token.cancelled() => break,
recv = rx.recv() => match recv {
Ok(event) => event,
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
tracing::warn!("Peer lifecycle watcher lagged by {skipped} events");
continue;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
},
};
let machine_id = identity::MachineId(peer_id.0);
match event {
ant_quic::PeerLifecycleEvent::Established { generation } => {
lifecycle_dm.record_lifecycle_established(machine_id, Some(generation));
}
ant_quic::PeerLifecycleEvent::Replaced { new_generation, .. } => {
lifecycle_dm.record_lifecycle_replaced(machine_id, new_generation);
}
ant_quic::PeerLifecycleEvent::Closing { generation, reason } => {
lifecycle_dm.record_lifecycle_blocked(
machine_id,
Some(generation),
format!("closing: {reason}"),
);
}
ant_quic::PeerLifecycleEvent::Closed { generation, reason } => {
lifecycle_dm.record_lifecycle_blocked(
machine_id,
Some(generation),
format!("closed: {reason}"),
);
if !lifecycle_network.is_reconnect_suppressed(peer_id.0) {
schedule_reconnect(
std::sync::Arc::clone(&lifecycle_network),
std::sync::Arc::clone(&lifecycle_machine_cache),
lifecycle_token.clone(),
peer_id.0,
std::sync::Arc::clone(&lifecycle_reconnects),
);
}
}
ant_quic::PeerLifecycleEvent::ReaderExited { generation } => {
tracing::debug!(
machine_prefix = %network::hex_prefix(machine_id.as_bytes(), 4),
generation,
"peer reader exited; waiting for Closed/Established before blocking direct DM"
);
}
}
}
});
}
fn start_direct_listener(&self) {
if self
.direct_listener_started
.swap(true, std::sync::atomic::Ordering::AcqRel)
{
return;
}
let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
return;
};
let dm = std::sync::Arc::clone(&self.direct_messaging);
let discovery_cache = std::sync::Arc::clone(&self.identity_discovery_cache);
let contact_store = std::sync::Arc::clone(&self.contact_store);
let revocation_set = std::sync::Arc::clone(&self.revocation_set);
let token = self.shutdown_token.clone();
self.spawn_tracked(async move {
tracing::info!(target: "x0x::direct", stage = "listener", "direct message listener started");
loop {
let recv = tokio::select! {
_ = token.cancelled() => break,
r = network.recv_direct() => r,
};
let Some((ant_peer_id, payload)) = recv else {
tracing::warn!(
target: "x0x::direct",
stage = "listener",
"network.recv_direct channel closed — listener exiting"
);
break;
};
let raw_bytes = payload.len();
if payload.len() < 32 {
tracing::warn!(
target: "x0x::direct",
stage = "listener",
machine_prefix = %crate::logging::LogTransportPeerId::from(&ant_peer_id),
raw_bytes,
outcome = "drop_too_short",
"direct message too short to contain sender id"
);
continue;
}
let mut sender_bytes = [0u8; 32];
sender_bytes.copy_from_slice(&payload[..32]);
let sender = identity::AgentId(sender_bytes);
let machine_id = identity::MachineId(ant_peer_id.0);
let data = payload[32..].to_vec();
let payload_bytes = data.len();
let digest = direct::dm_payload_digest_hex(&data);
tracing::debug!(
target: "dm.trace",
stage = "inbound_envelope_received",
sender = %hex::encode(sender.as_bytes()),
machine_id = %hex::encode(machine_id.as_bytes()),
path = "raw_quic",
bytes = payload_bytes,
raw_bytes,
digest = %digest,
);
let (verified, cert_not_after) = {
let cache = discovery_cache.read().await;
cache
.get(&sender)
.map(|entry| (entry.machine_id == machine_id, entry.cert_not_after))
.unwrap_or((false, None))
};
let trust_decision = {
let contacts = contact_store.read().await;
let evaluator = trust::TrustEvaluator::new(&contacts);
let ctx = trust::TrustContext {
agent_id: &sender,
machine_id: &machine_id,
};
Some(evaluator.evaluate(&ctx))
};
tracing::debug!(
target: "dm.trace",
stage = "inbound_trust_evaluated",
sender = %hex::encode(sender.as_bytes()),
machine_id = %hex::encode(machine_id.as_bytes()),
path = "raw_quic",
verified,
decision = ?trust_decision,
digest = %digest,
);
tracing::info!(
target: "x0x::direct",
stage = "recv",
sender_prefix = %network::hex_prefix(&sender.0, 4),
machine_prefix = %network::hex_prefix(&machine_id.0, 4),
raw_bytes,
payload_bytes,
verified,
trust_decision = ?trust_decision,
"direct message received; dispatching to subscribers"
);
let peer_revoked = {
let revoked = revocation_set.read().await;
direct::inbound_peer_revoked(&revoked, &sender, &machine_id)
};
if peer_revoked {
dm.record_incoming_dropped_revoked();
tracing::info!(
target: "x0x::direct",
stage = "recv",
sender_prefix = %network::hex_prefix(&sender.0, 4),
machine_prefix = %network::hex_prefix(&machine_id.0, 4),
outcome = "drop_revoked",
"direct message from revoked sender dropped (direct-path revocation gate, mirrors EP3)"
);
continue;
}
if identity::is_expired(cert_not_after, Agent::unix_timestamp_secs()) {
dm.record_incoming_dropped_expired();
tracing::info!(
target: "x0x::direct",
stage = "recv",
sender_prefix = %network::hex_prefix(&sender.0, 4),
machine_prefix = %network::hex_prefix(&machine_id.0, 4),
outcome = "drop_expired",
"direct message from sender with expired cert dropped (runtime expiry gate, issue #191)"
);
continue;
}
dm.mark_connected(sender, machine_id).await;
let delivered = dm
.handle_incoming(machine_id, sender, data, verified, trust_decision)
.await;
tracing::debug!(
target: "dm.trace",
stage = "inbound_broadcast_published",
sender = %hex::encode(sender.as_bytes()),
machine_id = %hex::encode(machine_id.as_bytes()),
path = "raw_quic",
delivered,
subscribers = dm.subscriber_count(),
digest = %digest,
);
tracing::debug!(
target: "x0x::direct",
stage = "recv",
sender_prefix = %network::hex_prefix(&sender.0, 4),
payload_bytes,
subscriber_count = dm.subscriber_count(),
"direct message dispatched"
);
}
});
}
pub async fn open_peer_stream(
&self,
agent_id: &identity::AgentId,
protocol: streams::StreamProtocol,
) -> error::NetworkResult<streams::PeerStream> {
let (machine_id, cert_not_after) = {
let cache = self.identity_discovery_cache.read().await;
cache
.get(agent_id)
.map(|entry| (entry.machine_id, entry.cert_not_after))
.ok_or(error::NetworkError::PeerNotVerified {
agent_id: agent_id.0,
})?
};
let expired = identity::is_expired(cert_not_after, Self::unix_timestamp_secs());
let trust_decision = {
let contacts = self.contact_store.read().await;
let evaluator = trust::TrustEvaluator::new(&contacts);
Some(evaluator.evaluate(&trust::TrustContext {
agent_id,
machine_id: &machine_id,
}))
};
let (revoked_agent, revoked_machine) = {
let revoked = self.revocation_set.read().await;
(
revoked.is_agent_revoked(agent_id),
revoked.is_machine_revoked(&machine_id),
)
};
streams::stream_gate(
agent_id,
trust_decision,
revoked_agent,
revoked_machine,
expired,
)?;
let network = self
.network
.as_ref()
.ok_or_else(|| error::NetworkError::NodeError("network not initialized".to_string()))?;
let peer = ant_quic::PeerId(machine_id.0);
let (mut send, recv) = network.open_bi(&peer).await?;
streams::write_protocol_prefix(&mut send, protocol).await?;
tracing::info!(
target: "x0x::streams",
agent = %hex::encode(agent_id.as_bytes()),
machine = %hex::encode(machine_id.as_bytes()),
protocol = ?protocol,
"outbound peer stream opened (identity gate cleared)"
);
Ok(streams::PeerStream::new(
vec![*agent_id],
machine_id,
protocol,
send,
recv,
))
}
pub async fn next_incoming_stream(&self) -> Option<streams::PeerStream> {
let mut rx = self.stream_accept.receiver().lock().await;
rx.recv().await
}
fn start_stream_accept_loop(&self) {
if !self.stream_accept.start_once() {
return;
}
let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
return;
};
let discovery_cache = std::sync::Arc::clone(&self.identity_discovery_cache);
let contact_store = std::sync::Arc::clone(&self.contact_store);
let revocation_set = std::sync::Arc::clone(&self.revocation_set);
let incoming = std::sync::Arc::clone(&self.stream_accept);
let token = self.shutdown_token.clone();
self.spawn_tracked(async move {
tracing::info!(target: "x0x::streams", "byte-stream accept loop started");
loop {
let accepted = tokio::select! {
_ = token.cancelled() => break,
r = network.accept_bi() => r,
};
let (ant_peer_id, send, mut recv) = match accepted {
Ok(triple) => triple,
Err(e) => {
tracing::warn!(target: "x0x::streams", error=%e, "accept_bi failed; continuing");
continue;
}
};
let machine_id = identity::MachineId(ant_peer_id.0);
let agents: Vec<(identity::AgentId, Option<u64>)> = {
let cache = discovery_cache.read().await;
let mut found: Vec<(identity::AgentId, Option<u64>)> = cache
.values()
.filter(|a| a.machine_id == machine_id)
.map(|a| (a.agent_id, a.cert_not_after))
.collect();
found.sort_by_key(|(a, _)| a.0);
found
};
if agents.is_empty() {
tracing::info!(
target: "x0x::streams",
machine = %hex::encode(machine_id.as_bytes()),
outcome = "deny_not_verified",
"inbound stream from machine with no known agent — denied"
);
continue;
}
let now_secs = Agent::unix_timestamp_secs();
let mut gate_denied: Option<(identity::AgentId, error::NetworkError)> = None;
for (agent_id, cert_not_after) in &agents {
let expired = identity::is_expired(*cert_not_after, now_secs);
let trust_decision = {
let contacts = contact_store.read().await;
let evaluator = trust::TrustEvaluator::new(&contacts);
Some(evaluator.evaluate(&trust::TrustContext {
agent_id,
machine_id: &machine_id,
}))
};
let (revoked_agent, revoked_machine) = {
let revoked = revocation_set.read().await;
(
revoked.is_agent_revoked(agent_id),
revoked.is_machine_revoked(&machine_id),
)
};
if let Err(e) = streams::stream_gate(
agent_id,
trust_decision,
revoked_agent,
revoked_machine,
expired,
) {
gate_denied = Some((*agent_id, e));
break;
}
}
if let Some((agent_id, e)) = gate_denied {
tracing::info!(
target: "x0x::streams",
agent = %hex::encode(agent_id.as_bytes()),
machine = %hex::encode(machine_id.as_bytes()),
agent_count = agents.len(),
outcome = "deny_gate",
error = %e,
"inbound stream denied at identity gate (one agent on the machine failed)"
);
continue;
}
let agents: Vec<identity::AgentId> =
agents.into_iter().map(|(a, _)| a).collect();
let incoming_for_task = std::sync::Arc::clone(&incoming);
tokio::spawn(async move {
let protocol = match tokio::time::timeout(
streams::PREFIX_READ_TIMEOUT,
streams::read_protocol_prefix(&mut recv),
)
.await
{
Ok(Ok(p)) => p,
Ok(Err(e)) => {
tracing::info!(
target: "x0x::streams",
machine = %hex::encode(machine_id.as_bytes()),
outcome = "deny_protocol",
error = %e,
"inbound stream protocol prefix rejected"
);
return;
}
Err(_) => {
tracing::info!(
target: "x0x::streams",
machine = %hex::encode(machine_id.as_bytes()),
outcome = "deny_prefix_timeout",
"inbound stream prefix byte timed out — resetting"
);
return;
}
};
let peer_stream =
streams::PeerStream::new(agents, machine_id, protocol, send, recv);
if incoming_for_task.sender().try_send(peer_stream).is_err() {
tracing::debug!(
target: "x0x::streams",
"incoming-stream channel full; dropping accepted stream"
);
}
});
}
});
}
pub async fn start_identity_heartbeat(&self) -> error::Result<()> {
let mut handle_guard = self.heartbeat_handle.lock().await;
if self.shutdown_token.is_cancelled() {
return Ok(());
}
if handle_guard.is_some() {
return Ok(());
}
let Some(runtime) = self.gossip_runtime.as_ref().map(std::sync::Arc::clone) else {
return Err(error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized — cannot start heartbeat",
)));
};
let Some(network) = self.network.as_ref().map(std::sync::Arc::clone) else {
return Err(error::IdentityError::Storage(std::io::Error::other(
"network not initialized — cannot start heartbeat",
)));
};
let allow_local_discovery_addrs = allow_local_discovery_addresses(network.config());
let ctx = HeartbeatContext {
identity: std::sync::Arc::clone(&self.identity),
runtime,
network,
interval_secs: self.heartbeat_interval_secs,
cache: std::sync::Arc::clone(&self.identity_discovery_cache),
machine_cache: std::sync::Arc::clone(&self.machine_discovery_cache),
user_identity_consented: std::sync::Arc::clone(&self.user_identity_consented),
allow_local_discovery_addrs,
revocation_set: std::sync::Arc::clone(&self.revocation_set),
};
let handle = tokio::task::spawn(async move {
let mut ticker =
tokio::time::interval(std::time::Duration::from_secs(ctx.interval_secs));
ticker.tick().await; loop {
ticker.tick().await;
if let Err(e) = ctx.announce().await {
tracing::warn!("identity heartbeat announce failed: {e}");
}
}
});
*handle_guard = Some(handle);
Ok(())
}
pub async fn advertise_identity(&self, validity_ms: u64) -> error::Result<()> {
use saorsa_gossip_rendezvous::{Capability, ProviderSummary};
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized — cannot advertise identity",
))
})?;
let peer_id = runtime.peer_id();
let addresses = self.announcement_addresses();
let addr_bytes = bincode::serialize(&addresses).map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to serialize addresses for rendezvous: {e}"
))
})?;
let mut summary = ProviderSummary::new(
self.agent_id().0,
peer_id,
vec![Capability::Identity],
validity_ms,
)
.with_extensions(addr_bytes);
summary
.sign_raw(self.identity.machine_keypair().secret_key().as_bytes())
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to sign rendezvous summary: {e}"
)))
})?;
let cbor_bytes = summary.to_cbor().map_err(|e| {
error::IdentityError::Serialization(format!(
"failed to CBOR-encode rendezvous summary: {e}"
))
})?;
let topic = rendezvous_shard_topic_for_agent(&self.agent_id());
runtime
.pubsub()
.publish(topic, bytes::Bytes::from(cbor_bytes))
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"failed to publish rendezvous summary: {e}"
)))
})?;
self.rendezvous_advertised
.store(true, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
pub async fn find_agent_rendezvous(
&self,
agent_id: identity::AgentId,
timeout_secs: u64,
) -> error::Result<Option<Vec<std::net::SocketAddr>>> {
use saorsa_gossip_rendezvous::ProviderSummary;
let runtime = match self.gossip_runtime.as_ref() {
Some(r) => r,
None => return Ok(None),
};
let topic = rendezvous_shard_topic_for_agent(&agent_id);
let mut sub = runtime.pubsub().subscribe(topic).await;
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
loop {
if tokio::time::Instant::now() >= deadline {
break;
}
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
tokio::select! {
Some(msg) = sub.recv() => {
let summary = match ProviderSummary::from_cbor(&msg.payload) {
Ok(s) => s,
Err(_) => continue,
};
if summary.target != agent_id.0 {
continue;
}
let cached_pub = self
.identity_discovery_cache
.read()
.await
.get(&agent_id)
.map(|e| e.machine_public_key.clone());
if let Some(pub_bytes) = cached_pub {
if !pub_bytes.is_empty()
&& !summary.verify_raw(&pub_bytes).unwrap_or(false)
{
tracing::warn!(
"Rendezvous summary signature verification failed for agent {:?}; discarding",
agent_id
);
continue;
}
}
let addrs: Vec<std::net::SocketAddr> = summary
.extensions
.as_deref()
.and_then(|b| {
use bincode::Options;
bincode::DefaultOptions::new()
.with_fixint_encoding()
.with_limit(crate::network::MAX_MESSAGE_DESERIALIZE_SIZE)
.deserialize(b)
.ok()
})
.unwrap_or_default();
if !addrs.is_empty() {
return Ok(Some(addrs));
}
}
_ = tokio::time::sleep(remaining) => break,
}
}
Ok(None)
}
#[doc(hidden)]
pub fn insert_capability_for_testing(
&self,
agent_id: identity::AgentId,
machine_id: identity::MachineId,
capabilities: dm::DmCapabilities,
) {
self.capability_store
.insert(agent_id, machine_id, capabilities, dm::now_unix_ms());
}
#[doc(hidden)]
pub async fn insert_discovered_agent_for_testing(&self, agent: DiscoveredAgent) {
let agent_id = agent.agent_id;
let machine_id = agent.machine_id;
upsert_discovered_machine_from_agent(&self.machine_discovery_cache, &agent).await;
upsert_discovered_agent(&self.identity_discovery_cache, agent).await;
if machine_id.0 != [0u8; 32] {
self.direct_messaging
.register_agent(agent_id, machine_id)
.await;
if let Some(ref network) = self.network {
let ant_peer_id = ant_quic::PeerId(machine_id.0);
if network.is_connected(&ant_peer_id).await {
self.direct_messaging
.mark_connected(agent_id, machine_id)
.await;
}
}
}
}
#[doc(hidden)]
pub async fn set_contact_trusted_for_testing(&self, agent_id: identity::AgentId) {
let mut store = self.contact_store.write().await;
store.add(contacts::Contact {
agent_id,
trust_level: contacts::TrustLevel::Trusted,
label: None,
added_at: 0,
last_seen: None,
identity_type: contacts::IdentityType::Anonymous,
machines: Vec::new(),
dm_capabilities: None,
});
}
#[doc(hidden)]
pub async fn push_relayed_dm_for_testing(
&self,
from_peer: ant_quic::PeerId,
relayed: peer_relay::RelayedDm,
) -> bool {
let Some(ref network) = self.network else {
return false;
};
let sender_agent_id = relayed.header.sender_agent_id;
network
.test_relayed_dm_sender()
.send((from_peer, sender_agent_id, relayed))
.await
.is_ok()
}
pub async fn create_task_list(&self, name: &str, topic: &str) -> error::Result<TaskListHandle> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
let peer_id = runtime.peer_id();
let list_id = crdt::TaskListId::from_topic(topic);
let task_list = crdt::TaskList::new(list_id, name.to_string(), peer_id);
let sync = crdt::TaskListSync::new(
task_list,
std::sync::Arc::clone(runtime.pubsub()),
topic.to_string(),
peer_id,
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"task list sync creation failed: {}",
e
)))
})?;
let sync = std::sync::Arc::new(sync);
sync.start_with_spawner(|fut| self.spawn_tracked(fut))
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"task list sync start failed: {}",
e
)))
})?;
Ok(TaskListHandle {
sync,
agent_id: self.agent_id(),
peer_id,
replica_epoch: TaskListHandle::fresh_epoch(),
signing: std::sync::Arc::new(gossip::SigningContext::from_keypair(
self.identity.agent_keypair(),
)),
})
}
pub async fn join_task_list(&self, topic: &str) -> error::Result<TaskListHandle> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
let peer_id = runtime.peer_id();
let list_id = crdt::TaskListId::from_topic(topic);
let task_list = crdt::TaskList::new(list_id, String::new(), peer_id);
let sync = crdt::TaskListSync::new(
task_list,
std::sync::Arc::clone(runtime.pubsub()),
topic.to_string(),
peer_id,
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"task list sync creation failed: {}",
e
)))
})?;
let sync = std::sync::Arc::new(sync);
sync.start_with_spawner(|fut| self.spawn_tracked(fut))
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"task list sync start failed: {}",
e
)))
})?;
Ok(TaskListHandle {
sync,
agent_id: self.agent_id(),
peer_id,
replica_epoch: TaskListHandle::fresh_epoch(),
signing: std::sync::Arc::new(gossip::SigningContext::from_keypair(
self.identity.agent_keypair(),
)),
})
}
}
const RECONNECT_BACKOFF_DELAYS: &[std::time::Duration] = &[
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(2),
std::time::Duration::from_secs(4),
std::time::Duration::from_secs(8),
std::time::Duration::from_secs(16),
];
const RECONNECT_FAILURE_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(60);
fn announcement_should_auto_connect(reconnect_suppressed: bool, already_connected: bool) -> bool {
!reconnect_suppressed && !already_connected
}
const ANNOUNCEMENT_AUTO_CONNECT_RETRY_COOLDOWN: std::time::Duration =
std::time::Duration::from_secs(60);
fn announcement_auto_connect_retry_allowed(
last_attempt: Option<std::time::Instant>,
now: std::time::Instant,
) -> bool {
last_attempt.is_none_or(|last| {
now.saturating_duration_since(last) >= ANNOUNCEMENT_AUTO_CONNECT_RETRY_COOLDOWN
})
}
fn jittered_backoff_delay(delay: std::time::Duration) -> std::time::Duration {
let factor = 0.8 + rand::random::<f64>() * 0.4;
delay.mul_f64(factor)
}
type ReconnectTracker = std::sync::Arc<
std::sync::Mutex<std::collections::HashMap<[u8; 32], tokio::task::JoinHandle<()>>>,
>;
fn schedule_reconnect(
network: std::sync::Arc<network::NetworkNode>,
machine_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::MachineId, DiscoveredMachine>>,
>,
shutdown_token: tokio_util::sync::CancellationToken,
peer_id_bytes: [u8; 32],
active_reconnects: ReconnectTracker,
) {
if network.is_reconnect_suppressed(peer_id_bytes) {
return;
}
let mut tracker = match active_reconnects.lock() {
Ok(t) => t,
Err(_) => return,
};
if tracker.contains_key(&peer_id_bytes) {
return;
}
let tracker_clone = std::sync::Arc::clone(&active_reconnects);
let handle = tokio::spawn(async move {
let peer_id = ant_quic::PeerId(peer_id_bytes);
let machine_id = identity::MachineId(peer_id_bytes);
let prefix = network::hex_prefix(&peer_id_bytes, 4);
tracing::info!(
target: "x0x::connect",
peer_id_prefix = %prefix,
"scheduling proactive reconnect for disconnected peer",
);
'attempts: for (attempt, &delay) in RECONNECT_BACKOFF_DELAYS.iter().enumerate() {
tokio::select! {
_ = shutdown_token.cancelled() => break,
_ = tokio::time::sleep(jittered_backoff_delay(delay)) => {}
}
if network.is_reconnect_suppressed(peer_id_bytes) {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %prefix,
attempt,
"reconnect cancelled: peer reconnect-suppressed",
);
break;
}
if network.is_connected(&peer_id).await {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %prefix,
attempt,
"reconnect cancelled: peer already connected",
);
break;
}
let candidate_addrs = {
let cache = machine_cache.read().await;
cache
.get(&machine_id)
.map(|m| m.addresses.clone())
.unwrap_or_default()
};
tracing::info!(
target: "x0x::connect",
peer_id_prefix = %prefix,
attempt = attempt + 1,
max_attempts = RECONNECT_BACKOFF_DELAYS.len(),
candidate_addrs = candidate_addrs.len(),
"proactive reconnect attempt",
);
let mut connected = false;
for addr in &candidate_addrs {
if network.is_reconnect_suppressed(peer_id_bytes) {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %prefix,
"reconnect aborted before dial: peer reconnect-suppressed",
);
break 'attempts;
}
match tokio::time::timeout(
std::time::Duration::from_secs(5),
network.connect_addr(*addr),
)
.await
{
Ok(Ok(connected_peer)) if connected_peer == peer_id => {
tracing::info!(
target: "x0x::connect",
peer_id_prefix = %prefix,
attempt = attempt + 1,
addr = %addr,
"proactive reconnect succeeded via discovery address",
);
connected = true;
break;
}
Ok(Ok(other)) => {
tracing::warn!(
target: "x0x::connect",
peer_id_prefix = %prefix,
addr = %addr,
"reconnect resolved to unexpected peer {:?}",
other,
);
}
Ok(Err(e)) => {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %prefix,
addr = %addr,
error = %e,
"reconnect attempt at discovery address failed",
);
}
Err(_) => {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %prefix,
addr = %addr,
"reconnect attempt at discovery address timed out",
);
}
}
}
if !connected {
if network.is_reconnect_suppressed(peer_id_bytes) {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %prefix,
"reconnect aborted before bootstrap-cache dial: peer reconnect-suppressed",
);
break 'attempts;
}
match network.connect_cached_peer(peer_id).await {
Ok(_) => {
tracing::info!(
target: "x0x::connect",
peer_id_prefix = %prefix,
attempt = attempt + 1,
"proactive reconnect succeeded via bootstrap cache",
);
connected = true;
}
Err(e) => {
tracing::debug!(
target: "x0x::connect",
peer_id_prefix = %prefix,
error = %e,
"reconnect via bootstrap cache failed",
);
}
}
}
if connected {
break;
}
}
if !network.is_connected(&peer_id).await {
tokio::select! {
_ = shutdown_token.cancelled() => {}
_ = tokio::time::sleep(RECONNECT_FAILURE_COOLDOWN) => {}
}
}
if let Ok(mut t) = tracker_clone.lock() {
t.remove(&peer_id_bytes);
}
});
tracker.insert(peer_id_bytes, handle);
}
impl AgentBuilder {
pub fn with_machine_key<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.machine_key_path = Some(path.as_ref().to_path_buf());
self
}
pub fn with_agent_key(mut self, keypair: identity::AgentKeypair) -> Self {
self.agent_keypair = Some(keypair);
self
}
pub fn with_agent_key_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.agent_key_path = Some(path.as_ref().to_path_buf());
self
}
#[must_use]
pub fn with_agent_cert_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.agent_cert_path = Some(path.as_ref().to_path_buf());
self
}
pub fn with_network_config(mut self, config: network::NetworkConfig) -> Self {
self.network_config = Some(config);
self
}
#[must_use]
pub fn with_gossip_config(mut self, config: gossip::GossipConfig) -> Self {
self.gossip_config = Some(config);
self
}
pub fn with_peer_cache_dir<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.peer_cache_dir = Some(path.as_ref().to_path_buf());
self
}
pub fn with_peer_cache_disabled(mut self) -> Self {
self.disable_peer_cache = true;
self
}
pub fn with_user_key(mut self, keypair: identity::UserKeypair) -> Self {
self.user_keypair = Some(keypair);
self
}
pub fn with_user_key_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.user_key_path = Some(path.as_ref().to_path_buf());
self
}
#[must_use]
pub fn with_heartbeat_interval(mut self, secs: u64) -> Self {
self.heartbeat_interval_secs = Some(secs);
self
}
#[must_use]
pub fn with_identity_ttl(mut self, secs: u64) -> Self {
self.identity_ttl_secs = Some(secs);
self
}
#[must_use]
pub fn with_presence_beacon_interval(mut self, secs: u64) -> Self {
self.presence_beacon_interval_secs = Some(secs);
self
}
#[must_use]
pub fn with_presence_event_poll_interval(mut self, secs: u64) -> Self {
self.presence_event_poll_interval_secs = Some(secs);
self
}
#[must_use]
pub fn with_presence_offline_timeout(mut self, secs: u64) -> Self {
self.presence_offline_timeout_secs = Some(secs);
self
}
#[must_use]
pub fn with_contact_store_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.contact_store_path = Some(path.as_ref().to_path_buf());
self
}
#[must_use]
pub fn with_identity_dir<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.identity_dir = Some(path.as_ref().to_path_buf());
self
}
pub async fn build(self) -> error::Result<Agent> {
let machine_keypair = if let Some(path) = self.machine_key_path {
match storage::load_machine_keypair_from(&path).await {
Ok(kp) => kp,
Err(_) => {
let kp = identity::MachineKeypair::generate()?;
storage::save_machine_keypair_to(&kp, &path).await?;
kp
}
}
} else if storage::machine_keypair_exists().await {
storage::load_machine_keypair().await?
} else {
let kp = identity::MachineKeypair::generate()?;
storage::save_machine_keypair(&kp).await?;
kp
};
let agent_keypair = if let Some(kp) = self.agent_keypair {
kp
} else if let Some(path) = self.agent_key_path {
match storage::load_agent_keypair_from(&path).await {
Ok(kp) => kp,
Err(_) => {
let kp = identity::AgentKeypair::generate()?;
storage::save_agent_keypair_to(&kp, &path).await?;
kp
}
}
} else if storage::agent_keypair_exists().await {
storage::load_agent_keypair_default().await?
} else {
let kp = identity::AgentKeypair::generate()?;
storage::save_agent_keypair_default(&kp).await?;
kp
};
let user_keypair = if let Some(kp) = self.user_keypair {
Some(kp)
} else if let Some(path) = self.user_key_path {
storage::load_user_keypair_from(&path).await.ok()
} else if storage::user_keypair_exists().await {
storage::load_user_keypair().await.ok()
} else {
None
};
let identity = if let Some(user_kp) = user_keypair {
let cert_path = self.agent_cert_path.clone();
let existing_cert = if let Some(ref p) = cert_path {
if tokio::fs::try_exists(p).await.unwrap_or(false) {
storage::load_agent_certificate_from(p).await.ok()
} else {
None
}
} else if storage::agent_certificate_exists().await {
storage::load_agent_certificate().await.ok()
} else {
None
};
let cert_still_valid = existing_cert.as_ref().is_some_and(|c| {
let user_match = c
.user_id()
.map(|uid| uid == user_kp.user_id())
.unwrap_or(false);
let agent_match = c
.agent_id()
.map(|aid| aid == agent_keypair.agent_id())
.unwrap_or(false);
user_match && agent_match
});
let cert = if cert_still_valid {
existing_cert.ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"agent certificate validity check succeeded with no certificate loaded",
))
})?
} else {
let new_cert = identity::AgentCertificate::issue(&user_kp, &agent_keypair)?;
if let Some(ref p) = cert_path {
storage::save_agent_certificate_to(&new_cert, p).await?;
} else {
storage::save_agent_certificate(&new_cert).await?;
}
new_cert
};
identity::Identity::new_with_user(machine_keypair, agent_keypair, user_kp, cert)
} else {
identity::Identity::new(machine_keypair, agent_keypair)
};
let bootstrap_cache_config = if self.network_config.is_some() {
let cache_dir = self.peer_cache_dir.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".x0x")
.join("peers")
});
Some(
ant_quic::BootstrapCacheConfig::builder()
.cache_dir(cache_dir)
.min_peers_to_save(1)
.persist(!self.disable_peer_cache)
.build(),
)
} else {
None
};
let machine_keypair = {
let pk = ant_quic::MlDsaPublicKey::from_bytes(
identity.machine_keypair().public_key().as_bytes(),
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"invalid machine public key: {e}"
)))
})?;
let sk = ant_quic::MlDsaSecretKey::from_bytes(
identity.machine_keypair().secret_key().as_bytes(),
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"invalid machine secret key: {e}"
)))
})?;
Some((pk, sk))
};
let peer_relay_config = self
.network_config
.as_ref()
.map(|cfg| cfg.peer_relay.clone())
.unwrap_or_default();
let mut parsed_relay_candidates = Vec::with_capacity(peer_relay_config.candidates.len());
for hex_str in &peer_relay_config.candidates {
let trimmed = hex_str.trim();
let bytes = hex::decode(trimmed).map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"invalid relay candidate hex {trimmed:?}: {e}"
)))
})?;
let arr: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
error::IdentityError::Storage(std::io::Error::other(format!(
"relay candidate must be 32 bytes (got {} bytes)",
v.len()
)))
})?;
parsed_relay_candidates.push(identity::AgentId(arr));
}
let peer_relay = std::sync::Arc::new(peer_relay::PeerRelay::with_policy(
peer_relay_config.to_policy(),
));
let relay_candidates =
std::sync::Arc::new(tokio::sync::RwLock::new(parsed_relay_candidates));
let identity_discovery_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
> = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
let network = if let Some(config) = self.network_config {
let node = network::NetworkNode::new(config, bootstrap_cache_config, machine_keypair)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"network initialization failed: {}",
e
)))
})?;
debug_assert_eq!(
node.peer_id().0,
identity.machine_id().0,
"ant-quic PeerId must equal MachineId after identity unification"
);
Some(std::sync::Arc::new(node))
} else {
None
};
let bootstrap_cache = network.as_ref().and_then(|n| n.bootstrap_cache());
let revocation_set = std::sync::Arc::new(tokio::sync::RwLock::new(
storage::load_revocation_set(self.identity_dir.as_deref()).await,
));
let contacts_path = self.contact_store_path.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".x0x")
.join("contacts.json")
});
let contact_store = std::sync::Arc::new(tokio::sync::RwLock::new(
contacts::ContactStore::new(contacts_path),
));
if let Some(ref net) = network {
spawn_relay_dm_listener(
std::sync::Arc::clone(net),
std::sync::Arc::clone(&peer_relay),
std::sync::Arc::clone(&identity_discovery_cache),
std::sync::Arc::clone(&revocation_set),
std::sync::Arc::clone(&contact_store),
identity.agent_id(),
);
}
let signing_ctx = std::sync::Arc::new(gossip::SigningContext::from_keypair(
identity.agent_keypair(),
));
let gossip_runtime = if let Some(ref net) = network {
let runtime = gossip::GossipRuntime::new(
self.gossip_config.unwrap_or_default(),
std::sync::Arc::clone(net),
Some(signing_ctx),
)
.await
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"gossip runtime initialization failed: {}",
e
)))
})?;
Some(std::sync::Arc::new(runtime))
} else {
None
};
let gossip_cache_adapter = bootstrap_cache.as_ref().map(|cache| {
saorsa_gossip_coordinator::GossipCacheAdapter::new(std::sync::Arc::clone(cache))
});
let direct_messaging = std::sync::Arc::new(direct::DirectMessaging::new());
let presence = if let Some(ref net) = network {
let peer_id = saorsa_gossip_transport::GossipTransport::local_peer_id(net.as_ref());
let mut presence_config = presence::PresenceConfig::default();
if let Some(secs) = self.presence_beacon_interval_secs {
presence_config.beacon_interval_secs = secs;
}
if let Some(secs) = self.presence_event_poll_interval_secs {
presence_config.event_poll_interval_secs = secs;
}
if let Some(secs) = self.presence_offline_timeout_secs {
presence_config.adaptive_timeout_fallback_secs = secs;
}
let pw = presence::PresenceWrapper::new(
peer_id,
identity.machine_keypair().to_bytes(),
std::sync::Arc::clone(net),
presence_config,
bootstrap_cache.clone(),
)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"presence initialization failed: {}",
e
)))
})?;
let pw_arc = std::sync::Arc::new(pw);
if let Some(ref rt) = gossip_runtime {
rt.set_presence(std::sync::Arc::clone(&pw_arc));
}
Some(pw_arc)
} else {
None
};
Ok(Agent {
identity: std::sync::Arc::new(identity),
network,
gossip_runtime,
bootstrap_cache,
gossip_cache_adapter,
identity_discovery_cache,
authenticated_machine_bindings: std::sync::Arc::new(tokio::sync::RwLock::new(
dm_inbox::AuthenticatedMachineBindingCache::default(),
)),
machine_discovery_cache: std::sync::Arc::new(tokio::sync::RwLock::new(
std::collections::HashMap::new(),
)),
user_discovery_cache: std::sync::Arc::new(tokio::sync::RwLock::new(
std::collections::HashMap::new(),
)),
identity_listener_started: std::sync::atomic::AtomicBool::new(false),
heartbeat_interval_secs: self
.heartbeat_interval_secs
.unwrap_or(IDENTITY_HEARTBEAT_INTERVAL_SECS),
identity_ttl_secs: self.identity_ttl_secs.unwrap_or(IDENTITY_TTL_SECS),
heartbeat_handle: tokio::sync::Mutex::new(None),
discovery_cache_reaper_handle: tokio::sync::Mutex::new(None),
rendezvous_advertised: std::sync::atomic::AtomicBool::new(false),
contact_store,
direct_messaging,
network_event_listener_started: std::sync::atomic::AtomicBool::new(false),
direct_listener_started: std::sync::atomic::AtomicBool::new(false),
presence,
user_identity_consented: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
capability_store: std::sync::Arc::new(dm_capability::CapabilityStore::new()),
dm_capabilities_tx: std::sync::Arc::new({
let (tx, _rx) = tokio::sync::watch::channel(dm::DmCapabilities::pending());
tx
}),
dm_inflight_acks: std::sync::Arc::new(dm::InFlightAcks::new()),
recent_delivery_cache: std::sync::Arc::new(dm::RecentDeliveryCache::with_defaults()),
capability_advert_service: tokio::sync::Mutex::new(None),
dm_inbox_service: tokio::sync::Mutex::new(None),
revocation_set,
identity_dir: self.identity_dir,
shutdown_token: tokio_util::sync::CancellationToken::new(),
tracked_tasks: std::sync::Arc::new(std::sync::Mutex::new(TrackedTasks {
closed: false,
handles: Vec::new(),
})),
peer_relay,
relay_candidates,
stream_accept: std::sync::Arc::new(streams::StreamAccept::new(256)),
})
}
}
#[derive(Clone)]
pub struct TaskListHandle {
sync: std::sync::Arc<crdt::TaskListSync>,
agent_id: identity::AgentId,
peer_id: saorsa_gossip_types::PeerId,
replica_epoch: u64,
signing: std::sync::Arc<crate::gossip::SigningContext>,
}
impl std::fmt::Debug for TaskListHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TaskListHandle")
.field("agent_id", &self.agent_id)
.field("peer_id", &self.peer_id)
.finish_non_exhaustive()
}
}
impl TaskListHandle {
fn fresh_epoch() -> u64 {
use rand::RngCore;
let mut bytes = [0u8; 8];
match rand::rngs::OsRng.try_fill_bytes(&mut bytes) {
Ok(()) => {
let epoch = u64::from_le_bytes(bytes);
if epoch == 0 {
1
} else {
epoch
}
}
Err(_) => std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(1)
.max(1),
}
}
fn current_fence(&self, revision: u64) -> FenceToken {
FenceToken {
epoch: self.replica_epoch,
revision,
}
}
pub async fn set_authorized_agents(
&self,
agents: std::collections::HashSet<identity::AgentId>,
) {
let mut list = self.sync.write().await;
list.set_authorized_agents(agents);
}
pub async fn clear_authorized_agents(&self) {
let mut list = self.sync.write().await;
list.clear_authorized_agents();
}
#[cfg(test)]
pub fn set_replica_epoch_for_testing(&mut self, epoch: u64) {
self.replica_epoch = epoch;
}
}
impl TaskListHandle {
pub async fn add_task(
&self,
title: String,
description: String,
) -> error::Result<crdt::TaskId> {
let (task_id, _version) = self.add_task_versioned(title, description).await?;
Ok(task_id)
}
pub async fn add_task_versioned(
&self,
title: String,
description: String,
) -> error::Result<(crdt::TaskId, u64)> {
let (task_id, version, delta) = {
let mut list = self.sync.write().await;
let seq = list.next_seq();
let task_id = crdt::TaskId::new(&title, &self.agent_id, seq);
let metadata = crdt::TaskMetadata::new(title, description, 128, self.agent_id, seq);
let task = crdt::TaskItem::new(task_id, metadata, self.peer_id);
list.add_task(task.clone(), self.peer_id, seq)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"add_task failed: {}",
e
)))
})?;
let tag = (self.peer_id, seq);
let version = list.current_version();
let delta = crdt::TaskListDelta::for_add(task_id, task, tag, version);
(task_id, version, delta)
};
if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
tracing::warn!("failed to publish add_task delta: {}", e);
}
Ok((task_id, version))
}
pub async fn claim_task(&self, task_id: crdt::TaskId) -> error::Result<()> {
self.claim_task_versioned(task_id, None).await.map(|_| ())
}
pub async fn claim_task_versioned(
&self,
task_id: crdt::TaskId,
expected: Option<FenceToken>,
) -> error::Result<TaskMutationOutcome> {
let (fence, delta, advisory) = {
let mut list = self.sync.write().await;
if let Some(expected) = expected {
let current = list.current_version();
if expected.epoch != self.replica_epoch || expected.revision != current {
return Ok(TaskMutationOutcome::StaleLocalVersion {
current: self.current_fence(current),
});
}
}
let seq = list.next_seq();
list.claim_task(&task_id, self.agent_id, self.peer_id, seq, &self.signing)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"claim_task failed: {}",
e
)))
})?;
let task = list.get_task(&task_id).ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other("task disappeared after claim"))
})?;
let winner = task.claim_record();
let advisory = AdvisoryOwnership {
agent: self.agent_id,
locally_winning: winner.is_some_and(|(a, _)| a == self.agent_id),
current_winner: winner,
};
let full_task = task.clone();
let version = list.current_version();
(
self.current_fence(version),
crdt::TaskListDelta::for_state_change(task_id, full_task, version),
advisory,
)
};
if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
tracing::warn!("failed to publish claim_task delta: {}", e);
}
Ok(TaskMutationOutcome::Committed { fence, advisory })
}
pub async fn complete_task(&self, task_id: crdt::TaskId) -> error::Result<()> {
self.complete_task_versioned(task_id, None)
.await
.map(|_| ())
}
pub async fn complete_task_versioned(
&self,
task_id: crdt::TaskId,
expected: Option<FenceToken>,
) -> error::Result<TaskMutationOutcome> {
let (fence, delta, advisory) = {
let mut list = self.sync.write().await;
if let Some(expected) = expected {
let current = list.current_version();
if expected.epoch != self.replica_epoch || expected.revision != current {
return Ok(TaskMutationOutcome::StaleLocalVersion {
current: self.current_fence(current),
});
}
}
let seq = list.next_seq();
list.complete_task(&task_id, self.agent_id, self.peer_id, seq, &self.signing)
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"complete_task failed: {}",
e
)))
})?;
let task = list.get_task(&task_id).ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"task disappeared after complete",
))
})?;
let winner = task.completion_record();
let advisory = AdvisoryOwnership {
agent: self.agent_id,
locally_winning: winner.is_some_and(|(a, _)| a == self.agent_id),
current_winner: winner,
};
let full_task = task.clone();
let version = list.current_version();
(
self.current_fence(version),
crdt::TaskListDelta::for_state_change(task_id, full_task, version),
advisory,
)
};
if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
tracing::warn!("failed to publish complete_task delta: {}", e);
}
Ok(TaskMutationOutcome::Committed { fence, advisory })
}
pub async fn list_tasks(&self) -> error::Result<Vec<TaskSnapshot>> {
let (tasks, _version) = self.list_tasks_with_version().await?;
Ok(tasks)
}
pub async fn list_tasks_with_version(&self) -> error::Result<(Vec<TaskSnapshot>, FenceToken)> {
let list = self.sync.read().await;
let version = list.current_version();
let tasks = list.tasks_ordered();
let snapshots = tasks
.into_iter()
.map(|task| {
let claim = task.claim_record();
let completion = task.completion_record();
TaskSnapshot {
id: *task.id(),
title: task.title().to_string(),
description: task.description().to_string(),
state: task.current_state(),
assignee: task.assignee().copied(),
owner: None,
priority: task.priority(),
claimed_by: claim.map(|(agent, _)| agent),
claimed_at: claim.map(|(_, ts)| ts),
completed_by: completion.map(|(agent, _)| agent),
completed_at: completion.map(|(_, ts)| ts),
}
})
.collect();
Ok((snapshots, self.current_fence(version)))
}
pub async fn version(&self) -> FenceToken {
let revision = self.sync.read().await.current_version();
self.current_fence(revision)
}
pub async fn reorder(&self, task_ids: Vec<crdt::TaskId>) -> error::Result<()> {
let delta = {
let mut list = self.sync.write().await;
list.reorder(task_ids.clone(), self.peer_id).map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"reorder failed: {}",
e
)))
})?;
crdt::TaskListDelta::for_reorder(
list.ordering_register().clone(),
list.current_version(),
)
};
if let Err(e) = self.sync.publish_delta(self.peer_id, delta).await {
tracing::warn!("failed to publish reorder delta: {}", e);
}
Ok(())
}
}
impl Agent {
pub async fn create_kv_store(&self, name: &str, topic: &str) -> error::Result<KvStoreHandle> {
self.create_kv_store_with_policy(name, topic, kv::AccessPolicy::Signed)
.await
}
pub async fn create_kv_store_with_policy(
&self,
name: &str,
topic: &str,
policy: kv::AccessPolicy,
) -> error::Result<KvStoreHandle> {
self.create_kv_store_inner(name, topic, policy, None).await
}
pub async fn create_kv_store_persistent(
&self,
name: &str,
topic: &str,
policy: kv::AccessPolicy,
state_dir: &std::path::Path,
) -> error::Result<KvStoreHandle> {
self.create_kv_store_inner(name, topic, policy, Some(state_dir))
.await
}
async fn create_kv_store_inner(
&self,
name: &str,
topic: &str,
policy: kv::AccessPolicy,
state_dir: Option<&std::path::Path>,
) -> error::Result<KvStoreHandle> {
let store_id = kv::KvStoreId::for_topic_owner(topic, &self.agent_id());
let persist_path = state_dir.map(|d| kv_snapshot_path(d, &store_id));
let store = match persist_path.as_deref().map(kv::sync::load_snapshot) {
Some(Ok(Some(snap))) => {
if snap.id() != &store_id {
return Err(kv_storage_err(format!(
"kv snapshot store-id mismatch for topic {topic}"
)));
}
if snap.owner() != Some(&self.agent_id()) {
return Err(kv_storage_err(format!(
"kv snapshot for topic {topic} is owned by a different agent"
)));
}
if matches!(policy, kv::AccessPolicy::AppendOnly)
&& !matches!(snap.policy(), kv::AccessPolicy::AppendOnly)
{
return Err(kv_storage_err(format!(
"kv snapshot for topic {topic} is not append_only but the store was registered append_only; refusing to downgrade"
)));
}
if matches!(snap.policy(), kv::AccessPolicy::AppendOnly)
&& !matches!(policy, kv::AccessPolicy::AppendOnly)
{
tracing::warn!(
"kv store {topic}: snapshot policy is terminal append_only; ignoring requested policy {policy}"
);
}
snap
}
Some(Ok(None)) | None => {
kv::KvStore::new(store_id, name.to_string(), self.agent_id(), policy)
}
Some(Err(e)) => {
return Err(kv_storage_err(format!(
"kv snapshot for topic {topic} is unreadable ({e}); refusing to start with amnesia — repair or remove the snapshot file explicitly"
)));
}
};
let (sync, peer_id) = self.spawn_kv_sync(store, topic, persist_path).await?;
let (pk_bytes, sk_bytes) = self.identity().agent_keypair().to_bytes();
Ok(KvStoreHandle {
sync,
agent_id: self.agent_id(),
peer_id,
owner_signing: Some(std::sync::Arc::new(OwnerSigningMaterial {
public_key_bytes: pk_bytes,
secret_key_bytes: sk_bytes,
})),
})
}
async fn spawn_kv_sync(
&self,
store: kv::KvStore,
topic: &str,
persist_path: Option<std::path::PathBuf>,
) -> error::Result<(std::sync::Arc<kv::KvStoreSync>, saorsa_gossip_types::PeerId)> {
let runtime = self.gossip_runtime.as_ref().ok_or_else(|| {
error::IdentityError::Storage(std::io::Error::other(
"gossip runtime not initialized - configure agent with network first",
))
})?;
let peer_id = runtime.peer_id();
let sync = kv::KvStoreSync::new(
store,
std::sync::Arc::clone(runtime.pubsub()),
topic.to_string(),
peer_id,
Some(self.agent_id()),
)
.map_err(|e| kv_storage_err(format!("kv store sync creation failed: {e}")))?;
let persistent = persist_path.is_some();
if let Some(path) = persist_path {
sync.set_persist_path(path);
}
let sync = std::sync::Arc::new(sync);
if persistent {
sync.persist().await.map_err(|e| {
kv_storage_err(format!(
"kv store snapshot dir is not writable ({e}); refusing to start a \
persistent store without durability"
))
})?;
}
sync.start_with_spawner(|fut| self.spawn_tracked(fut))
.await
.map_err(|e| kv_storage_err(format!("kv store sync start failed: {e}")))?;
Ok((sync, peer_id))
}
pub async fn join_kv_store(
&self,
topic: &str,
owner: identity::AgentId,
channel: kv::store::AnchorChannel,
) -> error::Result<KvStoreHandle> {
self.join_kv_store_inner(topic, owner, channel, None).await
}
pub async fn join_kv_store_persistent(
&self,
topic: &str,
owner: identity::AgentId,
channel: kv::store::AnchorChannel,
state_dir: &std::path::Path,
) -> error::Result<KvStoreHandle> {
self.join_kv_store_inner(topic, owner, channel, Some(state_dir))
.await
}
async fn join_kv_store_inner(
&self,
topic: &str,
owner: identity::AgentId,
channel: kv::store::AnchorChannel,
state_dir: Option<&std::path::Path>,
) -> error::Result<KvStoreHandle> {
let store_id = kv::KvStoreId::for_topic_owner(topic, &owner);
let persist_path = state_dir.map(|d| kv_snapshot_path(d, &store_id));
let store = match persist_path.as_deref().map(kv::sync::load_snapshot) {
Some(Ok(Some(snap))) => {
if snap.id() != &store_id {
return Err(kv_storage_err(format!(
"kv snapshot store-id mismatch for topic {topic}"
)));
}
if snap.owner() != Some(&owner) {
return Err(kv_storage_err(format!(
"kv snapshot for topic {topic} is anchored on a different owner"
)));
}
snap
}
Some(Ok(None)) | None => {
kv::KvStore::new_replica(store_id, String::new(), Some(owner), channel)
}
Some(Err(e)) => {
return Err(kv_storage_err(format!(
"kv snapshot for topic {topic} is unreadable ({e}); refusing to start with amnesia — repair or remove the snapshot file explicitly"
)));
}
};
let (sync, peer_id) = self.spawn_kv_sync(store, topic, persist_path).await?;
Ok(KvStoreHandle {
sync,
agent_id: self.agent_id(),
peer_id,
owner_signing: None,
})
}
}
fn kv_snapshot_path(dir: &std::path::Path, id: &kv::KvStoreId) -> std::path::PathBuf {
dir.join(format!("{}.bin", hex::encode(id.as_bytes())))
}
fn kv_storage_err(msg: String) -> error::IdentityError {
error::IdentityError::Storage(std::io::Error::other(msg))
}
#[derive(Clone)]
pub struct KvStoreHandle {
sync: std::sync::Arc<kv::KvStoreSync>,
agent_id: identity::AgentId,
peer_id: saorsa_gossip_types::PeerId,
owner_signing: Option<std::sync::Arc<OwnerSigningMaterial>>,
}
#[derive(Clone)]
pub struct OwnerSigningMaterial {
pub public_key_bytes: Vec<u8>,
pub secret_key_bytes: Vec<u8>,
}
impl std::fmt::Debug for KvStoreHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KvStoreHandle")
.field("agent_id", &self.agent_id)
.field("peer_id", &self.peer_id)
.finish_non_exhaustive()
}
}
impl KvStoreHandle {
#[must_use]
pub fn peer_id(&self) -> saorsa_gossip_types::PeerId {
self.peer_id
}
pub async fn ownership_info(&self) -> KvStoreOwnershipInfo {
let s = self.sync.read().await;
let src = s.ownership_source();
let (status, announced) = match &src {
kv::OwnershipSource::Anchored { .. } => (kv::OwnershipStatus::Anchored, None),
kv::OwnershipSource::Unknown => (kv::OwnershipStatus::Unknown, None),
kv::OwnershipSource::Conflict { announced, .. } => (
kv::OwnershipStatus::Conflict,
Some(hex::encode(announced.as_bytes())),
),
};
KvStoreOwnershipInfo {
owner: s.owner().map(|o| hex::encode(o.as_bytes())),
policy: s.policy().to_string(),
version: s.current_version(),
policy_version: s.policy_version(),
ownership_status: status,
announced_owner: announced,
durability_degraded: self.sync.durability_degraded(),
}
}
fn produce_checkpoint(&self, store: &mut kv::KvStore) -> Option<kv::store::OwnerCheckpoint> {
let signing = self.owner_signing.as_ref()?;
if store.owner() != Some(&self.agent_id) {
return None;
}
let topic = self.sync.topic();
let id = *store.id();
let pairs = store.checkpoint_pairs();
let root = kv::store::content_root(&id, store.name(), &pairs);
let seq = store.highest_checkpoint_seq.checked_add(1)?;
let policy_version = store.policy_version();
let policy = store.policy().clone();
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let pubkey = ant_quic::MlDsaPublicKey::from_bytes(&signing.public_key_bytes).ok()?;
let seckey = ant_quic::MlDsaSecretKey::from_bytes(&signing.secret_key_bytes).ok()?;
let cp = kv::store::make_owner_checkpoint(kv::store::OwnerCheckpointParams {
topic,
store_id: &id,
secret_key: &seckey,
public_key: &pubkey,
policy: &policy,
policy_version,
checkpoint_seq: seq,
content_root: root,
timestamp: ts,
})
.ok()?;
store.latest_checkpoint = Some(cp.clone());
store.highest_checkpoint_seq = seq;
Some(cp)
}
fn check_local_write(store: &kv::KvStore, writer: &identity::AgentId) -> error::Result<()> {
store.authorize_local_write(writer).map_err(|e| match e {
kv::KvError::Unauthorized(msg) => error::IdentityError::Unauthorized(msg),
other => error::IdentityError::Unauthorized(other.to_string()),
})
}
pub async fn put(
&self,
key: String,
value: Vec<u8>,
content_type: String,
) -> error::Result<()> {
let _ = self.put_with_delta(key, value, content_type).await?;
Ok(())
}
pub async fn put_with_delta(
&self,
key: String,
value: Vec<u8>,
content_type: String,
) -> error::Result<kv::KvStoreDelta> {
self.sync.ensure_durable().await.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"kv store durability degraded: snapshot persistence failing ({e}); \
local writes refused until a snapshot succeeds"
)))
})?;
let delta = {
let mut store = self.sync.write().await;
Self::check_local_write(&store, &self.agent_id)?;
let version_before = store.current_version();
store
.put(
key.clone(),
value.clone(),
content_type.clone(),
self.peer_id,
)
.map_err(|e| match e {
kv::KvError::ImmutableKey(k) => error::IdentityError::ImmutableKey(k),
other => error::IdentityError::Storage(std::io::Error::other(format!(
"kv put failed: {other}",
))),
})?;
if store.current_version() == version_before {
return Ok(kv::KvStoreDelta::new(version_before));
}
let entry = store.get(&key).cloned();
let version = store.current_version();
let mut delta = match entry {
Some(e) => {
kv::KvStoreDelta::for_put(key, e, (self.peer_id, store.next_seq()), version)
}
None => {
return Err(error::IdentityError::Storage(std::io::Error::other(
"kv put succeeded but entry was not readable",
)));
}
};
if let Some(cp) = self.produce_checkpoint(&mut store) {
delta.owner_checkpoint = Some(cp);
delta.name_update = Some(store.name_register().clone());
}
delta
};
self.sync.persist().await.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"kv write applied locally but snapshot persistence FAILED ({e}); \
delta not published; store is durability-degraded"
)))
})?;
if let Err(e) = self.sync.publish_delta(self.peer_id, delta.clone()).await {
tracing::warn!("failed to publish kv put delta: {e}");
}
Ok(delta)
}
pub async fn get(&self, key: &str) -> error::Result<Option<KvEntrySnapshot>> {
let store = self.sync.read().await;
Ok(store.get(key).map(|e| KvEntrySnapshot {
key: e.key.clone(),
value: e.value.clone(),
content_hash: hex::encode(e.content_hash),
content_type: e.content_type.clone(),
metadata: e.metadata.clone(),
created_at: e.created_at,
updated_at: e.updated_at,
}))
}
pub async fn remove(&self, key: &str) -> error::Result<()> {
let _ = self.remove_with_delta(key).await?;
Ok(())
}
pub async fn remove_with_delta(&self, key: &str) -> error::Result<kv::KvStoreDelta> {
self.sync.ensure_durable().await.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"kv store durability degraded: snapshot persistence failing ({e}); \
local writes refused until a snapshot succeeds"
)))
})?;
let delta = {
let mut store = self.sync.write().await;
Self::check_local_write(&store, &self.agent_id)?;
store.remove(key).map_err(|e| match e {
kv::KvError::ImmutableKey(k) => error::IdentityError::ImmutableKey(k),
other => error::IdentityError::Storage(std::io::Error::other(format!(
"kv remove failed: {other}",
))),
})?;
let mut d = kv::KvStoreDelta::new(store.current_version());
d.removed
.insert(key.to_string(), std::collections::HashSet::new());
if let Some(cp) = self.produce_checkpoint(&mut store) {
d.owner_checkpoint = Some(cp);
d.name_update = Some(store.name_register().clone());
}
d
};
self.sync.persist().await.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"kv remove applied locally but snapshot persistence FAILED ({e}); \
delta not published; store is durability-degraded"
)))
})?;
if let Err(e) = self.sync.publish_delta(self.peer_id, delta.clone()).await {
tracing::warn!("failed to publish kv remove delta: {e}");
}
Ok(delta)
}
pub async fn apply_remote_delta(
&self,
peer_id: saorsa_gossip_types::PeerId,
delta: &kv::KvStoreDelta,
writer: Option<identity::AgentId>,
) -> error::Result<()> {
{
let mut store = self.sync.write().await;
store
.merge_delta(delta, peer_id, writer.as_ref())
.map_err(|e| {
error::IdentityError::Storage(std::io::Error::other(format!(
"kv direct delta merge failed: {e}",
)))
})?;
}
let _ = self.sync.persist().await;
Ok(())
}
pub async fn keys(&self) -> error::Result<Vec<KvEntrySnapshot>> {
let store = self.sync.read().await;
Ok(store
.active_entries()
.into_iter()
.map(|e| KvEntrySnapshot {
key: e.key.clone(),
value: e.value.clone(),
content_hash: hex::encode(e.content_hash),
content_type: e.content_type.clone(),
metadata: e.metadata.clone(),
created_at: e.created_at,
updated_at: e.updated_at,
})
.collect())
}
pub async fn name(&self) -> error::Result<String> {
let store = self.sync.read().await;
Ok(store.name().to_string())
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct KvEntrySnapshot {
pub key: String,
pub value: Vec<u8>,
pub content_hash: String,
pub content_type: String,
pub metadata: std::collections::HashMap<String, String>,
pub created_at: u64,
pub updated_at: u64,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct KvStoreOwnershipInfo {
pub owner: Option<String>,
pub policy: String,
pub version: u64,
pub policy_version: u64,
pub ownership_status: kv::OwnershipStatus,
pub announced_owner: Option<String>,
pub durability_degraded: bool,
}
#[derive(Debug, Clone)]
pub struct TaskSnapshot {
pub id: crdt::TaskId,
pub title: String,
pub description: String,
pub state: crdt::CheckboxState,
pub assignee: Option<identity::AgentId>,
pub owner: Option<identity::UserId>,
pub priority: u8,
pub claimed_by: Option<identity::AgentId>,
pub claimed_at: Option<u64>,
pub completed_by: Option<identity::AgentId>,
pub completed_at: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskMutationOutcome {
Committed {
fence: FenceToken,
advisory: AdvisoryOwnership,
},
StaleLocalVersion {
current: FenceToken,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FenceToken {
pub epoch: u64,
pub revision: u64,
}
impl FenceToken {
#[must_use]
pub fn to_wire(&self) -> String {
format!("{}:{}", self.epoch, self.revision)
}
pub fn from_wire(s: &str) -> Result<Self, &'static str> {
let (epoch, revision) = s
.split_once(':')
.ok_or("malformed fence token: expected 'epoch:revision'")?;
let epoch = epoch
.parse()
.map_err(|_| "malformed fence token: epoch is not a valid u64")?;
let revision = revision
.parse()
.map_err(|_| "malformed fence token: revision is not a valid u64")?;
Ok(Self { epoch, revision })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdvisoryOwnership {
pub agent: identity::AgentId,
pub locally_winning: bool,
pub current_winner: Option<(identity::AgentId, u64)>,
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const NAME: &str = "x0x";
fn spawn_relay_dm_listener(
network: std::sync::Arc<network::NetworkNode>,
peer_relay: std::sync::Arc<peer_relay::PeerRelay>,
identity_discovery_cache: std::sync::Arc<
tokio::sync::RwLock<std::collections::HashMap<identity::AgentId, DiscoveredAgent>>,
>,
revocation_set: std::sync::Arc<tokio::sync::RwLock<revocation::RevocationSet>>,
contact_store: std::sync::Arc<tokio::sync::RwLock<contacts::ContactStore>>,
local_agent_id: identity::AgentId,
) {
tokio::spawn(async move {
tracing::info!(target: "x0x::relay", stage = "listener", "relay-DM listener started");
loop {
let Some((relay_peer_id, _relay_sender_agent_id, relayed)) =
network.recv_relayed_dm().await
else {
tracing::warn!(
target: "x0x::relay",
stage = "listener",
"network.recv_relayed_dm channel closed - listener exiting"
);
break;
};
let now_ms = dm::now_unix_ms();
let sender_agent_id = identity::AgentId(relayed.header.sender_agent_id);
let (is_sender_contact, is_sender_blocked) = {
let store = contact_store.read().await;
match store.get(&sender_agent_id) {
Some(c) => (
matches!(
c.trust_level,
contacts::TrustLevel::Known | contacts::TrustLevel::Trusted
),
c.trust_level == contacts::TrustLevel::Blocked,
),
None => (false, false),
}
};
let disposition = peer_relay.disposition_for(
&relayed,
&local_agent_id,
now_ms,
is_sender_contact,
is_sender_blocked,
);
if matches!(
disposition,
peer_relay::RelayDisposition::DeliverLocally
| peer_relay::RelayDisposition::Forward { .. }
) {
let origin = identity::AgentId(relayed.inner.sender_agent_id);
let revoked = { revocation_set.read().await.is_agent_revoked(&origin) };
if revoked {
peer_relay.record_relay_dropped_revoked();
tracing::info!(
target: "x0x::relay",
stage = "revoked_drop",
relay_peer = ?relay_peer_id,
origin = %hex::encode(origin.as_bytes()),
"relayed DM dropped: origin agent is revoked"
);
continue;
}
}
match disposition {
peer_relay::RelayDisposition::DeliverLocally => {
let sender_machine_id = relayed.inner.sender_machine_id;
let sender_peer_id = ant_quic::PeerId(sender_machine_id);
let inner_wire = match postcard::to_allocvec(&relayed.inner) {
Ok(b) => b,
Err(e) => {
tracing::warn!(
target: "x0x::relay",
stage = "deliver_local",
relay_peer = ?relay_peer_id,
error = %e,
"failed to re-encode inner envelope for local delivery"
);
continue;
}
};
let mut payload = Vec::with_capacity(32 + inner_wire.len());
payload.extend_from_slice(&relayed.inner.sender_agent_id);
payload.extend_from_slice(&inner_wire);
if let Err(e) = network
.inject_inbound_direct(sender_peer_id, bytes::Bytes::from(payload))
.await
{
tracing::warn!(
target: "x0x::relay",
stage = "deliver_local",
relay_peer = ?relay_peer_id,
error = %e,
"DeliverLocally inject onto direct channel failed"
);
}
}
peer_relay::RelayDisposition::Forward { dst_agent_id } => {
let dst = identity::AgentId(dst_agent_id);
let dst_machine_id = {
let cache = identity_discovery_cache.read().await;
cache.get(&dst).map(|d| d.machine_id)
};
let Some(dst_machine_id) = dst_machine_id else {
tracing::warn!(
target: "x0x::relay",
stage = "forward",
dst = %hex::encode(dst.as_bytes()),
"Forward dropped: dst not in identity-discovery cache"
);
continue;
};
let inner_wire = match postcard::to_allocvec(&relayed.inner) {
Ok(b) => b,
Err(e) => {
tracing::warn!(
target: "x0x::relay",
stage = "forward",
dst = %hex::encode(dst.as_bytes()),
error = %e,
"failed to re-encode inner envelope for forward"
);
continue;
}
};
let forward_bytes = u64::try_from(inner_wire.len()).unwrap_or(u64::MAX);
let reservation = match peer_relay
.reserve_forward(relayed.header.sender_agent_id, forward_bytes)
{
Ok(reservation) => reservation,
Err(reason) => {
tracing::debug!(
target: "x0x::relay",
stage = "refuse",
relay_peer = ?relay_peer_id,
reason = ?reason,
"RelayedDm forward refused by quota admission"
);
continue;
}
};
let dst_peer_id = ant_quic::PeerId(dst_machine_id.0);
match network
.send_direct_typed(
&dst_peer_id,
local_agent_id.as_bytes(),
network::DIRECT_MESSAGE_STREAM_TYPE,
&inner_wire,
)
.await
{
Ok(()) => reservation.commit(),
Err(e) => {
tracing::warn!(
target: "x0x::relay",
stage = "forward",
dst = %hex::encode(dst.as_bytes()),
error = %e,
"Forward send_direct_typed failed"
);
}
}
}
peer_relay::RelayDisposition::Refuse(reason) => {
tracing::debug!(
target: "x0x::relay",
stage = "refuse",
relay_peer = ?relay_peer_id,
reason = ?reason,
"RelayedDm refused"
);
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
fn sa(s: &str) -> std::net::SocketAddr {
s.parse().expect("valid SocketAddr literal in test")
}
#[test]
fn discovery_rebroadcast_is_one_shot_per_announcement_key() {
let now = std::time::Instant::now();
let mut state = std::collections::HashMap::new();
assert!(should_rebroadcast_discovery_once(
&mut state,
(7_u8, 42_u64),
now
));
assert!(!should_rebroadcast_discovery_once(
&mut state,
(7_u8, 42_u64),
now + std::time::Duration::from_secs(20),
));
assert!(should_rebroadcast_discovery_once(
&mut state,
(7_u8, 43_u64),
now + std::time::Duration::from_secs(20),
));
}
#[test]
fn discovery_ttl_uses_local_last_seen_not_sender_timestamp() {
let cutoff = 900;
assert!(discovery_record_is_live(100, 1_000, cutoff));
assert!(discovery_record_is_live(10_000, cutoff, cutoff));
assert!(!discovery_record_is_live(10_000, cutoff - 1, cutoff));
}
#[test]
fn is_publicly_advertisable_rejects_lan_and_special_scopes() {
assert!(
!is_publicly_advertisable(sa("127.0.0.1:5483")),
"loopback v4"
);
assert!(!is_publicly_advertisable(sa("10.1.2.3:5483")), "rfc1918 /8");
assert!(
!is_publicly_advertisable(sa("172.20.0.5:5483")),
"rfc1918 /12"
);
assert!(
!is_publicly_advertisable(sa("192.168.1.5:5483")),
"rfc1918 /16"
);
assert!(
!is_publicly_advertisable(sa("169.254.1.1:5483")),
"link-local v4"
);
assert!(
!is_publicly_advertisable(sa("100.64.1.1:5483")),
"CGNAT (unreachable outside carrier)"
);
assert!(
!is_publicly_advertisable(sa("0.0.0.0:5483")),
"unspecified v4"
);
assert!(!is_publicly_advertisable(sa("[::1]:5483")), "loopback v6");
assert!(
!is_publicly_advertisable(sa("[fe80::1]:5483")),
"link-local v6"
);
assert!(!is_publicly_advertisable(sa("[fd00::1]:5483")), "ULA v6");
assert!(
!is_publicly_advertisable(sa("1.2.3.4:0")),
"port 0 on global v4"
);
assert!(is_publicly_advertisable(sa("1.2.3.4:5483")), "global v4");
assert!(
is_publicly_advertisable(sa("[2001:db8::1]:5483")),
"global v6 (documentation doc but is_globally_routable permits)",
);
assert!(
is_publicly_advertisable(sa("8.8.8.8:9000")),
"global v4 on non-default port",
);
assert!(
!is_publicly_advertisable(sa("192.0.2.1:5483")),
"TEST-NET-1 documentation range"
);
assert!(
!is_publicly_advertisable(sa("203.0.113.10:5483")),
"TEST-NET-3 documentation range"
);
}
#[test]
fn public_address_filter_drops_global_discovery_unsafe_candidates() {
let filtered = filter_publicly_advertisable_addrs(vec![
sa("127.0.0.1:5483"),
sa("10.1.2.3:5483"),
sa("100.64.1.1:5483"),
sa("169.254.1.1:5483"),
sa("1.2.3.4:0"),
sa("[::1]:5483"),
sa("[fd00::1]:5483"),
sa("8.8.8.8:5483"),
sa("[2001:db8::1]:5483"),
]);
assert_eq!(filtered, vec![sa("8.8.8.8:5483"), sa("[2001:db8::1]:5483")]);
}
#[test]
fn local_discovery_filter_keeps_same_partition_candidates() {
let filtered = filter_discovery_announcement_addrs(
vec![
sa("127.0.0.1:5483"),
sa("10.1.2.3:5483"),
sa("100.64.1.1:5483"),
sa("169.254.1.1:5483"),
sa("1.2.3.4:0"),
sa("[::1]:5483"),
sa("[fd00::1]:5483"),
sa("8.8.8.8:5483"),
],
true,
);
assert_eq!(
filtered,
vec![
sa("127.0.0.1:5483"),
sa("10.1.2.3:5483"),
sa("100.64.1.1:5483"),
sa("[::1]:5483"),
sa("[fd00::1]:5483"),
sa("8.8.8.8:5483"),
],
);
}
#[test]
fn local_discovery_scope_tracks_bootstrap_partition() {
let mut config = network::NetworkConfig {
bootstrap_nodes: Vec::new(),
..network::NetworkConfig::default()
};
assert!(allow_local_discovery_addresses(&config));
config.bootstrap_nodes = vec![sa("127.0.0.1:5483"), sa("192.168.1.10:5483")];
assert!(allow_local_discovery_addresses(&config));
config.bootstrap_nodes = vec![sa("127.0.0.1:5483"), sa("8.8.8.8:5483")];
assert!(!allow_local_discovery_addresses(&config));
}
#[test]
fn local_direct_probe_addrs_prioritizes_same_lan_ipv4() {
let local = [std::net::Ipv4Addr::new(192, 168, 1, 212)];
let ranked = local_direct_probe_addrs_with_local_v4s(
&[
sa("100.118.167.101:27749"),
sa("192.168.0.1:27749"),
sa("192.168.1.108:27749"),
sa("[2a0d:3344:32d:2e10::1]:27749"),
sa("[fd7a:115c:a1e0::b01:a7ac]:27749"),
],
&local,
);
assert_eq!(
ranked,
vec![
sa("192.168.1.108:27749"),
sa("192.168.0.1:27749"),
sa("100.118.167.101:27749"),
],
);
}
#[tokio::test]
async fn announcement_builders_filter_global_discovery_addresses() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.build()
.await
.expect("agent");
let addresses = vec![
sa("192.168.1.5:5483"),
sa("100.64.1.1:5483"),
sa("1.2.3.4:0"),
sa("8.8.8.8:5483"),
sa("[fd00::1]:5483"),
sa("[2001:db8::1]:5483"),
];
let expected = vec![sa("8.8.8.8:5483"), sa("[2001:db8::1]:5483")];
let identity_announcement = agent
.build_identity_announcement_with_addrs(IdentityAnnouncementBuildOptions {
include_user_identity: false,
human_consent: false,
addresses: addresses.clone(),
assist_snapshot: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
allow_local_scope: false,
})
.expect("identity announcement");
assert_eq!(identity_announcement.addresses, expected);
identity_announcement
.verify()
.expect("filtered identity announcement verifies");
let machine_announcement = build_machine_announcement_for_identity(
&agent.identity,
addresses,
1,
None,
Vec::new(),
Vec::new(),
false,
)
.expect("machine announcement");
assert_eq!(machine_announcement.addresses, expected);
machine_announcement
.verify()
.expect("filtered machine announcement verifies");
}
#[test]
fn presence_parse_addr_hints_drops_private_scopes() {
let hints = vec![
"127.0.0.1:5483".to_string(),
"10.200.0.1:5483".to_string(),
"[fd00::1]:5483".to_string(),
"1.2.3.4:5483".to_string(),
"[2001:db8::1]:5483".to_string(),
"not-an-address".to_string(),
];
let parsed = presence::parse_addr_hints(&hints);
let got: Vec<String> = parsed.iter().map(|a| a.to_string()).collect();
assert_eq!(
got,
vec!["1.2.3.4:5483".to_string(), "[2001:db8::1]:5483".to_string()],
"only globally-advertisable addresses survive inbound parsing"
);
}
#[test]
fn name_is_palindrome() {
let name = NAME;
let reversed: String = name.chars().rev().collect();
assert_eq!(name, reversed, "x0x must be a palindrome");
}
#[test]
fn name_is_three_bytes() {
assert_eq!(NAME.len(), 3, "x0x must be exactly three bytes");
}
#[test]
fn name_is_ai_native() {
assert!(NAME.chars().all(|c| c.is_ascii_alphanumeric()));
}
#[test]
fn raw_quic_receive_ack_failure_falls_back_when_gossip_available() {
let err = error::NetworkError::ConnectionFailed(
"send_with_receive_ack failed: Connection closed: Superseded".to_string(),
);
assert!(
!Agent::raw_quic_error_should_stop_fallback(&err, true),
"transient raw ACK lifecycle churn should not suppress gossip fallback"
);
assert!(
Agent::raw_quic_error_should_stop_fallback(&err, false),
"without a gossip capability, the raw ACK failure remains terminal"
);
}
#[test]
fn raw_quic_receive_backpressure_falls_back_when_gossip_available() {
let err = error::NetworkError::RemoteReceiveBackpressured(
"send_with_receive_ack failed: Remote receive pipeline rejected payload: Backpressured"
.to_string(),
);
assert!(
!Agent::raw_quic_error_should_stop_fallback(&err, true),
"receiver congestion should allow gossip fallback"
);
assert!(
Agent::raw_quic_error_should_stop_fallback(&err, false),
"without a gossip capability, receiver congestion remains terminal"
);
assert!(matches!(
Agent::map_raw_quic_dm_error(err),
dm::DmError::ReceiverBackpressured { .. }
));
}
#[test]
fn raw_quic_payload_errors_still_stop_fallback() {
let err = error::NetworkError::PayloadTooLarge { size: 2, max: 1 };
assert!(Agent::raw_quic_error_should_stop_fallback(&err, true));
}
#[tokio::test]
async fn unusable_capability_advert_falls_back_to_contact_card() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
let target = identity::AgentId([7_u8; 32]);
let target_machine = identity::MachineId([9_u8; 32]);
agent.capability_store().insert(
target,
target_machine,
dm::DmCapabilities::pending(),
dm_capability::now_unix_ms(),
);
agent.contacts().write().await.add(contacts::Contact {
agent_id: target,
trust_level: contacts::TrustLevel::Trusted,
label: None,
added_at: 0,
last_seen: None,
identity_type: contacts::IdentityType::Known,
machines: Vec::new(),
dm_capabilities: Some(dm::DmCapabilities::v1_gossip_ready(vec![42_u8; 1184])),
});
let err = agent
.send_direct_with_config(
&target,
b"contact-card-capability".to_vec(),
dm::DmSendConfig {
require_gossip: true,
..dm::DmSendConfig::default()
},
)
.await
.expect_err("contact-card capability should be used before gossip runtime fails");
assert!(
matches!(err, dm::DmError::LocalGossipUnavailable(_)),
"unexpected error: {err:?}"
);
}
#[tokio::test]
async fn self_dm_uses_loopback_and_delivers_to_subscribers() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
let mut rx = agent.subscribe_direct();
let payload = b"loopback-self-dm".to_vec();
let receipt = agent
.send_direct_with_config(
&agent.agent_id(),
payload.clone(),
dm::DmSendConfig::default(),
)
.await
.expect("self-DM should use loopback path");
assert_eq!(receipt.path, dm::DmPath::Loopback);
let msg = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
.await
.expect("self-DM should be delivered promptly")
.expect("subscriber should remain open");
assert_eq!(msg.sender, agent.agent_id());
assert_eq!(msg.machine_id, agent.machine_id());
assert_eq!(msg.payload, payload);
assert!(msg.verified);
assert_eq!(msg.trust_decision, Some(trust::TrustDecision::Accept));
let diagnostics = agent.direct_messaging().diagnostics_snapshot();
assert_eq!(diagnostics.stats.outgoing_send_succeeded, 1);
assert_eq!(diagnostics.stats.outgoing_path_loopback, 1);
assert_eq!(diagnostics.stats.incoming_envelopes_total, 1);
assert_eq!(diagnostics.stats.incoming_delivered_to_subscribe, 1);
}
fn loopback_network_config() -> network::NetworkConfig {
network::NetworkConfig {
bind_addr: Some("127.0.0.1:0".parse().expect("loopback addr")),
bootstrap_nodes: Vec::new(),
..network::NetworkConfig::default()
}
}
fn normalize_loopback_addr(addr: std::net::SocketAddr) -> std::net::SocketAddr {
if addr.ip().is_unspecified() {
std::net::SocketAddr::new(
std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
addr.port(),
)
} else {
addr
}
}
#[tokio::test]
async fn shutdown_aborts_identity_heartbeat_task() {
struct DropFlag(std::sync::Arc<std::sync::atomic::AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, std::sync::atomic::Ordering::Release);
}
}
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let dropped_for_task = std::sync::Arc::clone(&dropped);
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
let handle = tokio::spawn(async move {
let _drop_flag = DropFlag(dropped_for_task);
let _ = started_tx.send(());
std::future::pending::<()>().await;
});
started_rx.await.expect("heartbeat task started");
*agent.heartbeat_handle.lock().await = Some(handle);
assert!(agent.heartbeat_handle.lock().await.is_some());
agent.shutdown().await;
assert!(agent.heartbeat_handle.lock().await.is_none());
assert!(dropped.load(std::sync::atomic::Ordering::Acquire));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn shutdown_drains_crdt_kv_sync_tasks() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("agent");
let baseline = agent
.tracked_tasks
.lock()
.expect("tracked_tasks")
.handles
.len();
agent
.create_kv_store("ws15-store", "ws15-store-topic")
.await
.expect("create kv store");
agent
.create_task_list("ws15-list", "ws15-list-topic")
.await
.expect("create task list");
let (registered, abort_handles): (usize, Vec<tokio::task::AbortHandle>) = {
let guard = agent.tracked_tasks.lock().expect("tracked_tasks");
let aborts = guard.handles.iter().map(|h| h.abort_handle()).collect();
(guard.handles.len(), aborts)
};
assert!(
registered > baseline,
"CRDT/KV sync loops must register with spawn_tracked \
(registry {registered}, baseline {baseline})"
);
agent.shutdown().await;
let after = agent.tracked_tasks.lock().expect("tracked_tasks");
assert!(
after.closed,
"tracked-task registry must be closed after shutdown"
);
assert!(
after.handles.is_empty(),
"tracked-task registry must be drained after shutdown ({} left)",
after.handles.len()
);
drop(after);
for handle in &abort_handles {
assert!(
handle.is_finished(),
"a CRDT/KV sync task did not terminate after shutdown"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn append_only_idempotent_seq_and_owner_restart_persistence() {
let dir = tempfile::tempdir().expect("tmpdir");
let state_dir = dir.path().join("kv-stores");
let build_agent = || async {
Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("agent")
};
let agent = build_agent().await;
let store = agent
.create_kv_store_persistent(
"log",
"ao-persist-topic",
kv::AccessPolicy::AppendOnly,
&state_dir,
)
.await
.expect("create persistent append_only store");
store
.put("k".to_string(), b"v1".to_vec(), "text/plain".to_string())
.await
.expect("first append");
assert_eq!(store.sync.read().await.highest_checkpoint_seq, 1);
store
.put("k".to_string(), b"v1".to_vec(), "text/plain".to_string())
.await
.expect("idempotent re-put accepted");
assert_eq!(
store.sync.read().await.highest_checkpoint_seq,
1,
"idempotent re-put must not advance the checkpoint sequence"
);
let err = store
.put("k".to_string(), b"v2".to_vec(), "text/plain".to_string())
.await
.expect_err("rewrite rejected");
assert!(matches!(err, error::IdentityError::ImmutableKey(_)));
store
.put("k2".to_string(), b"v2".to_vec(), "text/plain".to_string())
.await
.expect("second append");
assert_eq!(store.sync.read().await.highest_checkpoint_seq, 2);
agent.shutdown().await;
let agent2 = build_agent().await;
let restored = agent2
.create_kv_store_persistent(
"log",
"ao-persist-topic",
kv::AccessPolicy::Signed,
&state_dir,
)
.await
.expect("restore from snapshot");
{
let s = restored.sync.read().await;
assert_eq!(
*s.policy(),
kv::AccessPolicy::AppendOnly,
"snapshot's terminal append_only policy wins over the request"
);
assert_eq!(s.highest_checkpoint_seq, 2, "HWM survives restart");
assert!(s.get("k").is_some() && s.get("k2").is_some());
assert!(
s.seq_counter_value() >= 4,
"persisted seq-counter ceiling restored exactly (got {}, need >= 4)",
s.seq_counter_value()
);
assert!(
s.seq_counter_value() > s.current_version(),
"counter must exceed version (double mint per put) — a \
version floor would reuse tags"
);
}
let err = restored
.put(
"k".to_string(),
b"REWRITE".to_vec(),
"text/plain".to_string(),
)
.await
.expect_err("post-restart rewrite rejected");
assert!(matches!(err, error::IdentityError::ImmutableKey(_)));
agent2.shutdown().await;
let agent3 = build_agent().await;
let store_id = kv::KvStoreId::for_topic_owner("ao-persist-topic", &agent3.agent_id());
let snap_path = kv_snapshot_path(&state_dir, &store_id);
let good_bytes = std::fs::read(&snap_path).expect("snapshot exists");
std::fs::write(&snap_path, b"garbage").expect("corrupt");
assert!(
agent3
.create_kv_store_persistent(
"log",
"ao-persist-topic",
kv::AccessPolicy::AppendOnly,
&state_dir,
)
.await
.is_err(),
"corrupt snapshot must fail closed, not start empty"
);
std::fs::write(&snap_path, good_bytes).expect("restore snapshot");
let signed_store = agent3
.create_kv_store_persistent(
"plain",
"signed-persist-topic",
kv::AccessPolicy::Signed,
&state_dir,
)
.await
.expect("create signed persistent store");
drop(signed_store);
assert!(
agent3
.create_kv_store_persistent(
"plain",
"signed-persist-topic",
kv::AccessPolicy::AppendOnly,
&state_dir,
)
.await
.is_err(),
"append_only requested over a Signed snapshot must fail closed"
);
agent3.shutdown().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn kv_snapshot_covers_direct_delivery_and_concurrent_bursts() {
let dir = tempfile::tempdir().expect("tmpdir");
let state_dir = dir.path().join("kv-stores");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("agent");
let src = agent
.create_kv_store_persistent(
"src",
"dd-src-topic",
kv::AccessPolicy::AppendOnly,
&state_dir,
)
.await
.expect("create src");
let mut delta = src
.put_with_delta("k".to_string(), b"v".to_vec(), "text/plain".to_string())
.await
.expect("owner put");
delta.owner_checkpoint = None;
let replica_dir = dir.path().join("kv-stores-replica");
let replica = agent
.join_kv_store_persistent(
"dd-replica-topic",
agent.agent_id(),
kv::store::AnchorChannel::RestParam,
&replica_dir,
)
.await
.expect("join replica");
replica
.apply_remote_delta(replica.peer_id(), &delta, Some(agent.agent_id()))
.await
.expect("direct delivery merge");
let replica_path = kv_snapshot_path(
&replica_dir,
&kv::KvStoreId::for_topic_owner("dd-replica-topic", &agent.agent_id()),
);
let snap = kv::sync::load_snapshot(&replica_path)
.expect("load replica snapshot")
.expect("replica snapshot present");
assert_eq!(
snap.get("k").map(|e| e.value.clone()),
Some(b"v".to_vec()),
"direct-delivery mutation must be persisted (no unpersisted-merge window)"
);
let src_path = kv_snapshot_path(
&state_dir,
&kv::KvStoreId::for_topic_owner("dd-src-topic", &agent.agent_id()),
);
for round in 0..3u8 {
let mut tasks = Vec::new();
for i in 0..20u8 {
let s = src.clone();
tasks.push(tokio::spawn(async move {
s.put(
format!("burst-{round}-{i}"),
vec![round, i],
"application/octet-stream".to_string(),
)
.await
}));
}
for t in tasks {
t.await.expect("join").expect("burst put");
}
let store_version = src.sync.read().await.current_version();
let snap = kv::sync::load_snapshot(&src_path)
.expect("load src snapshot")
.expect("src snapshot present");
assert_eq!(
snap.current_version(),
store_version,
"round {round}: snapshot must not lag or regress the store version"
);
}
agent.shutdown().await;
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn kv_persist_failure_fails_closed_and_recovers() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tmpdir");
let state_dir = dir.path().join("kv-stores");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("agent");
let store = agent
.create_kv_store_persistent(
"degrade",
"degrade-topic",
kv::AccessPolicy::AppendOnly,
&state_dir,
)
.await
.expect("create");
store
.put("k1".to_string(), b"v1".to_vec(), "text/plain".to_string())
.await
.expect("healthy put");
assert!(!store.ownership_info().await.durability_degraded);
let mut topic_sub = agent
.subscribe("degrade-topic")
.await
.expect("subscribe main topic");
std::fs::set_permissions(&state_dir, std::fs::Permissions::from_mode(0o555))
.expect("chmod ro");
let err = store
.put("k2".to_string(), b"v2".to_vec(), "text/plain".to_string())
.await
.expect_err("persist failure must fail the local write");
assert!(
format!("{err}").contains("durability-degraded"),
"error names the durability failure; got: {err}"
);
assert!(
store.ownership_info().await.durability_degraded,
"degraded flag surfaced in store info"
);
let err = store
.put("k3".to_string(), b"v3".to_vec(), "text/plain".to_string())
.await
.expect_err("degraded store refuses further local writes");
assert!(
format!("{err}").contains("durability degraded"),
"gate error names degradation; got: {err}"
);
assert!(
store.get("k3").await.expect("read").is_none(),
"fail-closed BEFORE mutation: k3 must not exist even in memory"
);
assert!(
tokio::time::timeout(std::time::Duration::from_millis(800), topic_sub.recv())
.await
.is_err(),
"failed-persist local writes must not publish a delta (durability before announcement)"
);
let mut remote = kv::KvStoreDelta::new(50);
remote.added.insert(
"remote-k".to_string(),
(
kv::KvEntry::new(
"remote-k".to_string(),
b"rv".to_vec(),
"text/plain".to_string(),
),
(store.peer_id(), 999),
),
);
store
.apply_remote_delta(store.peer_id(), &remote, Some(agent.agent_id()))
.await
.expect("remote merge must CONTINUE while durability-degraded");
assert!(
store.get("remote-k").await.expect("read").is_some(),
"remote delta applied while degraded"
);
assert!(
store.ownership_info().await.durability_degraded,
"still degraded after the remote merge (its persist also failed) — degraded, not wedged"
);
std::fs::set_permissions(&state_dir, std::fs::Permissions::from_mode(0o755))
.expect("chmod rw");
store
.put("k4".to_string(), b"v4".to_vec(), "text/plain".to_string())
.await
.expect("recovered put");
assert!(!store.ownership_info().await.durability_degraded);
assert!(
tokio::time::timeout(std::time::Duration::from_secs(15), topic_sub.recv())
.await
.ok()
.flatten()
.is_some(),
"recovered put must publish its delta (subscription sees the topic)"
);
let snap_path = kv_snapshot_path(
&state_dir,
&kv::KvStoreId::for_topic_owner("degrade-topic", &agent.agent_id()),
);
let snap = kv::sync::load_snapshot(&snap_path)
.expect("load")
.expect("present");
for k in ["k1", "k2", "k4", "remote-k"] {
assert!(
snap.get(k).is_some(),
"recovered snapshot must contain {k} (k2/remote-k were applied in \
memory while degraded and are captured by the recovery persist)"
);
}
assert!(snap.get("k3").is_none(), "refused write never existed");
agent.shutdown().await;
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn kv_remove_persist_failure_fails_closed_and_recovers() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tmpdir");
let state_dir = dir.path().join("kv-stores");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("agent");
let store = agent
.create_kv_store_persistent(
"rm-degrade",
"rm-degrade-topic",
kv::AccessPolicy::Signed,
&state_dir,
)
.await
.expect("create");
for k in ["k1", "k2", "k3"] {
store
.put(k.to_string(), b"v".to_vec(), "text/plain".to_string())
.await
.expect("seed put");
}
std::fs::set_permissions(&state_dir, std::fs::Permissions::from_mode(0o555))
.expect("chmod ro");
let err = store
.remove("k1")
.await
.expect_err("persist failure must fail the local remove");
assert!(
format!("{err}").contains("durability-degraded"),
"error names the durability failure; got: {err}"
);
assert!(store.ownership_info().await.durability_degraded);
assert!(
store.get("k1").await.expect("read").is_none(),
"k1 tombstone applied in memory before the failed persist"
);
let err = store
.remove("k2")
.await
.expect_err("degraded store refuses further local removes");
assert!(
format!("{err}").contains("durability degraded"),
"gate error names degradation; got: {err}"
);
assert!(
store.get("k2").await.expect("read").is_some(),
"fail-closed BEFORE mutation: k2 still present"
);
std::fs::set_permissions(&state_dir, std::fs::Permissions::from_mode(0o755))
.expect("chmod rw");
store.remove("k2").await.expect("recovered remove");
assert!(!store.ownership_info().await.durability_degraded);
let snap_path = kv_snapshot_path(
&state_dir,
&kv::KvStoreId::for_topic_owner("rm-degrade-topic", &agent.agent_id()),
);
let snap = kv::sync::load_snapshot(&snap_path)
.expect("load")
.expect("present");
assert!(snap.get("k1").is_none(), "pending tombstone recovered");
assert!(snap.get("k2").is_none(), "post-recovery remove persisted");
assert!(snap.get("k3").is_some(), "untouched key retained");
agent.shutdown().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pre_restart_fence_token_rejected_at_same_revision_after_epoch_change() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("agent");
let mut handle = agent
.create_task_list("restart-fence", "restart-fence-topic")
.await
.expect("create task list");
let (task_id, _added_version) = handle
.add_task_versioned("task".to_string(), "d".to_string())
.await
.expect("add task");
let pre_restart_token = handle.version().await;
let revision = pre_restart_token.revision;
handle.set_replica_epoch_for_testing(pre_restart_token.epoch.wrapping_add(1));
let outcome = handle
.claim_task_versioned(task_id, Some(pre_restart_token))
.await
.expect("claim call");
match outcome {
crate::TaskMutationOutcome::StaleLocalVersion { current } => {
assert_eq!(
current.revision, revision,
"a rejected fence must not change the revision"
);
assert_ne!(
current.epoch, pre_restart_token.epoch,
"the echoed current token carries the new (post-restart) epoch"
);
}
other => panic!("expected StaleLocalVersion, got {other:?}"),
}
let tasks = handle.list_tasks().await.expect("list tasks");
let t = tasks
.iter()
.find(|t| t.id == task_id)
.expect("task present after rejection");
assert!(
t.state.is_empty(),
"a rejected fence must not mutate the task: {:?}",
t.state
);
let post_restart_token = handle.version().await;
assert_ne!(
post_restart_token.epoch, pre_restart_token.epoch,
"post-restart token carries the new epoch"
);
let outcome2 = handle
.claim_task_versioned(task_id, Some(post_restart_token))
.await
.expect("claim call 2");
assert!(
matches!(outcome2, crate::TaskMutationOutcome::Committed { .. }),
"a post-restart token at the current revision must commit"
);
agent.shutdown().await;
}
#[tokio::test]
async fn begin_shutdown_closes_registry_and_cancels_token() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
assert!(
!agent.shutdown_token.is_cancelled(),
"token must not be cancelled before shutdown begins"
);
assert!(
!agent.tracked_tasks.lock().expect("tracked_tasks").closed,
"registry must be open before shutdown begins"
);
agent.begin_shutdown();
assert!(
agent.shutdown_token.is_cancelled(),
"begin_shutdown must cancel the shutdown token"
);
assert!(
agent.tracked_tasks.lock().expect("tracked_tasks").closed,
"begin_shutdown must close the tracked-task registry"
);
}
#[tokio::test]
async fn spawn_tracked_refuses_after_begin_shutdown() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
agent.spawn_tracked(async {});
let before = agent
.tracked_tasks
.lock()
.expect("tracked_tasks")
.handles
.len();
assert_eq!(
before, 1,
"spawn_tracked must register a task before begin_shutdown"
);
agent.begin_shutdown();
agent.spawn_tracked(async {});
{
let after = agent.tracked_tasks.lock().expect("tracked_tasks");
assert_eq!(
after.handles.len(),
before,
"spawn_tracked must be a no-op (handle NOT pushed) after begin_shutdown \
closed the registry — a racing join_network would otherwise leak a task"
);
assert!(after.closed, "registry must remain closed");
}
agent.shutdown().await;
}
#[tokio::test]
async fn shutdown_is_idempotent_and_never_panics_on_second_call() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
agent.shutdown().await;
agent.shutdown().await;
let registry = agent.tracked_tasks.lock().expect("tracked_tasks");
assert!(
registry.closed,
"registry must remain closed after a double shutdown"
);
assert!(
registry.handles.is_empty(),
"registry must be drained after a double shutdown"
);
assert!(
agent.shutdown_token.is_cancelled(),
"token must remain cancelled after a double shutdown"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn shutdown_graces_a_token_respecting_tracked_task() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
let completed_gracefully = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let token = agent.shutdown_token.clone();
let cg = std::sync::Arc::clone(&completed_gracefully);
agent.spawn_tracked(async move {
token.cancelled().await;
cg.store(true, std::sync::atomic::Ordering::SeqCst);
});
agent.shutdown().await;
assert!(
completed_gracefully.load(std::sync::atomic::Ordering::SeqCst),
"token-respecting task must complete GRACEFULLY (set its flag before \
returning) — proves the grace-await precedes any force-abort"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn connected_peer_clears_stale_lifecycle_block_before_raw_send() {
let dir = tempfile::tempdir().expect("tmpdir");
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("alice");
let bob = Agent::builder()
.with_machine_key(dir.path().join("bob-machine.key"))
.with_agent_key_path(dir.path().join("bob-agent.key"))
.with_contact_store_path(dir.path().join("bob-contacts.json"))
.with_peer_cache_disabled()
.with_network_config(loopback_network_config())
.build()
.await
.expect("bob");
let bob_network = bob.network().expect("bob network");
let bob_addr = normalize_loopback_addr(bob_network.bound_addr().await.expect("bob bound"));
let alice_network = alice.network().expect("alice network");
let connected_peer = alice_network
.connect_addr(bob_addr)
.await
.expect("alice connects to bob");
assert_eq!(connected_peer.0, bob.machine_id().0);
let bob_peer = ant_quic::PeerId(bob.machine_id().0);
let connected_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < connected_deadline {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(alice_network.is_connected(&bob_peer).await);
alice
.direct_messaging()
.mark_connected(bob.agent_id(), bob.machine_id())
.await;
alice.direct_messaging().record_lifecycle_blocked(
bob.machine_id(),
Some(1),
"closed: stale test block",
);
assert!(alice
.direct_messaging()
.lifecycle_block_reason(&bob.machine_id())
.is_some());
let receipt = alice
.send_direct_with_config(
&bob.agent_id(),
b"stale-block-clear".to_vec(),
dm::DmSendConfig {
prefer_raw_quic_if_connected: true,
..dm::DmSendConfig::default()
},
)
.await
.expect("raw send should ignore and clear stale lifecycle block");
assert_eq!(receipt.path, dm::DmPath::RawQuic);
assert!(alice
.direct_messaging()
.lifecycle_block_reason(&bob.machine_id())
.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn proactive_reconnect_recovers_dropped_same_host_peer() {
let dir = tempfile::tempdir().expect("tmpdir");
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_dir(dir.path().join("alice-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("alice");
let bob = Agent::builder()
.with_machine_key(dir.path().join("bob-machine.key"))
.with_agent_key_path(dir.path().join("bob-agent.key"))
.with_contact_store_path(dir.path().join("bob-contacts.json"))
.with_peer_cache_dir(dir.path().join("bob-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("bob");
alice.start_network_event_listener();
let bob_network = bob.network().expect("bob network");
let bob_addr = normalize_loopback_addr(bob_network.bound_addr().await.expect("bob bound"));
let alice_network = alice.network().expect("alice network");
let bob_peer = ant_quic::PeerId(bob.machine_id().0);
let connected_peer = alice_network
.connect_addr(bob_addr)
.await
.expect("alice connects to bob");
assert_eq!(connected_peer.0, bob.machine_id().0);
let connected_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < connected_deadline {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"alice should be connected to bob after initial dial"
);
alice_network
.disconnect(&bob_peer)
.await
.expect("disconnect bob");
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert!(
!alice_network.is_connected(&bob_peer).await,
"alice should be disconnected after explicit disconnect"
);
let reconnect_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20);
while tokio::time::Instant::now() < reconnect_deadline {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"alice should have proactively reconnected to bob \
within the backoff window"
);
alice
.direct_messaging()
.mark_connected(bob.agent_id(), bob.machine_id())
.await;
let receipt = alice
.send_direct_with_config(
&bob.agent_id(),
b"post-reconnect-probe".to_vec(),
dm::DmSendConfig {
prefer_raw_quic_if_connected: true,
..dm::DmSendConfig::default()
},
)
.await
.expect("DM should succeed over the recovered connection");
assert_eq!(
receipt.path,
dm::DmPath::RawQuic,
"DM should use the raw QUIC path over the recovered connection"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn proactive_reconnect_default_global_bootstrap_same_host() {
let dir = tempfile::tempdir().expect("tmpdir");
let global_cfg = network::NetworkConfig {
bind_addr: Some("127.0.0.1:0".parse().expect("loopback addr")),
bootstrap_nodes: vec!["142.93.199.50:5483".parse().expect("wan seed")],
..network::NetworkConfig::default()
};
assert!(
!allow_local_discovery_addresses(&global_cfg),
"test precondition: config must have allow_local_scope=FALSE"
);
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_dir(dir.path().join("alice-peers"))
.with_network_config(global_cfg.clone())
.build()
.await
.expect("alice");
let bob = Agent::builder()
.with_machine_key(dir.path().join("bob-machine.key"))
.with_agent_key_path(dir.path().join("bob-agent.key"))
.with_contact_store_path(dir.path().join("bob-contacts.json"))
.with_peer_cache_dir(dir.path().join("bob-peers"))
.with_network_config(global_cfg)
.build()
.await
.expect("bob");
alice.start_network_event_listener();
let bob_network = bob.network().expect("bob network");
let bob_addr = normalize_loopback_addr(bob_network.bound_addr().await.expect("bob bound"));
let alice_network = alice.network().expect("alice network");
let bob_peer = ant_quic::PeerId(bob.machine_id().0);
let connected = alice_network
.connect_addr(bob_addr)
.await
.expect("alice connects to bob");
assert_eq!(connected.0, bob.machine_id().0);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < deadline {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"alice should be connected to bob"
);
let cached_addr = alice_network
.connect_cached_peer(bob_peer)
.await
.expect("bootstrap cache should retain bob's loopback address");
assert_eq!(
cached_addr.port(),
bob_addr.port(),
"connect_cached_peer must return bob's port — \
confirms bootstrap cache retains the connection address"
);
assert!(
match cached_addr.ip() {
std::net::IpAddr::V4(v4) => v4.is_loopback(),
std::net::IpAddr::V6(v6) => {
v6.is_loopback() || v6.to_ipv4().is_some_and(|v4| v4.is_loopback())
}
},
"connect_cached_peer must return a loopback address — \
confirms bootstrap cache does not scope-filter (got {cached_addr})"
);
alice_network
.disconnect(&bob_peer)
.await
.expect("disconnect bob");
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert!(
!alice_network.is_connected(&bob_peer).await,
"alice should be disconnected"
);
let reconnect_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20);
while tokio::time::Instant::now() < reconnect_deadline {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"alice should have reconnected to bob via bootstrap cache \
even with allow_local_scope=FALSE"
);
alice
.direct_messaging()
.mark_connected(bob.agent_id(), bob.machine_id())
.await;
let receipt = alice
.send_direct_with_config(
&bob.agent_id(),
b"post-reconnect-global-bootstrap".to_vec(),
dm::DmSendConfig {
prefer_raw_quic_if_connected: true,
..dm::DmSendConfig::default()
},
)
.await
.expect("DM should succeed over the recovered connection");
assert_eq!(receipt.path, dm::DmPath::RawQuic);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn transport_disconnect_is_reconnect_eligible_and_recovers() {
let dir = tempfile::tempdir().expect("tmpdir");
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_dir(dir.path().join("alice-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("alice");
alice.start_network_event_listener();
let alice_network = alice.network().expect("alice network");
let bob = Agent::builder()
.with_machine_key(dir.path().join("bob-machine.key"))
.with_agent_key_path(dir.path().join("bob-agent.key"))
.with_contact_store_path(dir.path().join("bob-contacts.json"))
.with_peer_cache_dir(dir.path().join("bob-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("bob");
let bob_addr = normalize_loopback_addr(
bob.network()
.expect("bob net")
.bound_addr()
.await
.expect("bob bound"),
);
let bob_peer = ant_quic::PeerId(bob.machine_id().0);
let connected = alice_network
.connect_addr(bob_addr)
.await
.expect("alice connects to bob");
assert_eq!(connected.0, bob.machine_id().0);
let reg = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < reg {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"bob connected before transport disconnect"
);
assert!(
network::DisconnectReason::Transport.reconnect_eligible(),
"Transport is the only reconnect-eligible reason"
);
alice_network
.disconnect_with_reason(&bob_peer, network::DisconnectReason::Transport)
.await
.expect("transport disconnect");
assert!(
!alice_network.is_reconnect_suppressed(bob.machine_id().0),
"Transport disconnect must NOT set a tombstone"
);
assert!(
!alice_network.is_connected(&bob_peer).await,
"bob disconnected after transport disconnect"
);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20);
while tokio::time::Instant::now() < deadline {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"Transport disconnect must be redialed within the backoff window"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn admin_revocation_shutdown_disconnect_suppresses_proactive_reconnect() {
let reasons = [
network::DisconnectReason::Admin,
network::DisconnectReason::Revocation,
network::DisconnectReason::Shutdown,
];
let dir = tempfile::tempdir().expect("tmpdir");
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_dir(dir.path().join("alice-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("alice");
alice.start_network_event_listener();
let alice_network = alice.network().expect("alice network");
for reason in reasons {
let tag = format!("{reason:?}");
let bob = Agent::builder()
.with_machine_key(dir.path().join(format!("{tag}-machine.key")))
.with_agent_key_path(dir.path().join(format!("{tag}-agent.key")))
.with_contact_store_path(dir.path().join(format!("{tag}-contacts.json")))
.with_peer_cache_dir(dir.path().join(format!("{tag}-peers")))
.with_network_config(loopback_network_config())
.build()
.await
.expect("bob");
let bob_network = bob.network().expect("bob network");
let bob_addr =
normalize_loopback_addr(bob_network.bound_addr().await.expect("bob bound"));
let bob_peer = ant_quic::PeerId(bob.machine_id().0);
let bob_id = bob.machine_id().0;
let connected = alice_network
.connect_addr(bob_addr)
.await
.expect("alice connects to bob");
assert_eq!(connected.0, bob.machine_id().0, "{tag}: connected to bob");
let reg = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < reg {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"{tag}: alice connected to bob before disconnect"
);
assert!(
!alice_network.is_reconnect_suppressed(bob_id),
"{tag}: no tombstone before disconnect"
);
assert!(
!reason.reconnect_eligible(),
"{tag}: non-transport reasons are not reconnect-eligible"
);
alice_network
.disconnect_with_reason(&bob_peer, reason)
.await
.expect("disconnect_with_reason");
assert!(
!alice_network.is_connected(&bob_peer).await,
"{tag}: bob disconnected immediately"
);
assert!(
alice_network.is_reconnect_suppressed(bob_id),
"{tag}: suppression tombstone set"
);
let guard = tokio::time::Instant::now() + std::time::Duration::from_secs(6);
let mut redialed = false;
while tokio::time::Instant::now() < guard {
if alice_network.is_connected(&bob_peer).await {
redialed = true;
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
assert!(
!redialed,
"{tag}: suppressed peer must not be redialed within the backoff window"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn suppressed_peer_announcement_is_not_auto_connected() {
let dir = tempfile::tempdir().expect("tmpdir");
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_dir(dir.path().join("alice-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("alice");
let alice_network = alice.network().expect("alice network");
let bob = Agent::builder()
.with_machine_key(dir.path().join("bob-machine.key"))
.with_agent_key_path(dir.path().join("bob-agent.key"))
.with_contact_store_path(dir.path().join("bob-contacts.json"))
.with_peer_cache_dir(dir.path().join("bob-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("bob");
let bob_network = bob.network().expect("bob network");
let bob_addr = normalize_loopback_addr(bob_network.bound_addr().await.expect("bob bound"));
let bob_peer = ant_quic::PeerId(bob.machine_id().0);
let bob_id = bob.machine_id().0;
alice_network
.connect_addr(bob_addr)
.await
.expect("alice connects bob");
let reg = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < reg {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
alice_network
.disconnect_with_reason(&bob_peer, network::DisconnectReason::Revocation)
.await
.expect("revoke bob");
assert!(
alice_network.is_reconnect_suppressed(bob_id),
"revoked bob must carry a suppression tombstone"
);
let suppressed = alice_network.is_reconnect_suppressed(bob_id);
let connected = alice_network.is_connected(&bob_peer).await;
assert!(
!announcement_should_auto_connect(suppressed, connected),
"a suppressed peer's announcement must not trigger auto-connect"
);
}
#[test]
fn announcement_auto_connect_predicate_truth_table() {
assert!(announcement_should_auto_connect(false, false));
assert!(!announcement_should_auto_connect(false, true));
assert!(!announcement_should_auto_connect(true, false));
assert!(!announcement_should_auto_connect(true, true));
}
#[test]
fn announcement_auto_connect_retry_cooldown_gates_redials() {
let now = std::time::Instant::now();
assert!(announcement_auto_connect_retry_allowed(None, now));
assert!(!announcement_auto_connect_retry_allowed(Some(now), now));
let inside =
now + ANNOUNCEMENT_AUTO_CONNECT_RETRY_COOLDOWN - std::time::Duration::from_millis(1);
assert!(!announcement_auto_connect_retry_allowed(Some(now), inside));
let at_boundary = now + ANNOUNCEMENT_AUTO_CONNECT_RETRY_COOLDOWN;
assert!(announcement_auto_connect_retry_allowed(
Some(now),
at_boundary
));
assert!(!announcement_auto_connect_retry_allowed(
Some(at_boundary),
now
));
}
#[test]
fn jittered_backoff_stays_within_bounds() {
for &base in RECONNECT_BACKOFF_DELAYS {
let lower = base.mul_f64(0.8);
let upper = base.mul_f64(1.2);
for _ in 0..10_000 {
let j = jittered_backoff_delay(base);
assert!(
j >= lower && j <= upper,
"jittered delay {j:?} out of [{lower:?}, {upper:?}] for base {base:?}"
);
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn wrong_pinned_peer_stays_disconnected_for_full_backoff_window() {
let dir = tempfile::tempdir().expect("tmpdir");
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_dir(dir.path().join("alice-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("alice");
alice.start_network_event_listener();
let alice_network = alice.network().expect("alice network");
let imposter = Agent::builder()
.with_machine_key(dir.path().join("imp-machine.key"))
.with_agent_key_path(dir.path().join("imp-agent.key"))
.with_contact_store_path(dir.path().join("imp-contacts.json"))
.with_peer_cache_dir(dir.path().join("imp-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("imposter");
let imp_network = imposter.network().expect("imposter network");
let imp_addr = normalize_loopback_addr(imp_network.bound_addr().await.expect("imp bound"));
let imp_peer = ant_quic::PeerId(imposter.machine_id().0);
let connected = alice_network
.connect_addr(imp_addr)
.await
.expect("alice connects to imposter");
assert_eq!(connected.0, imposter.machine_id().0);
let reg = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < reg {
if alice_network.is_connected(&imp_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(
alice_network.is_connected(&imp_peer).await,
"imposter connected before rejection"
);
alice_network
.disconnect_with_reason(&imp_peer, network::DisconnectReason::PolicyRejection)
.await
.expect("policy reject");
assert!(
!alice_network.is_connected(&imp_peer).await,
"imposter disconnected after policy rejection"
);
assert!(
alice_network.is_reconnect_suppressed(imposter.machine_id().0),
"PolicyRejection sets a permanent tombstone"
);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(34);
while tokio::time::Instant::now() < deadline {
assert!(
!alice_network.is_connected(&imp_peer).await,
"wrong pinned peer must stay disconnected for the full backoff \
window despite being in the bootstrap cache"
);
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pool_cap_eviction_does_not_churn_redial() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut one_conn_cfg = loopback_network_config();
one_conn_cfg.max_connections = 1;
let alice = Agent::builder()
.with_machine_key(dir.path().join("alice-machine.key"))
.with_agent_key_path(dir.path().join("alice-agent.key"))
.with_contact_store_path(dir.path().join("alice-contacts.json"))
.with_peer_cache_dir(dir.path().join("alice-peers"))
.with_network_config(one_conn_cfg)
.build()
.await
.expect("alice");
alice.start_network_event_listener();
let alice_network = alice.network().expect("alice network");
let bob = Agent::builder()
.with_machine_key(dir.path().join("bob-machine.key"))
.with_agent_key_path(dir.path().join("bob-agent.key"))
.with_contact_store_path(dir.path().join("bob-contacts.json"))
.with_peer_cache_dir(dir.path().join("bob-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("bob");
let charlie = Agent::builder()
.with_machine_key(dir.path().join("charlie-machine.key"))
.with_agent_key_path(dir.path().join("charlie-agent.key"))
.with_contact_store_path(dir.path().join("charlie-contacts.json"))
.with_peer_cache_dir(dir.path().join("charlie-peers"))
.with_network_config(loopback_network_config())
.build()
.await
.expect("charlie");
let bob_addr = normalize_loopback_addr(
bob.network()
.expect("bob net")
.bound_addr()
.await
.expect("bob bound"),
);
let charlie_addr = normalize_loopback_addr(
charlie
.network()
.expect("charlie net")
.bound_addr()
.await
.expect("charlie bound"),
);
let bob_peer = ant_quic::PeerId(bob.machine_id().0);
let charlie_peer = ant_quic::PeerId(charlie.machine_id().0);
let cb = alice_network
.connect_addr(bob_addr)
.await
.expect("connect bob");
assert_eq!(cb.0, bob.machine_id().0);
let reg = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < reg {
if alice_network.is_connected(&bob_peer).await {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
assert!(
alice_network.is_connected(&bob_peer).await,
"bob connected before eviction"
);
let cc = alice_network
.connect_addr(charlie_addr)
.await
.expect("connect charlie");
assert_eq!(cc.0, charlie.machine_id().0);
let evict_dl = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
while tokio::time::Instant::now() < evict_dl {
if !alice_network.is_connected(&bob_peer).await
&& alice_network.is_reconnect_suppressed(bob.machine_id().0)
{
break;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
assert!(
alice_network.is_reconnect_suppressed(bob.machine_id().0),
"evicted bob must carry a PoolEviction tombstone"
);
assert!(
!alice_network.is_connected(&bob_peer).await,
"evicted bob must be disconnected"
);
let guard = tokio::time::Instant::now() + std::time::Duration::from_secs(6);
while tokio::time::Instant::now() < guard {
assert!(
!alice_network.is_connected(&bob_peer).await,
"evicted bob must not be redialed (churn)"
);
assert!(
alice_network.is_connected(&charlie_peer).await,
"charlie must stay connected — no churn displacement"
);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
#[tokio::test]
async fn agent_peer_relay_defaults_to_disabled_when_unconfigured() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
assert!(
!agent.peer_relay().policy().enabled,
"default Agent must have the relay engine disabled"
);
assert!(
agent.relay_candidates().await.is_empty(),
"default Agent must have no relay candidates"
);
}
#[tokio::test]
async fn agent_peer_relay_honors_configured_policy_and_candidates() {
let dir = tempfile::tempdir().expect("tmpdir");
let candidate_a = [0xAA_u8; 32];
let candidate_b = [0xBB_u8; 32];
let mut net_cfg = loopback_network_config();
net_cfg.peer_relay = network::PeerRelayConfig {
enabled: true,
require_contact_to_relay: false,
fail_threshold: 7,
fail_window_ms: 90_000,
candidates: vec![hex::encode(candidate_a), hex::encode(candidate_b)],
..Default::default()
};
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(net_cfg)
.build()
.await
.expect("agent");
let policy = agent.peer_relay().policy();
assert!(policy.enabled, "configured `enabled = true` must propagate");
assert_eq!(policy.fail_threshold, 7);
assert_eq!(policy.fail_window, std::time::Duration::from_millis(90_000));
let candidates = agent.relay_candidates().await;
assert_eq!(candidates.len(), 2, "both TOML candidates seeded");
assert!(candidates.iter().any(|c| c.0 == candidate_a));
assert!(candidates.iter().any(|c| c.0 == candidate_b));
}
#[tokio::test]
async fn send_direct_failure_records_failure_on_peer_relay() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
let unreachable = identity::AgentId([0x42; 32]);
let result = agent
.send_direct_with_config(
&unreachable,
b"x0x-0070b-bookkeeping".to_vec(),
dm::DmSendConfig::default(),
)
.await;
assert!(
result.is_err(),
"no network configured - direct send must fail"
);
assert_eq!(
agent.peer_relay().tracked_peer_count(),
1,
"failure must have produced a per-peer relay-engine entry"
);
}
#[tokio::test]
async fn send_direct_self_loopback_does_not_disturb_peer_relay() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
let _receipt = agent
.send_direct_with_config(
&agent.agent_id(),
b"loopback".to_vec(),
dm::DmSendConfig::default(),
)
.await
.expect("loopback self-DM");
assert_eq!(
agent.peer_relay().tracked_peer_count(),
0,
"loopback path must not register with the relay engine"
);
}
#[tokio::test]
async fn try_relay_fallback_returns_no_candidate_when_list_empty() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut net_cfg = loopback_network_config();
net_cfg.peer_relay = network::PeerRelayConfig {
enabled: true,
require_contact_to_relay: false,
fail_threshold: 3,
fail_window_ms: 60_000,
candidates: Vec::new(),
..Default::default()
};
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(net_cfg)
.build()
.await
.expect("agent");
let to = identity::AgentId([0xCD; 32]);
let err = agent
.try_relay_fallback(&to, b"payload".to_vec(), &[0u8; 32])
.await
.expect_err("empty candidate list must short-circuit");
assert!(
matches!(err, dm::DmError::NoRelayCandidate),
"expected NoRelayCandidate, got {err:?}"
);
}
#[tokio::test]
async fn try_relay_fallback_returns_no_candidate_when_machine_id_uncached() {
let dir = tempfile::tempdir().expect("tmpdir");
let candidate_hex = hex::encode([0xEE_u8; 32]);
let mut net_cfg = loopback_network_config();
net_cfg.peer_relay = network::PeerRelayConfig {
enabled: true,
require_contact_to_relay: false,
fail_threshold: 3,
fail_window_ms: 60_000,
candidates: vec![candidate_hex],
..Default::default()
};
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(net_cfg)
.build()
.await
.expect("agent");
let to = identity::AgentId([0xCD; 32]);
let err = agent
.try_relay_fallback(&to, b"payload".to_vec(), &[0u8; 32])
.await
.expect_err("uncached candidate must short-circuit");
assert!(
matches!(err, dm::DmError::NoRelayCandidate),
"expected NoRelayCandidate, got {err:?}"
);
}
#[tokio::test]
async fn send_direct_below_threshold_does_not_attempt_relay() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut net_cfg = loopback_network_config();
net_cfg.peer_relay = network::PeerRelayConfig {
enabled: true,
require_contact_to_relay: false,
fail_threshold: 5,
fail_window_ms: 60_000,
candidates: vec![hex::encode([0xEE_u8; 32])],
..Default::default()
};
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(net_cfg)
.build()
.await
.expect("agent");
let to = identity::AgentId([0xCD; 32]);
let result = agent
.send_direct_with_config(&to, b"first-attempt".to_vec(), dm::DmSendConfig::default())
.await;
assert!(result.is_err(), "no usable transport - direct send fails");
let snap = agent.peer_relay().stats().snapshot();
assert_eq!(
snap.relay_sent, 0,
"below threshold must not engage the relay path"
);
}
#[tokio::test]
async fn send_direct_above_threshold_without_candidates_surfaces_direct_err() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut net_cfg = loopback_network_config();
net_cfg.peer_relay = network::PeerRelayConfig {
enabled: true,
require_contact_to_relay: false,
fail_threshold: 3,
fail_window_ms: 60_000,
candidates: Vec::new(),
..Default::default()
};
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(net_cfg)
.build()
.await
.expect("agent");
let to = identity::AgentId([0xCD; 32]);
for _ in 0..agent.peer_relay().policy().fail_threshold {
agent.peer_relay().record_direct_failure(&to);
}
assert!(
agent.peer_relay().needs_relay(&to),
"engine must say the peer now needs a relay"
);
let err = agent
.send_direct_with_config(&to, b"x0x-0070b".to_vec(), dm::DmSendConfig::default())
.await
.expect_err("send must still fail when the relay path has no candidates");
assert!(
!matches!(err, dm::DmError::NoRelayCandidate),
"relay-side errors must not leak - original direct error must surface, got {err:?}"
);
}
#[tokio::test]
async fn send_direct_disabled_policy_does_not_engage_relay_seed() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.build()
.await
.expect("agent");
let to = identity::AgentId([0xCD; 32]);
for _ in 0..10 {
agent.peer_relay().record_direct_failure(&to);
}
assert!(
!agent.peer_relay().needs_relay(&to),
"disabled policy must never trigger needs_relay"
);
let err = agent
.send_direct_with_config(&to, b"x0x-0070b".to_vec(), dm::DmSendConfig::default())
.await
.expect_err("no network - direct send must fail");
assert!(
!matches!(err, dm::DmError::NoRelayCandidate),
"disabled policy must not surface any relay-side error, got {err:?}"
);
assert_eq!(
agent.peer_relay().stats().snapshot().relay_sent,
0,
"disabled policy must not advance relay_sent"
);
}
#[tokio::test]
async fn relay_dm_listener_refuses_bad_signature_and_ticks_counter() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut net_cfg = loopback_network_config();
net_cfg.peer_relay = network::PeerRelayConfig {
enabled: true,
require_contact_to_relay: false,
fail_threshold: 3,
fail_window_ms: 60_000,
candidates: Vec::new(),
..Default::default()
};
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_contact_store_path(dir.path().join("contacts.json"))
.with_peer_cache_disabled()
.with_network_config(net_cfg)
.build()
.await
.expect("agent");
let network = agent
.network
.as_ref()
.expect("agent built with network config");
let sender = network.test_relayed_dm_sender();
let relayed = peer_relay::RelayedDm {
header: peer_relay::RelayHeader {
version: peer_relay::RelayHeader::VERSION,
dst_agent_id: agent.agent_id().0,
sender_agent_id: [0x42; 32],
sender_public_key: Vec::new(),
originated_at_unix_ms: dm::now_unix_ms(),
signature: Vec::new(),
},
inner: dm::DmEnvelope {
protocol_version: 1,
request_id: [0u8; 16],
sender_agent_id: [0x42; 32],
sender_machine_id: [0x43; 32],
recipient_agent_id: agent.agent_id().0,
created_at_unix_ms: 0,
expires_at_unix_ms: 0,
body: dm::DmBody::Payload(dm::DmPayload {
kem_ciphertext: Vec::new(),
body_nonce: [0u8; 12],
body_ciphertext: Vec::new(),
}),
signature: Vec::new(),
},
};
let relay_peer = ant_quic::PeerId([0xEE; 32]);
let relay_wire_sender = [0xEE; 32];
sender
.send((relay_peer, relay_wire_sender, relayed))
.await
.expect("relayed_dm channel must accept push");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
let snap = agent.peer_relay().stats().snapshot();
if snap.relay_refused_bad_signature == 1 {
break;
}
if std::time::Instant::now() >= deadline {
panic!(
"relay-DM listener did not tick relay_refused_bad_signature within 2s - \
snapshot: {snap:?}"
);
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
let snap = agent.peer_relay().stats().snapshot();
assert_eq!(snap.relay_refused_bad_signature, 1);
assert_eq!(snap.relay_received, 0, "bad-sig path must not deliver");
assert_eq!(snap.relay_forwarded, 0, "bad-sig path must not forward");
}
#[tokio::test]
async fn agent_creates() {
let agent = Agent::new().await;
assert!(agent.is_ok());
}
#[tokio::test]
async fn agent_joins_network() {
let agent = Agent::new().await.unwrap();
assert!(agent.join_network().await.is_ok());
}
#[tokio::test]
async fn agent_subscribes() {
let agent = Agent::new().await.unwrap();
assert!(agent.subscribe("test-topic").await.is_err());
}
#[tokio::test]
async fn identity_announcement_machine_signature_verifies() {
let agent = Agent::builder()
.with_network_config(network::NetworkConfig::default())
.build()
.await
.unwrap();
let announcement = agent.build_identity_announcement(false, false).unwrap();
assert_eq!(announcement.agent_id, agent.agent_id());
assert_eq!(announcement.machine_id, agent.machine_id());
assert!(announcement.user_id.is_none());
assert!(announcement.agent_certificate.is_none());
assert!(announcement.verify().is_ok());
}
#[tokio::test]
async fn identity_announcement_requires_human_consent() {
let agent = Agent::builder()
.with_network_config(network::NetworkConfig::default())
.build()
.await
.unwrap();
let err = agent.build_identity_announcement(true, false).unwrap_err();
assert!(
err.to_string().contains("explicit human consent"),
"unexpected error: {err}"
);
}
#[tokio::test]
async fn identity_announcement_with_user_requires_user_identity() {
let agent = Agent::builder()
.with_network_config(network::NetworkConfig::default())
.build()
.await
.unwrap();
let err = agent.build_identity_announcement(true, true).unwrap_err();
assert!(
err.to_string().contains("no user identity is configured"),
"unexpected error: {err}"
);
}
#[tokio::test]
async fn announce_identity_populates_discovery_cache() {
let user_key = identity::UserKeypair::generate().unwrap();
let agent = Agent::builder()
.with_network_config(network::NetworkConfig::default())
.with_user_key(user_key)
.build()
.await
.unwrap();
agent.announce_identity(true, true).await.unwrap();
let discovered = agent.discovered_agent(agent.agent_id()).await.unwrap();
let entry = discovered.expect("agent should discover its own announcement");
assert_eq!(entry.agent_id, agent.agent_id());
assert_eq!(entry.machine_id, agent.machine_id());
assert_eq!(entry.user_id, agent.user_id());
}
#[tokio::test]
async fn revoked_agent_fails_machine_verification_even_when_cached() {
let user_key = identity::UserKeypair::generate().unwrap();
let agent = Agent::builder()
.with_network_config(network::NetworkConfig::default())
.with_user_key(user_key)
.build()
.await
.unwrap();
agent.announce_identity(true, true).await.unwrap();
let agent_id = agent.agent_id();
let machine_id = agent.machine_id();
assert!(
agent
.is_agent_machine_verified(&agent_id, &machine_id)
.await,
"a cached, signed self-announcement must verify before revocation"
);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let record = revocation::RevocationRecord::sign(
revocation::RevokedSubject::Agent(agent_id),
agent.identity().agent_keypair().public_key(),
agent.identity().agent_keypair().secret_key(),
now,
Some("ep2 test: key compromised".to_string()),
)
.unwrap();
{
let set = agent.revocation_set();
let mut set = set.write().await;
set.verify_and_insert(record, None)
.expect("self-revocation must verify and insert");
}
assert!(
!agent
.is_agent_machine_verified(&agent_id, &machine_id)
.await,
"a revoked agent must never pass machine verification while cached"
);
}
#[test]
fn issuer_revocation_propagates_over_gossip_via_cache_cert_lookup() {
let user = identity::UserKeypair::generate().unwrap();
let issued_agent = identity::AgentKeypair::generate().unwrap();
let agent_id = issued_agent.agent_id();
let cert = identity::AgentCertificate::issue(&user, &issued_agent).unwrap();
let mut cache = std::collections::HashMap::new();
cache.insert(
agent_id,
DiscoveredAgent {
agent_id,
machine_id: identity::MachineId([0u8; 32]),
user_id: None,
addresses: Vec::new(),
announced_at: 0,
last_seen: 0,
machine_public_key: Vec::new(),
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
cert_not_after: cert.not_after(),
agent_certificate: Some(cert.clone()),
agent_public_key: cert.agent_public_key().to_vec(),
},
);
let subject_certs = collect_subject_certs(&cache);
let looked_up = subject_certs
.get(&agent_id)
.expect("the cache lookup must find the subject cert");
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let record = revocation::RevocationRecord::sign(
revocation::RevokedSubject::Agent(agent_id),
user.public_key(),
user.secret_key(),
now,
Some("issuer revokes compromised agent".to_string()),
)
.unwrap();
let mut set = revocation::RevocationSet::new();
assert!(
set.verify_and_insert(record.clone(), Some(looked_up))
.unwrap(),
"issuer-revocation must verify and insert with the cache-resolved cert"
);
assert!(
set.is_agent_revoked(&agent_id),
"the agent must now be revoked network-wide"
);
assert!(
revocation::RevocationSet::new()
.verify_and_insert(record, None)
.is_err(),
"an issuer-revocation must be rejected without the subject cert \
(the pre-fix gossip behavior this closes)"
);
}
#[test]
fn identity_announcement_backward_compat_no_nat_fields() {
use identity::{AgentId, MachineId};
#[derive(serde::Serialize, serde::Deserialize)]
struct OldIdentityAnnouncementUnsigned {
agent_id: AgentId,
machine_id: MachineId,
user_id: Option<identity::UserId>,
agent_certificate: Option<identity::AgentCertificate>,
machine_public_key: Vec<u8>,
addresses: Vec<std::net::SocketAddr>,
announced_at: u64,
}
let agent_id = AgentId([1u8; 32]);
let machine_id = MachineId([2u8; 32]);
let old = OldIdentityAnnouncementUnsigned {
agent_id,
machine_id,
user_id: None,
agent_certificate: None,
machine_public_key: vec![0u8; 10],
addresses: Vec::new(),
announced_at: 1234,
};
let bytes = bincode::serialize(&old).expect("serialize old announcement");
let result = bincode::deserialize::<IdentityAnnouncementUnsigned>(&bytes);
assert!(
result.is_err(),
"Old-format announcement should not decode as new struct (protocol upgrade required)"
);
}
#[tokio::test]
async fn register_announced_machine_skips_self_agent() {
let dir = tempfile::tempdir().expect("tmpdir");
let store = std::sync::Arc::new(tokio::sync::RwLock::new(contacts::ContactStore::new(
dir.path().join("contacts.json"),
)));
let own = identity::AgentId([1u8; 32]);
let own_machine = identity::MachineId([2u8; 32]);
let added = super::register_announced_machine(&store, own, own, own_machine).await;
assert!(!added, "self-announcement must not register a machine");
let store = store.read().await;
assert!(
store.machines(&own).is_empty(),
"self-agent must have no machine record after a self-announcement"
);
assert!(
store.list().iter().all(|contact| contact.agent_id != own),
"self-agent must not appear in the contact store at all after a \
self-announcement"
);
}
#[tokio::test]
async fn register_announced_machine_registers_foreign_agent() {
let dir = tempfile::tempdir().expect("tmpdir");
let store = std::sync::Arc::new(tokio::sync::RwLock::new(contacts::ContactStore::new(
dir.path().join("contacts.json"),
)));
let own = identity::AgentId([1u8; 32]);
let peer = identity::AgentId([9u8; 32]);
let peer_machine = identity::MachineId([7u8; 32]);
let added = super::register_announced_machine(&store, own, peer, peer_machine).await;
assert!(added, "foreign announcement must register a new machine");
let added_again = super::register_announced_machine(&store, own, peer, peer_machine).await;
assert!(
!added_again,
"re-announcing the same machine must be idempotent"
);
let store = store.read().await;
let machines = store.machines(&peer);
assert_eq!(machines.len(), 1, "exactly one machine record");
assert_eq!(machines[0].machine_id, peer_machine);
assert!(
store.machines(&own).is_empty(),
"own agent must still have no contact entry"
);
}
#[test]
fn announcement_assist_snapshot_uses_capabilities_not_activity() {
let status = ant_quic::NodeStatus {
nat_type: ant_quic::NatType::FullCone,
can_receive_direct: true,
relay_service_enabled: true,
coordinator_service_enabled: true,
is_relaying: false,
is_coordinating: false,
..Default::default()
};
let snapshot = AnnouncementAssistSnapshot::from_node_status(&status);
assert_eq!(snapshot.nat_type.as_deref(), Some("Full Cone"));
assert_eq!(snapshot.can_receive_direct, Some(true));
assert_eq!(snapshot.relay_capable, Some(true));
assert_eq!(snapshot.coordinator_capable, Some(true));
assert_eq!(snapshot.relay_active, Some(false));
assert_eq!(snapshot.coordinator_active, Some(false));
}
#[test]
fn identity_announcement_nat_fields_round_trip() {
use identity::{AgentId, MachineId};
let unsigned = IdentityAnnouncementUnsigned {
agent_id: AgentId([1u8; 32]),
machine_id: MachineId([2u8; 32]),
user_id: None,
agent_certificate: None,
machine_public_key: vec![0u8; 10],
addresses: Vec::new(),
announced_at: 9999,
nat_type: Some("FullCone".to_string()),
can_receive_direct: Some(true),
is_relay: Some(false),
is_coordinator: Some(true),
reachable_via: vec![MachineId([5u8; 32])],
relay_candidates: vec![MachineId([6u8; 32])],
};
let bytes = bincode::serialize(&unsigned).expect("serialize");
let decoded: IdentityAnnouncementUnsigned =
bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(decoded.nat_type.as_deref(), Some("FullCone"));
assert_eq!(decoded.can_receive_direct, Some(true));
assert_eq!(decoded.is_relay, Some(false));
assert_eq!(decoded.is_coordinator, Some(true));
assert_eq!(decoded.reachable_via, vec![MachineId([5u8; 32])]);
assert_eq!(decoded.relay_candidates, vec![MachineId([6u8; 32])]);
}
#[tokio::test]
async fn announcement_decode_helpers_match_bincode_serialize_wire_format() {
let temp = tempfile::tempdir().unwrap();
let agent = Agent::builder()
.with_machine_key(temp.path().join("machine.key"))
.with_agent_key_path(temp.path().join("agent.key"))
.with_agent_cert_path(temp.path().join("agent.cert"))
.with_contact_store_path(temp.path().join("contacts.json"))
.build()
.await
.unwrap();
let identity = agent.build_identity_announcement(false, false).unwrap();
let identity_bytes = serialize_identity_announcement(&identity).unwrap();
let decoded_identity = deserialize_identity_announcement(&identity_bytes).unwrap();
assert_eq!(decoded_identity.agent_id, identity.agent_id);
assert_eq!(decoded_identity.machine_id, identity.machine_id);
assert_eq!(decoded_identity.agent_public_key, identity.agent_public_key);
assert!(
!decoded_identity.agent_public_key.is_empty(),
"v2 announcement must carry the agent public key"
);
let machine = agent.build_machine_announcement().unwrap();
let machine_bytes = bincode::serialize(&machine).unwrap();
let decoded_machine = deserialize_machine_announcement(&machine_bytes).unwrap();
assert_eq!(decoded_machine.machine_id, machine.machine_id);
assert_eq!(decoded_machine.addresses, machine.addresses);
}
#[tokio::test]
async fn deserialize_identity_announcement_rejects_trailing_bytes() {
let temp = tempfile::tempdir().unwrap();
let agent = Agent::builder()
.with_machine_key(temp.path().join("machine.key"))
.with_agent_key_path(temp.path().join("agent.key"))
.with_agent_cert_path(temp.path().join("agent.cert"))
.with_contact_store_path(temp.path().join("contacts.json"))
.build()
.await
.unwrap();
let announcement = agent.build_identity_announcement(false, false).unwrap();
let mut bytes = bincode::serialize(&announcement).unwrap();
bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
assert!(
deserialize_identity_announcement(&bytes).is_err(),
"identity announcements with trailing bytes must be rejected"
);
}
#[test]
fn identity_announcement_no_nat_fields_round_trip() {
use identity::{AgentId, MachineId};
let unsigned = IdentityAnnouncementUnsigned {
agent_id: AgentId([3u8; 32]),
machine_id: MachineId([4u8; 32]),
user_id: None,
agent_certificate: None,
machine_public_key: vec![0u8; 10],
addresses: Vec::new(),
announced_at: 42,
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
};
let bytes = bincode::serialize(&unsigned).expect("serialize");
let decoded: IdentityAnnouncementUnsigned =
bincode::deserialize(&bytes).expect("deserialize");
assert!(decoded.nat_type.is_none());
assert!(decoded.can_receive_direct.is_none());
assert!(decoded.is_relay.is_none());
assert!(decoded.is_coordinator.is_none());
assert!(decoded.reachable_via.is_empty());
assert!(decoded.relay_candidates.is_empty());
}
#[test]
fn identity_announcement_reachable_via_round_trip() {
use identity::{AgentId, MachineId};
let coord_a = MachineId([0xAAu8; 32]);
let coord_b = MachineId([0xBBu8; 32]);
let unsigned = IdentityAnnouncementUnsigned {
agent_id: AgentId([9u8; 32]),
machine_id: MachineId([8u8; 32]),
user_id: None,
agent_certificate: None,
machine_public_key: vec![0u8; 10],
addresses: Vec::new(),
announced_at: 555,
nat_type: Some("Symmetric".to_string()),
can_receive_direct: Some(false),
is_relay: Some(false),
is_coordinator: Some(false),
reachable_via: vec![coord_a, coord_b],
relay_candidates: vec![coord_a],
};
let bytes = bincode::serialize(&unsigned).expect("serialize");
let decoded: IdentityAnnouncementUnsigned =
bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(decoded.reachable_via, vec![coord_a, coord_b]);
assert_eq!(decoded.relay_candidates, vec![coord_a]);
let re_encoded = bincode::serialize(&decoded).expect("re-serialize");
assert_eq!(
bytes, re_encoded,
"canonical bincode round-trip must be stable"
);
}
#[test]
fn user_announcement_sign_and_verify() {
let user_kp = identity::UserKeypair::generate().unwrap();
let agent_kp_a = identity::AgentKeypair::generate().unwrap();
let agent_kp_b = identity::AgentKeypair::generate().unwrap();
let cert_a = identity::AgentCertificate::issue(&user_kp, &agent_kp_a).unwrap();
let cert_b = identity::AgentCertificate::issue(&user_kp, &agent_kp_b).unwrap();
let announcement = UserAnnouncement::sign(&user_kp, vec![cert_a, cert_b], 1234).unwrap();
announcement.verify().expect("freshly-signed must verify");
assert_eq!(announcement.user_id, user_kp.user_id());
assert_eq!(announcement.agent_certificates.len(), 2);
}
#[test]
fn deserialize_user_announcement_rejects_trailing_bytes() {
let user_kp = identity::UserKeypair::generate().unwrap();
let agent_kp = identity::AgentKeypair::generate().unwrap();
let cert = identity::AgentCertificate::issue(&user_kp, &agent_kp).unwrap();
let announcement = UserAnnouncement::sign(&user_kp, vec![cert], 1234).unwrap();
let mut bytes = bincode::serialize(&announcement).unwrap();
bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
assert!(
deserialize_user_announcement(&bytes).is_err(),
"user announcements with trailing bytes must be rejected"
);
}
#[test]
fn user_announcement_rejects_foreign_certificate() {
let user_kp = identity::UserKeypair::generate().unwrap();
let other_user = identity::UserKeypair::generate().unwrap();
let agent_kp = identity::AgentKeypair::generate().unwrap();
let foreign_cert = identity::AgentCertificate::issue(&other_user, &agent_kp).unwrap();
let err = UserAnnouncement::sign(&user_kp, vec![foreign_cert], 0).unwrap_err();
assert!(
err.to_string().contains("different user"),
"unexpected error: {err}"
);
}
#[test]
fn user_announcement_tampered_agent_cert_list_fails() {
let user_kp = identity::UserKeypair::generate().unwrap();
let agent_kp = identity::AgentKeypair::generate().unwrap();
let cert = identity::AgentCertificate::issue(&user_kp, &agent_kp).unwrap();
let mut announcement = UserAnnouncement::sign(&user_kp, vec![cert], 10).unwrap();
let other_user = identity::UserKeypair::generate().unwrap();
let other_agent = identity::AgentKeypair::generate().unwrap();
let foreign_cert = identity::AgentCertificate::issue(&other_user, &other_agent).unwrap();
announcement.agent_certificates.push(foreign_cert);
assert!(
announcement.verify().is_err(),
"announcement with appended foreign cert must fail verification"
);
}
#[test]
fn user_announcement_tampered_user_public_key_fails() {
let user_kp = identity::UserKeypair::generate().unwrap();
let agent_kp = identity::AgentKeypair::generate().unwrap();
let cert = identity::AgentCertificate::issue(&user_kp, &agent_kp).unwrap();
let mut announcement = UserAnnouncement::sign(&user_kp, vec![cert], 10).unwrap();
let other = identity::UserKeypair::generate().unwrap();
announcement.user_public_key = other.public_key().as_bytes().to_vec();
assert!(announcement.verify().is_err());
}
#[test]
fn user_shard_topic_is_deterministic() {
let user_id = identity::UserId([5u8; 32]);
let topic_a = shard_topic_for_user(&user_id);
let topic_b = shard_topic_for_user(&user_id);
assert_eq!(topic_a, topic_b);
assert!(topic_a.starts_with("x0x.user.shard.v2."));
}
}
#[test]
fn agent_shard_topic_is_deterministic() {
let agent_id = identity::AgentId([6u8; 32]);
let topic_a = shard_topic_for_agent(&agent_id);
let topic_b = shard_topic_for_agent(&agent_id);
assert_eq!(topic_a, topic_b);
assert!(topic_a.starts_with("x0x.identity.shard.v2."));
}
#[test]
fn machine_shard_topic_is_deterministic() {
let machine_id = identity::MachineId([7u8; 32]);
let topic_a = shard_topic_for_machine(&machine_id);
let topic_b = shard_topic_for_machine(&machine_id);
assert_eq!(topic_a, topic_b);
assert!(topic_a.starts_with("x0x.machine.shard.v2."));
}
#[test]
fn rendezvous_shard_topic_is_deterministic() {
let agent_id = identity::AgentId([8u8; 32]);
let topic_a = rendezvous_shard_topic_for_agent(&agent_id);
let topic_b = rendezvous_shard_topic_for_agent(&agent_id);
assert_eq!(topic_a, topic_b);
assert!(topic_a.starts_with("x0x.rendezvous.shard."));
}
#[test]
fn different_ids_produce_different_shard_topics() {
let agent_a = identity::AgentId([1u8; 32]);
let agent_b = identity::AgentId([2u8; 32]);
let topic_a = shard_topic_for_agent(&agent_a);
let topic_b = shard_topic_for_agent(&agent_b);
assert_ne!(
topic_a, topic_b,
"different agent IDs should produce different shard topics"
);
}
#[test]
fn collect_local_interface_addrs_returns_non_empty() {
let addrs = collect_local_interface_addrs(5483);
assert!(!addrs.is_empty(), "should find at least one interface");
for addr in &addrs {
assert_eq!(addr.port(), 5483, "all addrs should use port 5483");
}
}
#[test]
fn collect_local_interface_addrs_returns_reasonable_results() {
let addrs = collect_local_interface_addrs(9000);
assert!(!addrs.is_empty(), "should find at least one interface");
for addr in &addrs {
assert_eq!(addr.port(), 9000, "all addrs should use port 9000");
}
}
#[test]
fn is_globally_routable_v4_private() {
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(10, 0, 0, 1)
)));
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(172, 16, 0, 1)
)));
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(192, 168, 1, 1)
)));
}
#[test]
fn is_globally_routable_v4_global() {
assert!(is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(8, 8, 8, 8)
)));
assert!(is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(1, 2, 3, 4)
)));
}
#[test]
fn is_globally_routable_v6_private() {
assert!(!is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)
)));
assert!(!is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1)
)));
assert!(!is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)
)));
}
#[test]
fn is_globally_routable_v6_global() {
assert!(is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)
)));
assert!(is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888)
)));
}
#[test]
fn is_globally_routable_v4_cgnat() {
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(100, 64, 0, 1)
)));
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(100, 127, 255, 255)
)));
}
#[test]
fn is_globally_routable_v4_documentation() {
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(192, 0, 2, 1)
)));
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(198, 51, 100, 1)
)));
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(203, 0, 113, 1)
)));
}
#[test]
fn is_globally_routable_v4_broadcast() {
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(255, 255, 255, 255)
)));
}
#[test]
fn is_globally_routable_v4_unspecified() {
assert!(!is_globally_routable(std::net::IpAddr::V4(
std::net::Ipv4Addr::new(0, 0, 0, 0)
)));
}
#[test]
fn is_globally_routable_v6_unspecified() {
assert!(!is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)
)));
}
#[test]
fn is_globally_routable_v6_unique_local() {
assert!(!is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1)
)));
}
#[test]
fn is_globally_routable_v6_site_local() {
assert!(!is_globally_routable(std::net::IpAddr::V6(
std::net::Ipv6Addr::new(0xfec0, 0, 0, 0, 0, 0, 0, 1)
)));
}
#[test]
fn push_unique_adds_new_item() {
let mut items = vec![1, 2, 3];
push_unique(&mut items, 4);
assert_eq!(items, vec![1, 2, 3, 4]);
}
#[test]
fn push_unique_skips_existing_item() {
let mut items = vec![1, 2, 3];
push_unique(&mut items, 2);
assert_eq!(items, vec![1, 2, 3]);
}
#[test]
fn push_unique_works_with_empty() {
let mut items: Vec<i32> = vec![];
push_unique(&mut items, 42);
assert_eq!(items, vec![42]);
}
#[cfg(test)]
fn discovered_agent_fixture(
tag: u8,
announced_at: u64,
addrs: &[&str],
user_id: Option<identity::UserId>,
) -> DiscoveredAgent {
DiscoveredAgent {
agent_id: identity::AgentId([tag; 32]),
machine_id: identity::MachineId([tag; 32]),
user_id,
addresses: addrs
.iter()
.map(|a| a.parse().expect("valid socket addr"))
.collect(),
announced_at,
last_seen: announced_at,
machine_public_key: vec![tag],
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: Vec::new(),
}
}
#[cfg(test)]
fn signed_identity_announcement_fixture(
agent_id: identity::AgentId,
machine: &identity::MachineKeypair,
announced_at: u64,
) -> IdentityAnnouncement {
let machine_public_key = machine.public_key().as_bytes().to_vec();
let unsigned = IdentityAnnouncementUnsigned {
agent_id,
machine_id: machine.machine_id(),
user_id: None,
agent_certificate: None,
machine_public_key: machine_public_key.clone(),
addresses: Vec::new(),
announced_at,
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
};
let unsigned_bytes = bincode::serialize(&unsigned).expect("serialize announcement");
let machine_signature = ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
machine.secret_key(),
&unsigned_bytes,
)
.expect("sign announcement")
.as_bytes()
.to_vec();
IdentityAnnouncement {
agent_id,
machine_id: machine.machine_id(),
user_id: None,
agent_certificate: None,
machine_public_key,
machine_signature,
addresses: Vec::new(),
announced_at,
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
agent_public_key: Vec::new(),
}
}
#[cfg(test)]
fn verified_identity_origin_message(sender: &identity::AgentKeypair) -> gossip::PubSubMessage {
gossip::PubSubMessage {
topic: "identity-ingest-test".to_string(),
payload: bytes::Bytes::new(),
sender: Some(sender.agent_id()),
sender_public_key: Some(sender.public_key().as_bytes().to_vec()),
verified: true,
trust_level: None,
}
}
#[tokio::test]
async fn direct_origin_identity_ingest_populates_authenticated_binding() {
let sender = identity::AgentKeypair::generate().expect("sender keygen");
let machine = identity::MachineKeypair::generate().expect("machine keygen");
let now = 1_000;
let announcement = signed_identity_announcement_fixture(sender.agent_id(), &machine, now);
announcement.verify().expect("valid machine announcement");
let message = verified_identity_origin_message(&sender);
let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
dm_inbox::AuthenticatedMachineBindingCache::default(),
));
assert!(
record_authenticated_machine_binding_from_message(&bindings, &message, &announcement, now,)
.await
);
assert_eq!(
dm_inbox::authenticated_machine_binding_for_testing(&bindings, &sender.agent_id()).await,
Some(machine.machine_id())
);
}
#[tokio::test]
async fn wrong_origin_machine_announcement_cannot_populate_or_overwrite_binding() {
let victim = identity::AgentKeypair::generate().expect("victim keygen");
let attacker = identity::AgentKeypair::generate().expect("attacker keygen");
let trusted_machine = identity::MachineKeypair::generate().expect("trusted machine keygen");
let attacker_machine = identity::MachineKeypair::generate().expect("attacker machine keygen");
let now = 2_000;
let trusted = signed_identity_announcement_fixture(victim.agent_id(), &trusted_machine, now);
let trusted_message = verified_identity_origin_message(&victim);
let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
dm_inbox::AuthenticatedMachineBindingCache::default(),
));
assert!(
record_authenticated_machine_binding_from_message(
&bindings,
&trusted_message,
&trusted,
now,
)
.await
);
let poisoned =
signed_identity_announcement_fixture(victim.agent_id(), &attacker_machine, now + 1);
poisoned.verify().expect("valid attacker machine signature");
let attacker_message = verified_identity_origin_message(&attacker);
assert!(
!record_authenticated_machine_binding_from_message(
&bindings,
&attacker_message,
&poisoned,
now + 1,
)
.await
);
assert_eq!(
dm_inbox::authenticated_machine_binding_for_testing(&bindings, &victim.agent_id()).await,
Some(trusted_machine.machine_id())
);
}
#[tokio::test]
async fn verified_rebroadcast_cannot_populate_or_overwrite_authenticated_binding() {
let origin = identity::AgentKeypair::generate().expect("origin keygen");
let relay = identity::AgentKeypair::generate().expect("relay keygen");
let machine_a = identity::MachineKeypair::generate().expect("machine A keygen");
let machine_b = identity::MachineKeypair::generate().expect("machine B keygen");
let now = 3_000;
let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
dm_inbox::AuthenticatedMachineBindingCache::default(),
));
let direct = signed_identity_announcement_fixture(origin.agent_id(), &machine_a, now);
assert!(
record_authenticated_machine_binding_from_message(
&bindings,
&verified_identity_origin_message(&origin),
&direct,
now,
)
.await
);
let rebroadcast = signed_identity_announcement_fixture(origin.agent_id(), &machine_b, now + 1);
let relay_message = verified_identity_origin_message(&relay);
assert!(
!record_authenticated_machine_binding_from_message(
&bindings,
&relay_message,
&rebroadcast,
now + 1,
)
.await
);
assert_eq!(
dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
Some(machine_a.machine_id())
);
}
#[tokio::test]
async fn missing_or_invalid_sender_key_cannot_populate_authenticated_binding() {
let origin = identity::AgentKeypair::generate().expect("origin keygen");
let machine = identity::MachineKeypair::generate().expect("machine keygen");
let now = 4_000;
let announcement = signed_identity_announcement_fixture(origin.agent_id(), &machine, now);
let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
dm_inbox::AuthenticatedMachineBindingCache::default(),
));
let mut missing = verified_identity_origin_message(&origin);
missing.sender_public_key = None;
assert!(
!record_authenticated_machine_binding_from_message(
&bindings,
&missing,
&announcement,
now,
)
.await
);
let mut invalid = verified_identity_origin_message(&origin);
invalid.sender_public_key = Some(vec![0xFF; 8]);
assert!(
!record_authenticated_machine_binding_from_message(
&bindings,
&invalid,
&announcement,
now,
)
.await
);
assert_eq!(
dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
None
);
}
#[tokio::test]
async fn far_future_direct_origin_cannot_poison_portable_move_ordering() {
let origin = identity::AgentKeypair::generate().expect("origin keygen");
let machine_a = identity::MachineKeypair::generate().expect("machine A keygen");
let machine_b = identity::MachineKeypair::generate().expect("machine B keygen");
let now = 5_000;
let message = verified_identity_origin_message(&origin);
let bindings = std::sync::Arc::new(tokio::sync::RwLock::new(
dm_inbox::AuthenticatedMachineBindingCache::default(),
));
let current = signed_identity_announcement_fixture(origin.agent_id(), &machine_a, now);
assert!(
record_authenticated_machine_binding_from_message(&bindings, &message, ¤t, now,)
.await
);
let far_future = signed_identity_announcement_fixture(origin.agent_id(), &machine_b, u64::MAX);
assert!(
!record_authenticated_machine_binding_from_message(&bindings, &message, &far_future, now,)
.await
);
assert_eq!(
dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
Some(machine_a.machine_id())
);
let normal_move = signed_identity_announcement_fixture(origin.agent_id(), &machine_b, now + 1);
assert!(
record_authenticated_machine_binding_from_message(
&bindings,
&message,
&normal_move,
now + 1,
)
.await
);
assert_eq!(
dm_inbox::authenticated_machine_binding_for_testing(&bindings, &origin.agent_id()).await,
Some(machine_b.machine_id())
);
}
#[tokio::test]
async fn revocation_discovery_eviction_preserves_authenticated_binding() {
let tempdir = tempfile::tempdir().expect("tempdir");
let receiver = Agent::builder()
.with_identity_dir(tempdir.path())
.build()
.await
.expect("receiver agent");
let origin = identity::AgentKeypair::generate().expect("origin keygen");
let machine = identity::MachineKeypair::generate().expect("machine keygen");
let now = 6_000;
let announcement = signed_identity_announcement_fixture(origin.agent_id(), &machine, now);
assert!(
record_authenticated_machine_binding_from_message(
&receiver.authenticated_machine_bindings,
&verified_identity_origin_message(&origin),
&announcement,
now,
)
.await
);
let mut discovered = discovered_agent_fixture(0x66, now, &[], None);
discovered.agent_id = origin.agent_id();
discovered.machine_id = machine.machine_id();
discovered.machine_public_key = machine.public_key().as_bytes().to_vec();
receiver
.insert_discovered_agent_for_testing(discovered)
.await;
assert!(receiver.cached_agent(&origin.agent_id()).await.is_some());
receiver
.evict_revoked_subject(&revocation::RevokedSubject::Machine(machine.machine_id()))
.await;
assert!(receiver.cached_agent(&origin.agent_id()).await.is_none());
assert_eq!(
dm_inbox::authenticated_machine_binding_for_testing(
&receiver.authenticated_machine_bindings,
&origin.agent_id(),
)
.await,
Some(machine.machine_id())
);
}
#[tokio::test]
async fn upsert_discovered_agent_replaces_addresses_on_fresher_announcement() {
let cache = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
let id = identity::AgentId([7; 32]);
upsert_discovered_agent(
&cache,
discovered_agent_fixture(7, 100, &["10.0.0.1:5483", "8.8.8.8:5483"], None),
)
.await;
upsert_discovered_agent(
&cache,
discovered_agent_fixture(7, 200, &["1.2.3.4:5483"], None),
)
.await;
let guard = cache.read().await;
let entry = guard.get(&id).expect("entry present");
assert_eq!(
entry.addresses,
vec!["1.2.3.4:5483".parse().expect("addr")],
"fresher announcement must replace the address set, not union it"
);
}
#[tokio::test]
async fn upsert_discovered_agent_ignores_stale_announcement_addresses() {
let cache = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
let id = identity::AgentId([8; 32]);
upsert_discovered_agent(
&cache,
discovered_agent_fixture(8, 200, &["1.2.3.4:5483"], None),
)
.await;
upsert_discovered_agent(
&cache,
discovered_agent_fixture(8, 100, &["10.0.0.9:5483"], None),
)
.await;
let guard = cache.read().await;
let entry = guard.get(&id).expect("entry present");
assert_eq!(
entry.addresses,
vec!["1.2.3.4:5483".parse().expect("addr")],
"stale announcement must not add addresses"
);
assert_eq!(
entry.announced_at, 200,
"stale announcement must not regress announced_at"
);
}
#[tokio::test]
async fn upsert_discovered_agent_preserves_known_user_id() {
let cache = std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new()));
let id = identity::AgentId([9; 32]);
let user = identity::UserId([9; 32]);
upsert_discovered_agent(
&cache,
discovered_agent_fixture(9, 100, &["1.2.3.4:5483"], Some(user)),
)
.await;
upsert_discovered_agent(
&cache,
discovered_agent_fixture(9, 200, &["1.2.3.4:5483"], None),
)
.await;
let guard = cache.read().await;
let entry = guard.get(&id).expect("entry present");
assert_eq!(
entry.user_id,
Some(user),
"a fresher anonymous announcement must not erase a disclosed user_id"
);
}
#[test]
fn sort_discovered_machine_sorts_fields() {
let mut machine = DiscoveredMachine {
machine_id: identity::MachineId([3u8; 32]),
addresses: vec![
"10.0.0.2:5483".parse::<std::net::SocketAddr>().unwrap(),
"10.0.0.1:5483".parse::<std::net::SocketAddr>().unwrap(),
],
announced_at: 100,
last_seen: 100,
machine_public_key: vec![],
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: vec![
identity::MachineId([2u8; 32]),
identity::MachineId([1u8; 32]),
],
relay_candidates: vec![
identity::MachineId([4u8; 32]),
identity::MachineId([3u8; 32]),
],
agent_ids: vec![identity::AgentId([2u8; 32]), identity::AgentId([1u8; 32])],
user_ids: vec![identity::UserId([2u8; 32]), identity::UserId([1u8; 32])],
};
sort_discovered_machine(&mut machine);
assert_eq!(
machine.addresses[0],
"10.0.0.1:5483".parse::<std::net::SocketAddr>().unwrap()
);
assert_eq!(
machine.addresses[1],
"10.0.0.2:5483".parse::<std::net::SocketAddr>().unwrap()
);
assert_eq!(machine.reachable_via[0], identity::MachineId([1u8; 32]));
assert_eq!(machine.reachable_via[1], identity::MachineId([2u8; 32]));
assert_eq!(machine.relay_candidates[0], identity::MachineId([3u8; 32]));
assert_eq!(machine.relay_candidates[1], identity::MachineId([4u8; 32]));
assert_eq!(machine.agent_ids[0], identity::AgentId([1u8; 32]));
assert_eq!(machine.agent_ids[1], identity::AgentId([2u8; 32]));
assert_eq!(machine.user_ids[0], identity::UserId([1u8; 32]));
assert_eq!(machine.user_ids[1], identity::UserId([2u8; 32]));
}
#[tokio::test]
async fn dm_inbox_capability_upgrade_visible_to_late_subscriber() {
let dir = tempfile::tempdir().expect("tmpdir");
let agent = Agent::builder()
.with_machine_key(dir.path().join("machine.key"))
.with_agent_key_path(dir.path().join("agent.key"))
.with_peer_cache_dir(dir.path().join("peers"))
.with_network_config(network::NetworkConfig::default())
.build()
.await
.expect("agent");
let kem = std::sync::Arc::new(
groups::kem_envelope::AgentKemKeypair::generate().expect("kem keypair"),
);
agent
.start_dm_inbox(kem, dm_inbox::DmInboxConfig::default())
.await
.expect("start dm inbox");
let late_rx = agent.dm_capabilities_tx.subscribe();
let caps = late_rx.borrow().clone();
assert!(
caps.gossip_inbox,
"DM capability upgrade must be visible to subscribers that attach after start_dm_inbox (issue #101)"
);
assert!(
!caps.kem_public_key.is_empty(),
"upgraded capabilities must carry the KEM public key"
);
agent.stop_dm_inbox().await;
}
#[test]
fn deserialize_identity_announcement_rejects_empty() {
let result = deserialize_identity_announcement(&[]);
assert!(result.is_err());
}
#[test]
fn deserialize_machine_announcement_rejects_empty() {
let result = deserialize_machine_announcement(&[]);
assert!(result.is_err());
}
#[test]
fn deserialize_identity_announcement_rejects_garbage() {
let result = deserialize_identity_announcement(b"not-a-valid-bincode");
assert!(result.is_err());
}
#[test]
fn deserialize_machine_announcement_rejects_garbage() {
let result = deserialize_machine_announcement(b"not-a-valid-bincode");
assert!(result.is_err());
}
#[cfg(test)]
mod fence_token_tests {
use crate::FenceToken;
#[test]
fn wire_roundtrip_preserves_epoch_and_revision() {
let token = FenceToken {
epoch: 123,
revision: 456,
};
let wire = token.to_wire();
let back = FenceToken::from_wire(&wire).expect("roundtrip parses");
assert_eq!(back, token);
}
#[test]
fn from_wire_returns_err_for_malformed_and_overflow_tokens() {
assert!(FenceToken::from_wire("").is_err());
assert!(FenceToken::from_wire("no-colon").is_err());
assert!(FenceToken::from_wire(":5").is_err(), "missing epoch");
assert!(FenceToken::from_wire("1:").is_err(), "missing revision");
assert!(FenceToken::from_wire("1:2:3").is_err(), "too many parts");
assert!(FenceToken::from_wire("abc:2").is_err(), "non-numeric epoch");
assert!(
FenceToken::from_wire("1:xyz").is_err(),
"non-numeric revision"
);
assert!(
FenceToken::from_wire("999999999999999999999999999999:1").is_err(),
"epoch overflow must be rejected"
);
assert!(
FenceToken::from_wire("1:999999999999999999999999999999").is_err(),
"revision overflow must be rejected"
);
let ok = FenceToken::from_wire("123:456");
assert!(ok.is_ok(), "well-formed token must parse");
assert_eq!(
ok.unwrap(),
FenceToken {
epoch: 123,
revision: 456
}
);
}
#[test]
fn same_revision_different_epoch_is_stale_the_restart_aba_breaker() {
let pre_restart = FenceToken {
epoch: 100,
revision: 42,
};
let post_restart_same_revision = FenceToken {
epoch: 999,
revision: 42,
};
assert_ne!(
pre_restart, post_restart_same_revision,
"epoch differs ⇒ stale even at an identical revision (ABA safe)"
);
let current = FenceToken {
epoch: 999,
revision: 42,
};
assert_eq!(post_restart_same_revision, current, "exact match accepts");
}
#[test]
fn stale_revision_at_same_epoch_is_stale() {
let stale = FenceToken {
epoch: 7,
revision: 3,
};
let current = FenceToken {
epoch: 7,
revision: 4,
};
assert_ne!(stale, current, "revision moved ⇒ token is stale");
}
}