use crate::inbox::{BusMessage, Inbox, OutgoingReply};
use crate::reply::CorrelationId;
use crate::{BusError, Result, Topic};
use agent_mesh_discovery::{AnnounceConfig, Announcer, AnnouncerHandle};
use agent_mesh_protocol::{AgentKey, CertChain, Fingerprint, Recipient, SignedEnvelope, UserKey};
use agent_mesh_transport::{
do_handshake,
identity::agent_pubkey_to_iroh,
iroh_reexports::{Connection, Incoming, IncomingAddr, PublicKey},
recv_envelope, send_envelope, Endpoint, PeerResolver, ResolverHandle, TransportError,
};
use std::future::Future;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
const RESOLVE_TIMEOUT: Duration = Duration::from_secs(5);
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone)]
pub struct BusOptions {
pub announce: bool,
}
impl Default for BusOptions {
fn default() -> Self {
Self { announce: true }
}
}
pub struct Bus {
agent: Arc<AgentKey>,
user_fp: Fingerprint,
endpoint: Arc<Endpoint>,
resolver: Arc<PeerResolver>,
inbox: Arc<Inbox>,
sequence: Arc<AtomicU64>,
local_port: u16,
_resolver_handle: ResolverHandle,
_announcer: Option<AnnouncerHandle>,
accept_task: JoinHandle<()>,
}
impl Bus {
pub async fn bind(user: &UserKey, agent: AgentKey, port: u16) -> Result<Self> {
Self::bind_with(user, agent, port, BusOptions::default()).await
}
pub async fn bind_with(
user: &UserKey,
agent: AgentKey,
port: u16,
opts: BusOptions,
) -> Result<Self> {
let user_fp = user.fingerprint();
let endpoint = Endpoint::bind(&agent, port).await?;
let local_port = endpoint.port();
let endpoint = Arc::new(endpoint);
let (resolver, resolver_handle) = PeerResolver::start()?;
let resolver = Arc::new(resolver);
let inbox = Arc::new(Inbox::new());
let agent = Arc::new(agent);
let sequence = Arc::new(AtomicU64::new(1));
let announcer = if opts.announce {
Some(
Announcer::start(AnnounceConfig {
agent_fp: agent.fingerprint(),
agent_pubkey: Some(agent.public_bytes()),
user_fp,
capabilities: agent.cert().metadata.capabilities.clone(),
role: agent.cert().metadata.role.clone(),
host: agent.cert().metadata.host.clone(),
port: local_port,
})
.map_err(|e| {
BusError::Transport(TransportError::Iroh(format!("announce start: {e}")))
})?,
)
} else {
None
};
let accept_task = spawn_accept_loop(
endpoint.clone(),
agent.clone(),
inbox.clone(),
resolver.clone(),
sequence.clone(),
);
Ok(Self {
agent,
user_fp,
endpoint,
resolver,
inbox,
sequence,
local_port,
_resolver_handle: resolver_handle,
_announcer: announcer,
accept_task,
})
}
#[must_use]
pub fn user_fingerprint(&self) -> Fingerprint {
self.user_fp
}
#[must_use]
pub fn agent_fingerprint(&self) -> Fingerprint {
self.agent.fingerprint()
}
#[must_use]
pub fn local_port(&self) -> u16 {
self.local_port
}
pub async fn request(
&self,
peer_fp: Fingerprint,
topic: &Topic,
body: Vec<u8>,
timeout: Duration,
) -> Result<Vec<u8>> {
let correlation = CorrelationId::new_random();
let waiter = self.inbox.register_reply(correlation);
let msg = BusMessage::Request {
topic: topic.wire(),
correlation: correlation.0,
body,
};
if let Err(e) = self.send_to(peer_fp, msg).await {
self.inbox.cancel_reply(&correlation);
return Err(e);
}
match tokio::time::timeout(timeout, waiter).await {
Ok(Ok(payload)) => Ok(payload),
Ok(Err(_)) => Err(BusError::LostReply),
Err(_) => {
self.inbox.cancel_reply(&correlation);
Err(BusError::Timeout(timeout))
}
}
}
pub fn handle_requests<F, Fut>(&self, topic: Topic, handler: F)
where
F: Fn(Vec<u8>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Vec<u8>>> + Send + 'static,
{
let inbox = self.inbox.clone();
tokio::spawn(async move {
inbox.register_handler(topic, handler).await;
});
}
pub async fn publish_to(
&self,
peer_fp: Fingerprint,
topic: &Topic,
body: Vec<u8>,
) -> Result<()> {
let msg = BusMessage::Publish {
topic: topic.wire(),
body,
};
self.send_to(peer_fp, msg).await
}
pub async fn subscribe(&self, topic: &Topic) -> broadcast::Receiver<Vec<u8>> {
self.inbox.subscribe(topic).await
}
pub async fn close(self) -> Result<()> {
self.accept_task.abort();
match Arc::try_unwrap(self.endpoint) {
Ok(ep) => ep.close().await,
Err(_) => {
tracing::debug!("bus close: endpoint still shared, leaving Drop to clean up");
}
}
Ok(())
}
async fn send_to(&self, peer_fp: Fingerprint, msg: BusMessage) -> Result<()> {
let conn = dial_peer(&self.endpoint, &self.resolver, peer_fp).await?;
send_one(
&conn,
self.agent.cert(),
self.agent.as_ref(),
peer_fp,
&self.sequence,
msg,
)
.await
}
}
async fn dial_peer(
endpoint: &Endpoint,
resolver: &PeerResolver,
peer_fp: Fingerprint,
) -> Result<Connection> {
let peer = resolver
.resolve(&peer_fp, RESOLVE_TIMEOUT)
.await
.ok_or_else(|| {
BusError::Unreachable(format!(
"peer {} not announced within {:?}",
peer_fp.short(),
RESOLVE_TIMEOUT
))
})?;
let pubkey = peer.agent_pubkey.ok_or_else(|| {
BusError::Unreachable(format!(
"peer {} announced without ed25519 pubkey",
peer_fp.short()
))
})?;
let iroh_pk = agent_pubkey_to_iroh(&pubkey).ok_or_else(|| {
BusError::Unreachable(format!(
"peer {} advertised invalid ed25519 pubkey",
peer_fp.short()
))
})?;
let mut socket_addrs: Vec<SocketAddr> = peer
.addrs
.iter()
.copied()
.map(|ip| SocketAddr::new(ip, peer.port))
.collect();
let lo_v4: std::net::IpAddr = std::net::Ipv4Addr::LOCALHOST.into();
let lo_v6: std::net::IpAddr = std::net::Ipv6Addr::LOCALHOST.into();
let lo_addrs = [
SocketAddr::new(lo_v4, peer.port),
SocketAddr::new(lo_v6, peer.port),
];
for addr in lo_addrs {
if !socket_addrs.contains(&addr) {
socket_addrs.push(addr);
}
}
tracing::debug!(
peer = %peer_fp.short(),
addrs = ?socket_addrs,
"bus: dialing peer"
);
if socket_addrs.is_empty() {
return Err(BusError::Unreachable(format!(
"peer {} announced without socket addresses",
peer_fp.short()
)));
}
let conn = endpoint.dial(iroh_pk, socket_addrs).await?;
Ok(conn)
}
async fn send_one(
conn: &Connection,
our_cert: &CertChain,
sender: &AgentKey,
peer_agent_fp: Fingerprint,
sequence: &AtomicU64,
msg: BusMessage,
) -> Result<()> {
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| BusError::Transport(TransportError::Iroh(format!("open_bi: {e}"))))?;
tokio::time::timeout(
HANDSHAKE_TIMEOUT,
do_handshake(our_cert, &mut send, &mut recv, true),
)
.await
.map_err(|_| BusError::Timeout(HANDSHAKE_TIMEOUT))??;
let seq = sequence.fetch_add(1, Ordering::SeqCst);
let payload = serde_json::to_vec(&msg)?;
let env = SignedEnvelope::new(
sender,
Recipient::Direct {
agent_fp: peer_agent_fp,
},
seq,
payload,
);
send_envelope(&mut send, &env).await?;
send.finish()
.map_err(|e| BusError::Transport(TransportError::Iroh(format!("finish: {e}"))))?;
let _ = send.stopped().await;
Ok(())
}
fn spawn_accept_loop(
endpoint: Arc<Endpoint>,
agent: Arc<AgentKey>,
inbox: Arc<Inbox>,
resolver: Arc<PeerResolver>,
sequence: Arc<AtomicU64>,
) -> JoinHandle<()> {
tokio::spawn(async move {
loop {
let Some(incoming) = endpoint.accept().await else {
tracing::debug!("bus accept loop: endpoint closed");
break;
};
let agent = agent.clone();
let inbox = inbox.clone();
let endpoint = endpoint.clone();
let resolver = resolver.clone();
let sequence = sequence.clone();
tokio::spawn(async move {
if let Err(e) =
handle_incoming(incoming, agent, inbox, endpoint, resolver, sequence).await
{
tracing::warn!(error = %e, "bus: incoming connection error");
}
});
}
})
}
async fn handle_incoming(
incoming: Incoming,
agent: Arc<AgentKey>,
inbox: Arc<Inbox>,
endpoint: Arc<Endpoint>,
resolver: Arc<PeerResolver>,
sequence: Arc<AtomicU64>,
) -> Result<()> {
let reverse_addr = match incoming.remote_addr() {
IncomingAddr::Ip(addr) => Some(addr),
_ => None,
};
let conn = incoming
.await
.map_err(|e| BusError::Transport(TransportError::Iroh(format!("incoming: {e}"))))?;
let reverse: Option<(PublicKey, SocketAddr)> =
reverse_addr.map(|addr| (conn.remote_id(), addr));
loop {
let (mut send, mut recv) = match conn.accept_bi().await {
Ok(streams) => streams,
Err(e) => {
tracing::debug!(error = %e, "bus: accept_bi ended (peer closed)");
return Ok(());
}
};
let cert = agent.cert().clone();
let handshake_res = tokio::time::timeout(
HANDSHAKE_TIMEOUT,
do_handshake(&cert, &mut send, &mut recv, false),
)
.await;
match handshake_res {
Ok(Ok(_peer_cert)) => {}
Ok(Err(e)) => {
tracing::warn!(error = %e, "bus: handshake rejected");
continue;
}
Err(_) => {
tracing::warn!("bus: handshake timed out");
continue;
}
}
let env = match recv_envelope(&mut recv).await {
Ok(env) => env,
Err(e) => {
tracing::warn!(error = %e, "bus: envelope read failed");
continue;
}
};
let outgoing = match inbox.on_envelope(env).await {
Ok(out) => out,
Err(e) => {
tracing::warn!(error = %e, "bus: inbox rejected envelope");
continue;
}
};
if let Some(reply) = outgoing {
let endpoint = endpoint.clone();
let resolver = resolver.clone();
let agent = agent.clone();
let sequence = sequence.clone();
tokio::spawn(async move {
if let Err(e) =
ship_reply(endpoint, resolver, agent, sequence, reply, reverse).await
{
tracing::warn!(error = %e, "bus: reply ship failed");
}
});
}
}
}
async fn ship_reply(
endpoint: Arc<Endpoint>,
resolver: Arc<PeerResolver>,
agent: Arc<AgentKey>,
sequence: Arc<AtomicU64>,
reply: OutgoingReply,
reverse: Option<(PublicKey, SocketAddr)>,
) -> Result<()> {
let conn = dial_reply_peer(&endpoint, &resolver, reply.peer_fp, reverse).await?;
let msg = BusMessage::Reply {
correlation: reply.correlation.0,
body: reply.body,
};
send_one(
&conn,
agent.cert(),
agent.as_ref(),
reply.peer_fp,
&sequence,
msg,
)
.await
}
async fn dial_reply_peer(
endpoint: &Endpoint,
resolver: &PeerResolver,
peer_fp: Fingerprint,
reverse: Option<(PublicKey, SocketAddr)>,
) -> Result<Connection> {
if let Some((pubkey, addr)) = reverse {
if Fingerprint::of_bytes(pubkey.as_bytes()) == peer_fp {
match endpoint.dial(pubkey, [addr]).await {
Ok(conn) => return Ok(conn),
Err(e) => tracing::debug!(
peer = %peer_fp.short(),
%addr,
error = %e,
"bus: reply dial-back failed, falling back to mDNS"
),
}
} else {
tracing::warn!(
peer = %peer_fp.short(),
"bus: connection key does not match envelope sender; ignoring dial-back route"
);
}
}
dial_peer(endpoint, resolver, peer_fp).await
}
#[cfg(test)]
mod tests {
use super::*;
use agent_mesh_protocol::{AgentMetadata, Caveats, UserKey};
fn agent(user: &UserKey, role: &str) -> AgentKey {
AgentKey::issue(
user,
AgentMetadata {
role: role.into(),
host: "test".into(),
capabilities: vec!["test".into()],
issued_at: "2026-05-28T00:00:00Z".into(),
expires_at: None,
caveats: Caveats::top(),
},
)
}
#[tokio::test(flavor = "multi_thread")]
async fn bind_exposes_local_port_and_fingerprints() {
let user = UserKey::generate();
let a = agent(&user, "worker");
let a_fp = a.fingerprint();
let bus = Bus::bind(&user, a, 0).await.expect("bind");
assert!(bus.local_port() > 0);
assert_eq!(bus.user_fingerprint(), user.fingerprint());
assert_eq!(bus.agent_fingerprint(), a_fp);
bus.close().await.expect("close");
}
#[tokio::test(flavor = "multi_thread")]
async fn request_to_unknown_peer_errors_unreachable() {
let user = UserKey::generate();
let a = agent(&user, "alice");
let bus = Bus::bind(&user, a, 0).await.expect("bind");
let topic = Topic::new(user.fingerprint(), "echo");
let phantom_fp = Fingerprint([0xfeu8; 32]);
let res = bus
.request(
phantom_fp,
&topic,
b"x".to_vec(),
Duration::from_millis(200),
)
.await;
match res {
Err(BusError::Unreachable(_)) => {}
other => panic!("expected Unreachable, got {other:?}"),
}
bus.close().await.unwrap();
}
}