use std::collections::{BTreeSet, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use mcpmesh_local_api::{
BlobFetchCancelResult, BlobFetchResult, BlobPublishResult, BlobScopeList, InviteResult,
PairResult, PeerAddParams, PeerRemoveParams, PeerRenameParams, RegisterServiceParams,
ScopeInfo, SetRelaysResult,
};
use mcpmesh_net::errors::{ERR_UNREACHABLE, synthesized};
use mcpmesh_net::framing::{FrameReader, write_frame};
use serde_json::Value;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::allowlist::{PeerEntry, PeerStore};
use crate::audit::{AuditRecord, now_ts};
use crate::config::Config;
use crate::control::DaemonState;
use crate::pairing::Invite;
use crate::util::{blocking, epoch_now_u64};
use super::accept::swap_services;
use super::config_write::{
append_allow_to_config, remove_allow_from_config, remove_principal_from_service,
remove_service_from_config, write_relays, write_service_to_config,
};
use super::{MeshState, dial_service, pipe_session};
const INVITE_TTL: Duration = Duration::from_secs(24 * 60 * 60);
pub const RELAY_READY_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) async fn blob_publish(
state: &DaemonState,
scope: String,
path: String,
) -> Result<BlobPublishResult> {
let mesh = state.mesh_required()?;
let provider = mesh.app_blobs().await.context(
"app-blob provider not enabled (its store failed to build — check the daemon log)",
)?;
let (ticket, hash) = provider
.publish_scope(&scope, Path::new(&path))
.await
.context("publish blob into scope")?;
Ok(BlobPublishResult { ticket, hash })
}
pub(crate) async fn blob_grant(
state: &DaemonState,
scope: String,
principal: String,
) -> Result<()> {
let mesh = state.mesh_required()?;
let provider = mesh.app_blobs().await.context(
"app-blob provider not enabled (its store failed to build — check the daemon log)",
)?;
provider.grant(&scope, &principal)
}
pub(crate) async fn blob_revoke(
state: &DaemonState,
scope: String,
principals: Vec<String>,
) -> Result<()> {
let mesh = state.mesh_required()?;
let provider = mesh.app_blobs().await.context(
"app-blob provider not enabled (its store failed to build — check the daemon log)",
)?;
if !provider.has_scope(&scope) {
anyhow::bail!(NoSuchBlobScope(scope));
}
let changed = provider.revoke_from_scope(&scope, &principals)?;
tracing::info!(%scope, count = principals.len(), changed, "blob grants revoked");
Ok(())
}
pub(crate) async fn blob_unpublish(state: &DaemonState, scope: String, hash: String) -> Result<()> {
let mesh = state.mesh_required()?;
let provider = mesh.app_blobs().await.context(
"app-blob provider not enabled (its store failed to build — check the daemon log)",
)?;
let parsed = crate::blobs::parse_blob_hash(&hash)?;
let hash_hex = parsed.to_hex().to_string();
if !provider.has_scope(&scope) {
anyhow::bail!(NoSuchBlobScope(scope));
}
let changed = provider.unpublish(&scope, &hash_hex).await?;
tracing::info!(%scope, changed, "blob unpublished from scope");
Ok(())
}
#[derive(Debug)]
pub struct NoSuchBlobScope(pub String);
impl std::fmt::Display for NoSuchBlobScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"no blob scope named '{}' — 'blob_list' shows the scopes this daemon has",
self.0
)
}
}
impl std::error::Error for NoSuchBlobScope {}
#[derive(Debug)]
pub struct NoSuchBlob(pub String);
impl std::fmt::Display for NoSuchBlob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"blob '{}' is not held complete by this daemon — fetch it before republishing",
self.0
)
}
}
impl std::error::Error for NoSuchBlob {}
#[derive(Debug)]
pub struct BlobWithdrawn {
pub scope: String,
pub hash: String,
}
impl std::fmt::Display for BlobWithdrawn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"blob '{}' was withdrawn from scope '{}' — republishing will not restore it; \
'blob_publish' from the file if the re-share is intended",
self.hash, self.scope
)
}
}
impl std::error::Error for BlobWithdrawn {}
pub(crate) async fn blob_republish(
state: &DaemonState,
scope: String,
hash: String,
) -> Result<mcpmesh_local_api::BlobPublishResult> {
let mesh = state.mesh_required()?;
let provider = mesh.app_blobs().await.ok_or_else(|| {
anyhow::anyhow!(
"app-blob provider not enabled (its store failed to build — check the daemon log)"
)
})?;
let (ticket, hash) = provider.republish(&scope, &hash).await?;
tracing::info!(%scope, %hash, "blob republished");
Ok(mcpmesh_local_api::BlobPublishResult { ticket, hash })
}
pub(crate) async fn blob_list(
state: &DaemonState,
params: mcpmesh_local_api::BlobListParams,
) -> Result<BlobScopeList> {
let mesh = state.mesh_required()?;
let q = crate::blobs::scope::ListQuery {
scope: params.scope,
hash: params.hash,
limit: params.limit,
offset: params.offset,
counts_only: params.counts_only,
};
let Some(provider) = mesh.app_blobs().await else {
return Ok(BlobScopeList {
scopes: Vec::new(),
total: 0,
truncated: false,
});
};
let page = provider.list_page(&q)?;
Ok(BlobScopeList {
scopes: page
.rows
.into_iter()
.map(
|(name, hashes, grants, withdrawn, hash_count, grant_count, withdrawn_count)| {
ScopeInfo {
name,
hashes,
grants,
withdrawn,
hash_count,
grant_count,
withdrawn_count,
}
},
)
.collect(),
total: page.total,
truncated: page.truncated,
})
}
#[derive(Debug)]
pub struct Cancelled(pub String);
impl std::fmt::Display for Cancelled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"cancelled by blob_fetch_cancel: {} — partial chunks stay in the store, unlisted and \
unreclaimable (#80), exactly as they do when a fetch fails",
self.0
)
}
}
impl std::error::Error for Cancelled {}
struct FetchGuard {
mesh: Arc<MeshState>,
hash: String,
token: crate::cancel::CancelToken,
provider: Arc<crate::blobs::provider::AppBlobs>,
finished: bool,
}
impl FetchGuard {
fn register(
mesh: &Arc<MeshState>,
hash: String,
provider: Arc<crate::blobs::provider::AppBlobs>,
) -> Self {
let token = {
let mut map = mesh.fetches.lock().expect("fetches lock not poisoned");
let slot = map
.entry(hash.clone())
.or_insert_with(|| crate::daemon::FetchSlot {
token: crate::cancel::CancelToken::new(),
waiters: 0,
});
if slot.token.is_cancelled() {
slot.token = crate::cancel::CancelToken::new();
}
slot.waiters += 1;
slot.token.clone()
};
Self {
mesh: mesh.clone(),
hash,
token,
provider,
finished: false,
}
}
}
impl Drop for FetchGuard {
fn drop(&mut self) {
{
let mut map = self.mesh.fetches.lock().expect("fetches lock not poisoned");
if let Some(slot) = map.get_mut(&self.hash) {
slot.waiters = slot.waiters.saturating_sub(1);
if slot.waiters == 0 {
map.remove(&self.hash);
}
}
}
if !self.finished {
self.provider.emit_fetch_aborted(&self.hash);
}
}
}
pub(crate) async fn blob_fetch(
state: &DaemonState,
ticket: String,
dest_path: String,
) -> Result<BlobFetchResult> {
let mesh = state.mesh_required()?;
let provider = mesh.app_blobs().await.context(
"app-blob provider not enabled (its store failed to build — check the daemon log)",
)?;
let hash_hex = crate::blobs::provider::AppBlobs::ticket_hash(&ticket)?
.to_hex()
.to_string();
let mut guard = FetchGuard::register(mesh, hash_hex.clone(), provider.clone());
let dest = PathBuf::from(dest_path);
let work = async {
let hash = provider.fetch(&ticket).await.context("fetch blob")?;
let bytes_len = provider.export_to(hash, &dest).await?;
anyhow::Ok(BlobFetchResult {
hash: hash.to_hex().to_string(),
bytes_len,
})
};
let outcome = tokio::select! {
r = work => r,
() = guard.token.cancelled() => Err(Cancelled(hash_hex).into()),
};
guard.finished = true;
outcome
}
pub(crate) fn blob_fetch_cancel(state: &DaemonState, hash: &str) -> Result<BlobFetchCancelResult> {
let mesh = state.mesh_required()?;
let key = crate::blobs::parse_blob_hash(hash)?.to_hex().to_string();
let slot = mesh
.fetches
.lock()
.expect("fetches lock not poisoned")
.get(&key)
.cloned();
match slot {
Some(slot) => {
slot.token.cancel();
Ok(BlobFetchCancelResult { cancelled: true })
}
None => Ok(BlobFetchCancelResult { cancelled: false }),
}
}
async fn reload_services_from_disk(mesh: &Arc<MeshState>, why: &str) -> Result<()> {
let cfg = Config::load(&mesh.config_path)
.map_err(|e| anyhow::anyhow!("reload config after {why}: {e}"))?;
let ephemeral = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned")
.clone();
swap_services(
mesh,
crate::daemon::build_services_with_ephemeral(
&cfg,
&mesh.audit(),
&mesh.limits(),
&ephemeral,
),
);
Ok(())
}
pub(crate) async fn register_service(
state: &DaemonState,
params: RegisterServiceParams,
) -> Result<()> {
let mesh = state.mesh_required()?;
let _reload = mesh.reload_lock.lock().await;
let RegisterServiceParams {
name,
backend,
allow,
ephemeral,
rate_limit_per_min,
} = params;
if rate_limit_per_min == Some(0) {
anyhow::bail!(crate::control::InvalidParams(
"rate_limit_per_min must be at least 1 (omit it to use [limits].rate_limit_per_min)"
.into()
));
}
if ephemeral {
let cfg = Config::load(&mesh.config_path)
.map_err(|e| anyhow::anyhow!("config error in {}: {e}", mesh.config_path.display()))?;
if cfg.services.contains_key(&name) {
anyhow::bail!(
"service '{name}' is already registered persistently in config; \
use a different name for an ephemeral registration"
);
}
mesh.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned")
.insert(
name.clone(),
crate::daemon::EphemeralService {
backend,
allow: allow.clone(),
rate_limit_per_min,
},
);
reload_services_from_disk(mesh, "register-ephemeral").await?;
tracing::info!(service = %name, "registered ephemeral service");
return Ok(());
}
{
let map = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned");
if map.contains_key(&name) {
anyhow::bail!(
"service '{name}' is currently registered ephemerally; \
unregister it first, or use a different name for the persistent registration"
);
}
}
let config_path = mesh.config_path.clone();
let (name_w, backend_w, allow_w, rate_w) = (
name.clone(),
backend.clone(),
allow.clone(),
rate_limit_per_min,
);
blocking("join config write", move || {
write_service_to_config(&config_path, &name_w, &backend_w, &allow_w, rate_w)
})
.await??;
reload_services_from_disk(mesh, "register").await?;
tracing::info!(service = %name, "registered/updated service");
Ok(())
}
#[doc(hidden)]
pub async fn unregister_ephemeral(mesh: &Arc<MeshState>, names: &[String]) {
if names.is_empty() {
return;
}
let _reload = mesh.reload_lock.lock().await;
{
let mut map = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned");
for name in names {
map.remove(name);
}
}
if let Err(e) = reload_services_from_disk(mesh, "unregister-ephemeral").await {
tracing::warn!(%e, "reload after ephemeral unregister failed");
}
}
pub(crate) async fn add_peer(state: &DaemonState, params: PeerAddParams) -> Result<()> {
let mesh = state.mesh_required()?;
let PeerAddParams {
nickname,
endpoint_id,
allow,
} = params;
let endpoint_id = endpoint_id
.parse::<iroh::EndpointId>()
.map_err(|e| anyhow::anyhow!("peer_add: endpoint_id is not a valid EndpointId: {e}"))?;
let entry = PeerEntry {
endpoint_id: *endpoint_id.as_bytes(),
nickname: nickname.clone(),
services: allow,
paired_at: None,
user_id: None,
last_addr: None,
};
let store = mesh.store.clone();
blocking("join peer add", move || store.add(entry)).await??;
tracing::info!(peer = %nickname, "added peer to allowlist");
Ok(())
}
fn adopt_hook(mesh: &Arc<MeshState>) -> crate::pairing::rendezvous::AdoptBindingFn {
let mesh = mesh.clone();
Box::new(move |binding: crate::pairing::rendezvous::SelfBinding| {
let mesh = mesh.clone();
Box::pin(async move {
let path = mesh.adopted_binding_path();
let json = serde_json::to_vec(&binding)?;
blocking("join adopt binding write", move || {
crate::pairing::persist::write_private(&path, &json)
})
.await??;
mesh.set_self_binding_live(Some(binding.clone()));
mesh.audit().record(crate::audit::AuditRecord::trust(
now_ts(),
"self_enroll_adopt".into(),
Some(binding.user_pk.clone()),
None,
));
anyhow::Ok(())
})
})
}
pub async fn endorse_peer(
state: &DaemonState,
params: mcpmesh_local_api::PeerEndorseParams,
) -> Result<mcpmesh_local_api::PeerEndorseResult> {
let mesh = state.mesh_required()?;
let subject_id = params
.subject
.strip_prefix("eid:")
.unwrap_or(¶ms.subject)
.parse::<iroh::EndpointId>()
.map_err(|e| {
crate::control::InvalidParams(format!(
"peer_endorse: subject is not a valid endpoint id: {e}"
))
})?;
anyhow::ensure!(
mesh.adopted_binding
.read()
.expect("adopted_binding lock not poisoned")
.is_none(),
crate::control::InvalidParams(
"peer_endorse: this device was enrolled into another device's identity (#86) and does \
not hold that user key. Endorse from the device that does."
.into()
)
);
let path = mesh
.user_key_path
.get()
.cloned()
.ok_or_else(|| anyhow::anyhow!("peer_endorse: this daemon has no user key path"))?;
let subject_bytes = *subject_id.as_bytes();
let subject_uid = params.subject_user_id.clone();
let (endorsed_by, evidence) = blocking("join endorse", move || {
let (user_key, _created) = mcpmesh_trust::UserKey::load_or_generate(&path)?;
let evidence =
mcpmesh_trust::binding::endorse(&user_key, &subject_bytes, subject_uid.as_deref())?;
anyhow::Ok((mcpmesh_trust::binding::user_id(&user_key), evidence))
})
.await??;
Ok(mcpmesh_local_api::PeerEndorseResult {
endorsed_by,
evidence,
})
}
pub async fn introduce_peer(
state: &DaemonState,
params: mcpmesh_local_api::PeerIntroduceParams,
) -> Result<()> {
let mesh = state.mesh_required()?;
let mcpmesh_local_api::PeerIntroduceParams {
subject,
endorsed_by,
evidence,
subject_user_id,
subject_binding,
nickname,
} = params;
let nickname = validated_alias("nickname", Some(nickname))?
.expect("Some in, Some out — the None arm is unreachable here");
let subject_id = subject
.strip_prefix("eid:")
.unwrap_or(&subject)
.parse::<iroh::EndpointId>()
.map_err(|e| {
crate::control::InvalidParams(format!(
"peer_introduce: subject is not a valid endpoint id: {e}"
))
})?;
let subject_bytes = *subject_id.as_bytes();
anyhow::ensure!(
subject_bytes != *mesh.endpoint.id().as_bytes(),
crate::control::InvalidParams("peer_introduce: that is this node's own endpoint id".into())
);
let store = mesh.store.clone();
let endorser = endorsed_by.clone();
let known = blocking("join endorser lookup", move || {
anyhow::Ok(
store
.list()?
.into_iter()
.any(|e| e.paired_at.is_some() && e.user_id.as_deref() == Some(endorser.as_str())),
)
})
.await??;
anyhow::ensure!(
known,
crate::control::InvalidParams(
"peer_introduce: endorsed_by is not the user_id of a peer you are currently paired \
with — an introduction's trust chain must end at someone you paired with yourself"
.into()
)
);
mcpmesh_trust::binding::verify_endorsement(
&endorsed_by,
&evidence,
&subject_bytes,
subject_user_id.as_deref(),
)
.map_err(|e| {
crate::control::InvalidParams(format!(
"peer_introduce: the endorsement does not verify: {e}"
))
})?;
let verified_user_id = match (&subject_user_id, &subject_binding) {
(None, None) => None,
(Some(_), None) => anyhow::bail!(crate::control::InvalidParams(
"peer_introduce: subject_user_id requires subject_binding — the SUBJECT must prove it \
controls that user key, or an endorsement could name someone else's user_id and \
inherit their grants"
.into()
)),
(None, Some(_)) => anyhow::bail!(crate::control::InvalidParams(
"peer_introduce: subject_binding without subject_user_id has nothing to bind".into()
)),
(Some(uid), Some(sig)) => {
let proven = mcpmesh_trust::binding::verify_presented(uid, sig, &subject_bytes)
.map_err(|e| {
crate::control::InvalidParams(format!(
"peer_introduce: the subject's device binding does not verify: {e}"
))
})?;
Some(proven)
}
};
let store = mesh.store.clone();
let (nick, subj) = (nickname.clone(), subject_bytes);
let collides = blocking("join introduce collision check", move || {
anyhow::Ok(
store
.list()?
.into_iter()
.any(|e| e.nickname == nick && e.endpoint_id != subj),
)
})
.await??;
anyhow::ensure!(
!collides,
crate::control::InvalidParams(format!(
"peer_introduce: you already use the name '{nickname}' for a different peer"
))
);
let store = mesh.store.clone();
let already_paired = blocking("join introduce paired check", move || {
anyhow::Ok(
store
.resolve(&subject_bytes)?
.is_some_and(|e| e.paired_at.is_some()),
)
})
.await??;
anyhow::ensure!(
!already_paired,
crate::control::InvalidParams(
"peer_introduce: you are already paired with that peer — an introduction would REPLACE \
a row proven by a SAS ceremony with a weaker one"
.into()
)
);
let entry = PeerEntry {
endpoint_id: subject_bytes,
nickname: nickname.clone(),
services: vec![],
paired_at: None,
user_id: verified_user_id,
last_addr: None,
};
let store = mesh.store.clone();
blocking("join peer introduce", move || store.add(entry)).await??;
mesh.audit().record(crate::audit::AuditRecord::trust(
now_ts(),
"peer_introduce".into(),
Some(nickname.clone()),
Some(endorsed_by.clone()),
));
tracing::info!(peer = %nickname, "installed peer from an endorsement (#65)");
Ok(())
}
pub async fn remove_peer(state: &DaemonState, params: PeerRemoveParams) -> Result<()> {
let mesh = state.mesh_required()?;
let nickname = params.nickname;
let revoked = revoke_service_access(mesh, &nickname).await?;
if let Some(provider) = mesh.app_blobs().await {
let store = mesh.store.clone();
let nick_r = nickname.clone();
let principals: Vec<String> = blocking("join blob-revoke principals", move || {
let (targets, others): (Vec<_>, Vec<_>) = store
.list()?
.into_iter()
.partition(|e| e.nickname == nick_r);
let mut principals = Vec::new();
for t in &targets {
principals.push(mcpmesh_net::EndpointId::from_bytes(t.endpoint_id).principal());
if let Some(uid) = &t.user_id
&& !others.iter().any(|o| o.user_id.as_deref() == Some(uid))
&& !principals.contains(uid)
{
principals.push(uid.clone());
}
}
anyhow::Ok(principals)
})
.await??;
if !principals.is_empty()
&& let Err(e) = provider.revoke_principals(&principals)
{
tracing::warn!(%e, "blob-scope revoke on unpair failed");
}
}
let store = mesh.store.clone();
let nickname_w = nickname.clone();
let removed = blocking("join peer remove", move || store.remove(&nickname_w)).await??;
if !revoked && !removed {
anyhow::bail!("no paired peer named '{nickname}' — 'mcpmesh status' lists your peers");
}
tracing::info!(peer = %nickname, "unpaired peer");
mesh.audit().record(AuditRecord::trust(
now_ts(),
"unpair".into(),
Some(nickname.clone()),
None,
));
Ok(())
}
struct RenamePlan {
targets: Vec<PeerEntry>,
}
fn rename_plan(
store: &PeerStore,
user_id: Option<&str>,
nickname: Option<&str>,
to: &str,
) -> Result<Option<RenamePlan>> {
let all = store.list()?;
let targets: Vec<PeerEntry> = all
.iter()
.filter(|e| match user_id {
Some(u) => e.user_id.as_deref() == Some(u),
None => Some(e.nickname.as_str()) == nickname,
})
.cloned()
.collect();
if targets.is_empty() {
anyhow::bail!("peer_rename: no matching contact");
}
if targets.iter().all(|e| e.nickname == to) {
return Ok(None); }
let target_ids: std::collections::BTreeSet<[u8; 32]> =
targets.iter().map(|e| e.endpoint_id).collect();
if all
.iter()
.any(|e| e.nickname == to && !target_ids.contains(&e.endpoint_id))
{
anyhow::bail!("the nickname \"{to}\" is already used by another contact");
}
Ok(Some(RenamePlan { targets }))
}
pub async fn rename_peer(state: &DaemonState, params: PeerRenameParams) -> Result<()> {
let mesh = state.mesh_required()?;
let to = params.to.trim().to_string();
if to.is_empty() {
anyhow::bail!("peer_rename: the new nickname is empty");
}
let PeerRenameParams {
user_id, nickname, ..
} = params;
if user_id.is_none() && nickname.is_none() {
anyhow::bail!("peer_rename: no contact identified");
}
let _reload = mesh.reload_lock.lock().await;
let store = mesh.store.clone();
let (uid_c, pn_c, to_c) = (user_id.clone(), nickname.clone(), to.clone());
let plan = blocking("join rename plan", move || {
rename_plan(&store, uid_c.as_deref(), pn_c.as_deref(), &to_c)
})
.await??;
let RenamePlan { targets } = match plan {
Some(p) => p,
None => return Ok(()), };
let store = mesh.store.clone();
let to_c = to.clone();
blocking("join rename mutate", move || {
for mut e in targets {
e.nickname = to_c.clone();
store.add(e)?;
}
anyhow::Ok(())
})
.await??;
tracing::info!(to = %to, "renamed contact");
Ok(())
}
fn unregistered_service_error(requested: &[String], served: &[String]) -> Option<String> {
let unknown: Vec<&String> = requested.iter().filter(|r| !served.contains(r)).collect();
let quoted: Vec<String> = unknown.iter().map(|n| format!("'{n}'")).collect();
let named = match quoted.as_slice() {
[] => return None,
[one] => format!("no service named {one}"),
many => format!("no services named {}", many.join(", ")),
};
Some(if served.is_empty() {
format!(
"{named} — nothing is served yet; register one with \
'mcpmesh serve <name> -- <command>'"
)
} else {
format!(
"{named} — you serve: {} (see 'mcpmesh status')",
served.join(", ")
)
})
}
fn validated_alias(field: &str, alias: Option<String>) -> Result<Option<String>> {
let Some(raw) = alias else { return Ok(None) };
let name = raw.trim().to_string();
if name.is_empty() {
anyhow::bail!(crate::control::InvalidParams(format!(
"{field} must not be empty (omit it to use the name the peer suggests)"
)));
}
if name.contains('/') {
anyhow::bail!(crate::control::InvalidParams(format!(
"{field} must not contain '/': the nickname is the <peer>/<service> mount prefix, so \
one would make every mount of that peer unparseable"
)));
}
if name.chars().any(char::is_control) {
anyhow::bail!(crate::control::InvalidParams(format!(
"{field} must not contain control characters"
)));
}
if name.chars().count() > MAX_ALIAS_CHARS {
anyhow::bail!(crate::control::InvalidParams(format!(
"{field} is {} characters; the limit is {MAX_ALIAS_CHARS}",
name.chars().count()
)));
}
Ok(Some(name))
}
const MAX_ALIAS_CHARS: usize = 64;
pub(crate) async fn mint_invite(
services: Vec<String>,
app_label: Option<String>,
max_uses: Option<u32>,
peer_nickname: Option<String>,
as_self: bool,
mesh: &MeshState,
) -> Result<InviteResult> {
use rand::RngCore;
let uses_remaining = match max_uses {
None => 1,
Some(0) => anyhow::bail!(crate::control::InvalidParams(
"max_uses must be at least 1 (omit it for a single-use invite)".into()
)),
Some(n) => n.min(mcpmesh_local_api::MAX_INVITE_USES),
};
if as_self && max_uses.is_some_and(|n| n > 1) {
anyhow::bail!(crate::control::InvalidParams(format!(
"as_self cannot be combined with max_uses = {}: a multi-use SELF-ENROLLMENT invite is \
a standing offer to become this person. Mint one per device.",
max_uses.unwrap_or(1)
)));
}
if as_self && !services.is_empty() {
anyhow::bail!(crate::control::InvalidParams(
"as_self grants nothing: your own devices are not peers of each other. Omit services."
.into()
));
}
let peer_nickname = validated_alias("peer_nickname", peer_nickname)?;
if peer_nickname.is_some()
&& let Some(requested) = max_uses.filter(|n| *n > 1)
{
anyhow::bail!(crate::control::InvalidParams(format!(
"peer_nickname cannot be combined with max_uses = {requested}: one local name applied \
to every redeemer would collide on the second redemption. Mint separate single-use \
invites, or omit peer_nickname and rename afterwards with peer_rename"
)));
}
if let Some(alias) = &peer_nickname {
let store = mesh.store.clone();
let alias_c = alias.clone();
let taken = blocking("join alias collision check", move || {
anyhow::Ok(store.list()?.into_iter().any(|e| e.nickname == alias_c))
})
.await??;
if taken {
anyhow::bail!(crate::control::InvalidParams(format!(
"peer_nickname '{alias}' is already the name of a peer you have paired with — \
pick another, or rename that peer first with peer_rename"
)));
}
}
if let Some(label) = &app_label
&& label.len() > crate::pairing::MAX_APP_LABEL_LEN
{
anyhow::bail!(
"app_label is {} bytes; the maximum is {}",
label.len(),
crate::pairing::MAX_APP_LABEL_LEN
);
}
if services.is_empty() && !as_self {
anyhow::bail!(
"invite must name at least one registered service (an invite granting nothing is useless)"
);
}
let cfg = Config::load(&mesh.config_path)
.map_err(|e| anyhow::anyhow!("config error in {}: {e}", mesh.config_path.display()))?;
let ephemeral = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned")
.clone();
let served: Vec<String> = crate::daemon::known_service_names(&cfg, &ephemeral);
if let Some(msg) = unregistered_service_error(&services, &served) {
anyhow::bail!(msg);
}
let mut secret = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut secret);
let inviter_id = *mesh.endpoint.id().as_bytes();
let _ = tokio::time::timeout(RELAY_READY_TIMEOUT, mesh.endpoint.online()).await;
let inviter_addr_json = serde_json::to_string(&mesh.endpoint.addr())
.context("serialize our own endpoint address for the invite")?;
let now = epoch_now_u64();
let expires_at_epoch = now + INVITE_TTL.as_secs();
let invite = Invite {
as_self,
peer_nickname,
secret,
inviter_id,
inviter_addr_json,
nickname: mesh.self_nickname(),
services: services.clone(),
expires_at_epoch,
app_label,
uses_remaining,
};
let invite_line = invite.encode();
mesh.invites.remove_expired(now).await;
mesh.invites.mint(invite).await.context(
"persist the outstanding invite (its advertised TTL depends on surviving a restart)",
)?;
tracing::info!(?services, uses_remaining, "invite minted");
Ok(InviteResult {
invite_line,
expires_at_epoch,
uses_remaining,
})
}
pub(crate) async fn redeem(
state: &DaemonState,
invite_line: String,
as_nickname: Option<String>,
) -> Result<PairResult> {
let mesh = state.mesh_required()?;
let as_nickname = validated_alias("as_nickname", as_nickname)?;
let grant_mesh = mesh.clone();
let grant_back: crate::pairing::rendezvous::GrantBackFn =
Box::new(move |principal, display| {
let mesh = grant_mesh.clone();
Box::pin(async move {
let served: Vec<String> = match Config::load(&mesh.config_path) {
Ok(cfg) => cfg.services.keys().cloned().collect(),
Err(e) => {
tracing::warn!(%e, "mutual grant-back skipped: config unreadable");
return Ok(());
}
};
if served.is_empty() {
return Ok(()); }
if let Err(e) = grant_service_access(&mesh, &principal, &display, &served).await {
tracing::warn!(%e, "mutual grant-back failed (pairing still succeeded)");
}
Ok(())
})
});
crate::pairing::rendezvous::redeem_invite(
mesh.endpoint.clone(),
mesh.self_nickname(),
invite_line,
as_nickname,
Some(adopt_hook(mesh)),
mesh.store.clone(),
mesh.self_binding(),
Some(grant_back),
)
.await
}
pub(crate) async fn peer_services(
state: &DaemonState,
peer: String,
) -> Result<mcpmesh_local_api::PeerServicesResult> {
let mesh = state.mesh_required()?;
let endpoint_id = resolve_peer_endpoint(mesh, &peer).await?;
let entry = crate::daemon::reach::probe_peer_cached(mesh, endpoint_id).await;
anyhow::ensure!(
entry.reachable,
"peer '{peer}' is unreachable — cannot fetch its shared services"
);
Ok(mcpmesh_local_api::PeerServicesResult {
services: entry.services,
})
}
pub(crate) async fn peer_diagnostics(
state: &DaemonState,
peer: &str,
) -> Result<mcpmesh_local_api::PeerDiagnosticsResult> {
let mesh = state.mesh_required()?;
let endpoint_id = resolve_peer_endpoint(mesh, peer).await?;
let store = mesh.store.clone();
let entry = blocking("join peer-diagnostics store read", move || {
store.resolve(&endpoint_id)
})
.await??
.with_context(|| format!("peer '{peer}' is not in the allowlist"))?;
let id = iroh::EndpointId::from_bytes(&endpoint_id)
.map_err(|e| anyhow::anyhow!("stored endpoint id for '{peer}' is invalid: {e}"))?;
let dialed = crate::daemon::dial::stored_dial_addr(entry.last_addr.as_deref(), id);
let hint_addrs: Vec<String> = dialed
.addrs
.iter()
.map(|a| match a {
iroh::TransportAddr::Ip(s) => s.to_string(),
iroh::TransportAddr::Relay(u) => {
format!("relay {}", crate::daemon::sanitize_relay_url(u))
}
other => format!("{other:?}"),
})
.collect();
let hint_usable = entry.last_addr.is_some() && !dialed.addrs.is_empty();
let reachability = {
let cache = mesh
.reachability
.lock()
.expect("reachability lock not poisoned");
cache.get(&endpoint_id).map(|e| {
let age = (crate::util::epoch_now_i64() - e.probed_at).max(0);
crate::daemon::reach::reachability_row(
entry.nickname.clone(),
endpoint_id,
Some(e),
Some(age as u64),
)
})
};
Ok(mcpmesh_local_api::PeerDiagnosticsResult {
nickname: entry.nickname,
principal: mcpmesh_net::EndpointId::from_bytes(entry.endpoint_id).principal(),
user_id: entry.user_id,
paired_at: entry.paired_at,
last_addr: entry.last_addr,
hint_addrs,
hint_usable,
reachability,
})
}
async fn resolve_peer_endpoint(mesh: &Arc<MeshState>, peer: &str) -> Result<[u8; 32]> {
if let Some(hex) = peer.strip_prefix("eid:") {
let bytes = data_encoding::HEXLOWER
.decode(hex.as_bytes())
.map_err(|_| anyhow::anyhow!("invalid eid principal: not lowercase hex"))?;
return bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("invalid eid principal: expected 32 bytes"));
}
let store = mesh.store.clone();
let peer_owned = peer.to_string();
let eid = tokio::task::spawn_blocking(move || -> Result<Option<[u8; 32]>> {
if let Some(e) = store.entry_for(&peer_owned)? {
return Ok(Some(e.endpoint_id));
}
Ok(store
.entries_for_user(&peer_owned)?
.first()
.map(|e| e.endpoint_id))
})
.await
.context("join peer resolve for peer_services")??;
eid.with_context(|| format!("no paired peer '{peer}' — 'mcpmesh status' lists your peers"))
}
pub(crate) async fn unregister_service(state: &DaemonState, name: String) -> Result<()> {
let mesh = state.mesh_required()?;
let _reload = mesh.reload_lock.lock().await;
let dropped_ephemeral = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned")
.remove(&name)
.is_some();
let config_path = mesh.config_path.clone();
let name_w = name.clone();
let removed_config = blocking("join unregister config write", move || {
remove_service_from_config(&config_path, &name_w)
})
.await??;
if dropped_ephemeral || removed_config {
reload_services_from_disk(mesh, "unregister").await?;
}
tracing::info!(service = %name, dropped_ephemeral, removed_config, "unregistered service");
Ok(())
}
pub(crate) async fn set_relays(
state: &DaemonState,
relay_urls: Vec<String>,
) -> Result<SetRelaysResult> {
let mesh = state.mesh_required()?;
anyhow::ensure!(
!relay_urls.is_empty(),
"set_relays: relay_urls is empty (custom mode requires at least one relay; \
disable relays via a relay_mode=\"disabled\" restart)"
);
let parsed: Vec<iroh::RelayUrl> = relay_urls
.iter()
.map(|u| {
u.parse::<iroh::RelayUrl>()
.map_err(|e| anyhow::anyhow!("set_relays: relay url {u:?}: {e}"))
})
.collect::<Result<_>>()?;
let _reload = mesh.reload_lock.lock().await;
let posture = mesh.applied_relays();
let restart_required = posture.mode != "custom";
let desired_norm: Vec<String> = parsed.iter().map(|r| r.to_string()).collect();
let current_norm: Vec<String> = posture
.urls
.iter()
.filter_map(|u| u.parse::<iroh::RelayUrl>().ok().map(|r| r.to_string()))
.collect();
let desired_set: BTreeSet<&str> = desired_norm.iter().map(String::as_str).collect();
let current_set: BTreeSet<&str> = current_norm.iter().map(String::as_str).collect();
if current_set == desired_set {
return Ok(SetRelaysResult {
changed: false,
restart_required,
});
}
let config_path = mesh.config_path.clone();
let persisted = desired_norm.clone();
blocking("set_relays config write", move || {
write_relays(&config_path, &persisted)
})
.await??;
if !restart_required {
for (ru, norm) in parsed.iter().zip(desired_norm.iter()) {
if !current_set.contains(norm.as_str()) {
mesh.endpoint
.insert_relay(ru.clone(), Arc::new(iroh::RelayConfig::from(ru.clone())))
.await;
}
}
for norm in ¤t_norm {
if !desired_set.contains(norm.as_str())
&& let Ok(ru) = norm.parse::<iroh::RelayUrl>()
{
mesh.endpoint.remove_relay(&ru).await;
}
}
}
let new_mode = if restart_required {
&posture.mode
} else {
"custom"
};
mesh.set_applied_relays(new_mode, &desired_norm);
tracing::info!(
count = desired_norm.len(),
restart_required,
"set custom relay set"
);
Ok(SetRelaysResult {
changed: true,
restart_required,
})
}
pub async fn grant_service_access(
mesh: &Arc<MeshState>,
principal: &str,
display_nickname: &str,
services: &[String],
) -> Result<()> {
let _reload = mesh.reload_lock.lock().await;
let config_path = mesh.config_path.clone();
let principal_w = principal.to_string();
let config_services = services.to_vec();
let known_ephemeral: HashSet<String> = {
let map = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned");
services
.iter()
.filter(|s| map.contains_key(*s))
.cloned()
.collect()
};
let changed = blocking("join grant config write", move || {
append_allow_to_config(
&config_path,
&principal_w,
&config_services,
&known_ephemeral,
)
})
.await??;
let mut changed = changed;
for svc in services {
if let Some(moved) = mesh.grant_ephemeral(svc, principal) {
changed |= moved;
}
}
if changed {
reload_services_from_disk(mesh, "grant").await?;
}
tracing::info!(peer = %display_nickname, ?services, changed, "granted service access");
mesh.audit().record(AuditRecord::trust(
now_ts(),
"pair".into(),
Some(display_nickname.to_string()),
Some(principal.to_string()),
));
Ok(())
}
async fn apply_ephemeral_allow(mesh: &Arc<MeshState>, service: &str, why: &str) -> Result<()> {
let allow = {
let map = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned");
map.get(service).map(|e| e.allow.clone())
};
let updated = allow.and_then(|allow| mesh.services.get().with_allow_replaced(service, allow));
match updated {
Some(services) => {
swap_services(mesh, services);
Ok(())
}
None => reload_services_from_disk(mesh, why).await,
}
}
async fn service_servable_in_config(mesh: &Arc<MeshState>, service: &str) -> Result<bool> {
let config_path = mesh.config_path.clone();
let service = service.to_string();
blocking("join service-exists config read", move || {
let cfg = Config::load(&config_path)
.map_err(|e| anyhow::anyhow!("config error in {}: {e}", config_path.display()))?;
Ok(cfg
.services
.get(&service)
.is_some_and(|svc| svc.backend_result().is_ok()))
})
.await?
}
#[derive(Debug)]
pub struct NoSuchService(pub String);
impl std::fmt::Display for NoSuchService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"no service named '{}' that this daemon can serve — register it first \
('mcpmesh serve', or register_service), or check the config entry names exactly one \
of `run` / `socket`",
self.0
)
}
}
impl std::error::Error for NoSuchService {}
pub(crate) async fn service_allow_grant(
state: &DaemonState,
service: String,
principal: String,
) -> Result<()> {
grant_service_allow(state.mesh_required()?, service, principal).await
}
pub async fn grant_service_allow(
mesh: &Arc<MeshState>,
service: String,
principal: String,
) -> Result<()> {
let _reload = mesh.reload_lock.lock().await;
let is_ephemeral = {
let map = mesh
.ephemeral_services
.lock()
.expect("ephemeral_services lock not poisoned");
map.contains_key(&service)
};
if !is_ephemeral && !service_servable_in_config(mesh, &service).await? {
anyhow::bail!(NoSuchService(service));
}
let config_path = mesh.config_path.clone();
let (principal_w, services_w) = (principal.clone(), vec![service.clone()]);
let known_ephemeral: HashSet<String> = if is_ephemeral {
std::iter::once(service.clone()).collect()
} else {
HashSet::new()
};
let config_moved = blocking("join service-allow grant config write", move || {
append_allow_to_config(&config_path, &principal_w, &services_w, &known_ephemeral)
})
.await??;
let ephemeral_moved = mesh.grant_ephemeral(&service, &principal).unwrap_or(false);
if config_moved {
reload_services_from_disk(mesh, "service-allow-grant").await?;
} else if ephemeral_moved {
apply_ephemeral_allow(mesh, &service, "service-allow-grant").await?;
}
let changed = config_moved || ephemeral_moved;
tracing::info!(%service, %principal, changed, "service allow granted");
Ok(())
}
pub(crate) async fn service_allow_revoke(
state: &DaemonState,
service: String,
principal: String,
) -> Result<()> {
revoke_service_allow(state.mesh_required()?, service, principal).await
}
pub async fn revoke_service_allow(
mesh: &Arc<MeshState>,
service: String,
principal: String,
) -> Result<()> {
let _reload = mesh.reload_lock.lock().await;
let ephemeral_moved = mesh.revoke_ephemeral(&service, &principal);
let config_path = mesh.config_path.clone();
let (svc_w, principal_w) = (service.clone(), principal.clone());
let config_moved = blocking("join service-allow revoke config write", move || {
remove_principal_from_service(&config_path, &svc_w, &principal_w)
})
.await??;
if ephemeral_moved.is_none()
&& !config_moved
&& !service_servable_in_config(mesh, &service).await?
{
anyhow::bail!(NoSuchService(service));
}
let changed = config_moved || ephemeral_moved.unwrap_or(false);
let severed = if changed {
if config_moved {
reload_services_from_disk(mesh, "service-allow-revoke").await?;
} else {
apply_ephemeral_allow(mesh, &service, "service-allow-revoke").await?;
}
sever_principal(mesh, &principal).await?
} else {
0
};
tracing::info!(%service, %principal, changed, severed, "service allow revoked");
Ok(())
}
async fn sever_principal(mesh: &Arc<MeshState>, principal: &str) -> Result<usize> {
sever_principals(mesh, std::slice::from_ref(&principal.to_string())).await
}
async fn sever_principals(mesh: &Arc<MeshState>, principals: &[String]) -> Result<usize> {
let observer = mesh
.sever_observer
.lock()
.expect("sever observer lock not poisoned")
.clone();
if let Some(observe) = observer {
observe(&mesh.services.get());
}
let store = mesh.store.clone();
let roster = mesh.roster.view();
let principals_w = principals.to_vec();
let targets = blocking("join sever principal resolution", move || {
let mut all = std::collections::HashSet::new();
for principal in &principals_w {
all.extend(crate::daemon::sever::endpoints_for_principal(
&store,
roster.as_deref(),
principal,
)?);
}
anyhow::Ok(all)
})
.await??;
if targets.is_empty() {
return Ok(0);
}
Ok(mesh.conn_registry.sever_matching(
mcpmesh_net::CLOSE_UNAUTHORIZED, b"access revoked",
|eid, _| targets.contains(eid),
))
}
pub async fn revoke_service_access(mesh: &Arc<MeshState>, nickname: &str) -> Result<bool> {
let _reload = mesh.reload_lock.lock().await;
let store = mesh.store.clone();
let nick_r = nickname.to_string();
let principals: Vec<String> = blocking("join revoke principal resolution", move || {
let (targets, others): (Vec<_>, Vec<_>) = store
.list()?
.into_iter()
.partition(|e| e.nickname == nick_r);
let mut principals = Vec::new();
for target in &targets {
principals.push(mcpmesh_net::EndpointId::from_bytes(target.endpoint_id).principal());
if let Some(user_id) = &target.user_id {
let shared_with_a_paired_device = others
.iter()
.any(|o| o.user_id.as_deref() == Some(user_id) && o.paired_at.is_some());
if !shared_with_a_paired_device && !principals.contains(user_id) {
principals.push(user_id.clone());
}
}
}
anyhow::Ok(principals)
})
.await??;
if principals.is_empty() {
tracing::info!(peer = %nickname, changed = false, "revoked service access");
return Ok(false);
}
let config_path = mesh.config_path.clone();
let principals_w = principals.clone(); let changed = blocking("join revoke config write", move || {
remove_allow_from_config(&config_path, &principals_w)
})
.await??;
if changed {
reload_services_from_disk(mesh, "revoke").await?;
}
let severed = sever_principals(mesh, &principals).await?;
tracing::info!(peer = %nickname, changed, severed, "revoked service access");
Ok(changed)
}
pub(crate) async fn open_session<CR, CW>(
state: &DaemonState,
peer: &str,
service: &str,
control_reader: FrameReader<CR>,
mut control_writer: CW,
) -> Result<()>
where
CR: AsyncRead + Unpin + Send,
CW: AsyncWrite + Unpin + Send,
{
let Some(mesh) = state.mesh() else {
let _ = write_frame(
&mut control_writer,
&synthesized(Value::Null, ERR_UNREACHABLE, "daemon has no mesh"),
)
.await;
return Ok(());
};
let transport = match dial_service(mesh, peer, service).await {
Ok(t) => t,
Err(e) => {
mesh.audit().record(
AuditRecord::session_open(
now_ts(),
Some(peer.to_string()),
service.to_string(),
None,
)
.with_status("error"),
);
tracing::warn!(peer, service, %e, "open_session dial failed; answering -32055");
let _ = write_frame(
&mut control_writer,
&synthesized(Value::Null, ERR_UNREACHABLE, "peer unreachable"),
)
.await;
return Ok(());
}
};
pipe_session(transport, service, control_reader, control_writer).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::testutil::hermetic_mesh;
#[tokio::test(flavor = "multi_thread")]
async fn unregister_service_removes_the_entry_idempotently() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = [\"eid:beef\"]\n [services.notes]\nsocket = \"/run/notes.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path.clone()).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let has = |name: &str| {
crate::config::Config::load(&config_path)
.unwrap()
.services
.contains_key(name)
};
assert!(has("kb") && has("notes"));
unregister_service(&state, "kb".into()).await.unwrap();
assert!(!has("kb"), "kb removed");
assert!(has("notes"), "other services untouched");
unregister_service(&state, "kb".into()).await.unwrap();
unregister_service(&state, "ghost".into()).await.unwrap();
assert!(has("notes"));
}
#[tokio::test(flavor = "multi_thread")]
async fn peer_diagnostics_reports_the_hint_the_dial_would_actually_use() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let key = iroh::SecretKey::from_bytes(&[3u8; 32]);
let eid = *key.public().as_bytes();
let other = *iroh::SecretKey::from_bytes(&[4u8; 32]).public().as_bytes();
let seed = |last_addr: Option<String>| {
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: eid,
nickname: "jetson".into(),
services: vec![],
paired_at: Some("1753000000".into()),
user_id: None,
last_addr,
})
.unwrap();
};
seed(None);
let d = peer_diagnostics(&state, "jetson").await.unwrap();
assert_eq!(d.nickname, "jetson");
assert!(d.principal.starts_with("eid:"), "{}", d.principal);
assert_eq!(d.last_addr, None);
assert!(!d.hint_usable, "no hint cannot be a usable hint");
assert!(d.hint_addrs.is_empty());
assert_eq!(d.paired_at.as_deref(), Some("1753000000"));
let good = serde_json::to_string(&iroh::EndpointAddr::from_parts(
key.public(),
[iroh::TransportAddr::Ip(
"192.168.1.50:4433".parse().unwrap(),
)],
))
.unwrap();
seed(Some(good.clone()));
let d = peer_diagnostics(&state, "jetson").await.unwrap();
assert_eq!(d.last_addr.as_deref(), Some(good.as_str()));
assert!(d.hint_usable, "a well-formed hint for THIS peer is usable");
assert_eq!(d.hint_addrs, vec!["192.168.1.50:4433".to_string()]);
let mismatched = serde_json::to_string(&iroh::EndpointAddr::from_parts(
iroh::EndpointId::from_bytes(&other).unwrap(),
[iroh::TransportAddr::Ip(
"192.168.1.99:4433".parse().unwrap(),
)],
))
.unwrap();
seed(Some(mismatched.clone()));
let d = peer_diagnostics(&state, "jetson").await.unwrap();
assert_eq!(
d.last_addr.as_deref(),
Some(mismatched.as_str()),
"the stored value is reported verbatim — that is the evidence"
);
assert!(
!d.hint_usable,
"a hint for a different endpoint is discarded at every dial; saying otherwise sends \
the reader after an address the node never uses"
);
assert!(
d.hint_addrs.is_empty(),
"and its addresses are not this peer's"
);
seed(Some("not json at all".into()));
let d = peer_diagnostics(&state, "jetson").await.unwrap();
assert!(!d.hint_usable);
assert_eq!(d.last_addr.as_deref(), Some("not json at all"));
let relay_only = serde_json::to_string(&iroh::EndpointAddr::from_parts(
key.public(),
[iroh::TransportAddr::Relay(
"https://user:token@relay.example/".parse().unwrap(),
)],
))
.unwrap();
seed(Some(relay_only));
let d = peer_diagnostics(&state, "jetson").await.unwrap();
assert_eq!(
d.hint_addrs.len(),
1,
"the relay hint must be REPORTED: {d:?}"
);
assert!(d.hint_addrs[0].starts_with("relay "), "{:?}", d.hint_addrs);
assert!(
!d.hint_addrs[0].contains("token"),
"a relay URL's userinfo must be SANITIZED — this output is meant to be pasted into an \
issue, and every other surface sanitizes it: {:?}",
d.hint_addrs
);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_introduction_installs_identity_and_grants_nothing() {
use mcpmesh_local_api::PeerIntroduceParams;
use mcpmesh_trust::keys::UserKey;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let (carol, _) = UserKey::load_or_generate(&dir.path().join("carol.key")).unwrap();
let carol_uid = mcpmesh_trust::binding::user_id(&carol);
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: [0xC0; 32],
nickname: "carol".into(),
services: vec!["notes".into()],
paired_at: Some("1".into()),
user_id: Some(carol_uid.clone()),
last_addr: None,
})
.unwrap();
let eid_from = |seed: u8| -> ([u8; 32], String) {
let pk = iroh::SecretKey::from_bytes(&[seed; 32]).public();
(*pk.as_bytes(), pk.to_string())
};
let (bob_eid, bob_str) = eid_from(0xBB);
let evidence = mcpmesh_trust::binding::endorse(&carol, &bob_eid, None).unwrap();
let params = |endorsed_by: &str, evidence: &str, nickname: &str| PeerIntroduceParams {
subject: bob_str.clone(),
endorsed_by: endorsed_by.to_string(),
evidence: evidence.to_string(),
subject_user_id: None,
subject_binding: None,
nickname: nickname.to_string(),
};
introduce_peer(&state, params(&carol_uid, &evidence, "bob"))
.await
.expect("a valid endorsement from a paired peer installs the subject");
let bob = mesh
.store
.resolve(&bob_eid)
.unwrap()
.expect("the subject is now resolvable");
assert_eq!(bob.nickname, "bob");
assert!(
bob.services.is_empty(),
"AN INTRODUCTION MUST GRANT NOTHING — this is the property that bounds the whole \
feature: a compromised endorser can make us KNOW about a peer, never SERVE it. Got: \
{:?}",
bob.services
);
assert_eq!(
bob.paired_at, None,
"no SAS happened, so nothing may claim a pairing stamp"
);
let (mallory, _) = UserKey::load_or_generate(&dir.path().join("m.key")).unwrap();
let m_uid = mcpmesh_trust::binding::user_id(&mallory);
let m_ev = mcpmesh_trust::binding::endorse(&mallory, &eid_from(0xEE).0, None).unwrap();
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: eid_from(0xEE).1,
endorsed_by: m_uid,
evidence: m_ev,
subject_user_id: None,
subject_binding: None,
nickname: "eve".into(),
},
)
.await
.expect_err("an endorsement from a peer we never paired with must be refused");
assert!(
format!("{e:#}").contains("currently paired"),
"and say why: {e:#}"
);
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: eid_from(0xAA).1,
endorsed_by: carol_uid.clone(),
evidence: evidence.clone(),
subject_user_id: None,
subject_binding: None,
nickname: "someone".into(),
},
)
.await
.expect_err("an endorsement naming bob must not install someone else");
assert!(format!("{e:#}").contains("does not verify"), "{e:#}");
let ours = mesh.endpoint.id().to_string();
let self_ev =
mcpmesh_trust::binding::endorse(&carol, mesh.endpoint.id().as_bytes(), None).unwrap();
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: ours,
endorsed_by: carol_uid.clone(),
evidence: self_ev,
subject_user_id: None,
subject_binding: None,
nickname: "me".into(),
},
)
.await
.expect_err("introducing ourselves must be refused");
assert!(format!("{e:#}").contains("own endpoint id"), "{e:#}");
for bad in ["with/slash", "", " "] {
let ev_bad = mcpmesh_trust::binding::endorse(&carol, &eid_from(0xAB).0, None).unwrap();
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: eid_from(0xAB).1,
endorsed_by: carol_uid.clone(),
evidence: ev_bad,
subject_user_id: None,
subject_binding: None,
nickname: bad.to_string(),
},
)
.await
.unwrap_err();
assert!(
format!("{e:#}").contains("nickname"),
"a {bad:?} nickname must be refused by the shared validator: {e:#}"
);
}
let ev2 = mcpmesh_trust::binding::endorse(&carol, &eid_from(0xDD).0, None).unwrap();
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: eid_from(0xDD).1,
endorsed_by: carol_uid.clone(),
evidence: ev2,
subject_user_id: None,
subject_binding: None,
nickname: "carol".into(),
},
)
.await
.expect_err("a nickname already used for a different peer must be refused");
assert!(format!("{e:#}").contains("already use the name"), "{e:#}");
}
#[tokio::test(flavor = "multi_thread")]
async fn an_endorser_cannot_hand_the_subject_someone_elses_user_id() {
use mcpmesh_local_api::PeerIntroduceParams;
use mcpmesh_trust::keys::UserKey;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let (carol, _) = UserKey::load_or_generate(&dir.path().join("carol.key")).unwrap();
let carol_uid = mcpmesh_trust::binding::user_id(&carol);
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: [0xC0; 32],
nickname: "carol".into(),
services: vec![],
paired_at: Some("1".into()),
user_id: Some(carol_uid.clone()),
last_addr: None,
})
.unwrap();
let (alice, _) = UserKey::load_or_generate(&dir.path().join("alice.key")).unwrap();
let alice_uid = mcpmesh_trust::binding::user_id(&alice);
let mallory_pk = iroh::SecretKey::from_bytes(&[0x4D; 32]).public();
let mallory_eid = *mallory_pk.as_bytes();
let ev = mcpmesh_trust::binding::endorse(&carol, &mallory_eid, Some(&alice_uid)).unwrap();
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: mallory_pk.to_string(),
endorsed_by: carol_uid.clone(),
evidence: ev.clone(),
subject_user_id: Some(alice_uid.clone()),
subject_binding: None,
nickname: "mallory".into(),
},
)
.await
.expect_err("a user_id vouched for by the ENDORSER alone must be refused");
assert!(
format!("{e:#}").contains("subject_binding"),
"and say the subject must prove it: {e:#}"
);
let forged = mcpmesh_trust::binding::present(&alice, &mallory_eid).1;
let ok = introduce_peer(
&state,
PeerIntroduceParams {
subject: mallory_pk.to_string(),
endorsed_by: carol_uid.clone(),
evidence: ev,
subject_user_id: Some(alice_uid.clone()),
subject_binding: Some(forged),
nickname: "mallory".into(),
},
)
.await;
assert!(
ok.is_ok(),
"a binding Alice herself signed is valid by construction: {ok:?}"
);
let mallory2 = iroh::SecretKey::from_bytes(&[0x4E; 32]).public();
let ev2 =
mcpmesh_trust::binding::endorse(&carol, mallory2.as_bytes(), Some(&alice_uid)).unwrap();
let carol_forgery = mcpmesh_trust::binding::present(&carol, mallory2.as_bytes()).1;
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: mallory2.to_string(),
endorsed_by: carol_uid.clone(),
evidence: ev2,
subject_user_id: Some(alice_uid),
subject_binding: Some(carol_forgery),
nickname: "mallory2".into(),
},
)
.await
.expect_err("a binding signed by the ENDORSER's key cannot vouch for the VICTIM's user_id");
assert!(format!("{e:#}").contains("does not verify"), "{e:#}");
}
#[tokio::test(flavor = "multi_thread")]
async fn an_introduced_peer_cannot_introduce_others() {
use mcpmesh_local_api::PeerIntroduceParams;
use mcpmesh_trust::keys::UserKey;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let (carol, _) = UserKey::load_or_generate(&dir.path().join("carol.key")).unwrap();
let carol_uid = mcpmesh_trust::binding::user_id(&carol);
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: [0xC0; 32],
nickname: "carol".into(),
services: vec![],
paired_at: Some("1".into()),
user_id: Some(carol_uid.clone()),
last_addr: None,
})
.unwrap();
let (bobkey, _) = UserKey::load_or_generate(&dir.path().join("bob.key")).unwrap();
let bob_uid = mcpmesh_trust::binding::user_id(&bobkey);
let bob_pk = iroh::SecretKey::from_bytes(&[0xB0; 32]).public();
let bob_eid = *bob_pk.as_bytes();
introduce_peer(
&state,
PeerIntroduceParams {
subject: bob_pk.to_string(),
endorsed_by: carol_uid,
evidence: mcpmesh_trust::binding::endorse(&carol, &bob_eid, Some(&bob_uid))
.unwrap(),
subject_user_id: Some(bob_uid.clone()),
subject_binding: Some(mcpmesh_trust::binding::present(&bobkey, &bob_eid).1),
nickname: "bob".into(),
},
)
.await
.expect("carol is paired, so her endorsement installs bob");
let bob_row = mesh
.store
.resolve(&bob_eid)
.unwrap()
.expect("bob is installed");
assert_eq!(
bob_row.user_id.as_deref(),
Some(bob_uid.as_str()),
"a user_id the SUBJECT proved must be written — dropping it silently loses \
multi-device resolution, which is the reason the field exists"
);
let dave_pk = iroh::SecretKey::from_bytes(&[0xDA; 32]).public();
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: dave_pk.to_string(),
endorsed_by: bob_uid,
evidence: mcpmesh_trust::binding::endorse(&bobkey, dave_pk.as_bytes(), None)
.unwrap(),
subject_user_id: None,
subject_binding: None,
nickname: "dave".into(),
},
)
.await
.expect_err("an INTRODUCED peer must not be able to introduce others");
assert!(
format!("{e:#}").contains("currently paired"),
"the endorser check must require a PAIRING, not merely a stored user_id — otherwise \
introductions chain to unbounded depth: {e:#}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn endorse_peer_signs_the_subject_it_was_asked_about() {
use mcpmesh_local_api::PeerEndorseParams;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let key_path = dir.path().join("user.key");
mesh.set_user_key_path(key_path.clone());
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let subject = iroh::SecretKey::from_bytes(&[0x77; 32]).public();
let res = endorse_peer(
&state,
PeerEndorseParams {
subject: subject.to_string(),
subject_user_id: None,
},
)
.await
.expect("endorsing produces evidence");
mcpmesh_trust::binding::verify_endorsement(
&res.endorsed_by,
&res.evidence,
subject.as_bytes(),
None,
)
.expect("the evidence must verify for the subject we asked about");
let other = iroh::SecretKey::from_bytes(&[0x78; 32]).public();
assert!(
mcpmesh_trust::binding::verify_endorsement(
&res.endorsed_by,
&res.evidence,
other.as_bytes(),
None,
)
.is_err(),
"the signature must name the subject, not merely be well-formed"
);
let (uk, _) = mcpmesh_trust::UserKey::load_or_generate(&key_path).unwrap();
assert_eq!(res.endorsed_by, mcpmesh_trust::binding::user_id(&uk));
assert!(
mesh.store.list().unwrap().is_empty(),
"endorsing a peer must not install it locally"
);
endorse_peer(
&state,
PeerEndorseParams {
subject: "not-an-endpoint-id".into(),
subject_user_id: None,
},
)
.await
.expect_err("a malformed subject must be refused");
}
#[tokio::test(flavor = "multi_thread")]
async fn an_introduction_cannot_outlive_the_unpairing_of_its_endorser() {
use mcpmesh_local_api::PeerIntroduceParams;
use mcpmesh_trust::keys::UserKey;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
let (carol, _) = UserKey::load_or_generate(&dir.path().join("carol.key")).unwrap();
let carol_uid = mcpmesh_trust::binding::user_id(&carol);
std::fs::write(
&config_path,
format!("[services.notes]\nsocket = \"/run/n.sock\"\nallow = [\"{carol_uid}\"]\n"),
)
.unwrap();
let mesh = hermetic_mesh(config_path.clone()).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: [0xC0; 32],
nickname: "carol".into(),
services: vec!["notes".into()],
paired_at: Some("1".into()),
user_id: Some(carol_uid.clone()),
last_addr: None,
})
.unwrap();
let second = iroh::SecretKey::from_bytes(&[0xC1; 32]).public();
introduce_peer(
&state,
PeerIntroduceParams {
subject: second.to_string(),
endorsed_by: carol_uid.clone(),
evidence: mcpmesh_trust::binding::endorse(
&carol,
second.as_bytes(),
Some(&carol_uid),
)
.unwrap(),
subject_user_id: Some(carol_uid.clone()),
subject_binding: Some(mcpmesh_trust::binding::present(&carol, second.as_bytes()).1),
nickname: "carols-other-laptop".into(),
},
)
.await
.expect("carol is paired, so her self-endorsement installs");
remove_peer(
&state,
mcpmesh_local_api::PeerRemoveParams {
nickname: "carol".into(),
},
)
.await
.expect("unpair succeeds");
let cfg = std::fs::read_to_string(&config_path).unwrap();
assert!(
!cfg.contains(&carol_uid),
"unpairing must STRIP the grant even though an INTRODUCED row still shares the \
user_id — an introduced row is not a device you vouched for, so counting it as \
'still trusted' lets one pasted endorsement survive the unpairing forever. Config \
still reads:\n{cfg}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn introduce_refuses_the_shapes_it_claims_to_and_audits_the_write() {
use mcpmesh_local_api::PeerIntroduceParams;
use mcpmesh_trust::keys::UserKey;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let audit =
crate::audit::AuditSink::new(crate::audit::AuditLog::spawn(dir.path().join("audit")));
mesh.set_audit(audit.clone());
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let (carol, _) = UserKey::load_or_generate(&dir.path().join("carol.key")).unwrap();
let carol_uid = mcpmesh_trust::binding::user_id(&carol);
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: [0xC0; 32],
nickname: "carol".into(),
services: vec![],
paired_at: Some("1".into()),
user_id: Some(carol_uid.clone()),
last_addr: None,
})
.unwrap();
let subj = iroh::SecretKey::from_bytes(&[0x51; 32]).public();
let ev = mcpmesh_trust::binding::endorse(&carol, subj.as_bytes(), None).unwrap();
let base = |uid: Option<String>, bind: Option<String>| PeerIntroduceParams {
subject: subj.to_string(),
endorsed_by: carol_uid.clone(),
evidence: ev.clone(),
subject_user_id: uid,
subject_binding: bind,
nickname: "subj".into(),
};
let e = introduce_peer(&state, base(None, Some("b64u:whatever".into())))
.await
.expect_err("subject_binding without subject_user_id must be refused");
assert!(format!("{e:#}").contains("nothing to bind"), "{e:#}");
let mut rx = audit.subscribe().expect("auditing enabled");
introduce_peer(&state, base(None, None)).await.unwrap();
let mut recs = Vec::new();
while let Ok(r) = rx.try_recv() {
recs.push(r);
}
let rec = recs
.iter()
.find(|r| r.event.as_deref() == Some("peer_introduce"))
.expect(
"a trust-establishing write with NO human ceremony is exactly the one an operator \
needs a record of — pair/unpair/roster_install all emit one",
);
assert_eq!(rec.target.as_deref(), Some("subj"));
assert_eq!(
rec.principal.as_deref(),
Some(carol_uid.as_str()),
"the record must name the ENDORSER — the question anyone reading it will have"
);
let paired = iroh::SecretKey::from_bytes(&[0x52; 32]).public();
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: *paired.as_bytes(),
nickname: "dana".into(),
services: vec![],
paired_at: Some("7".into()),
user_id: None,
last_addr: Some("hint".into()),
})
.unwrap();
let ev2 = mcpmesh_trust::binding::endorse(&carol, paired.as_bytes(), None).unwrap();
let e = introduce_peer(
&state,
PeerIntroduceParams {
subject: paired.to_string(),
endorsed_by: carol_uid.clone(),
evidence: ev2,
subject_user_id: None,
subject_binding: None,
nickname: "dana2".into(),
},
)
.await
.expect_err("introducing an already-PAIRED peer must be refused");
assert!(format!("{e:#}").contains("already paired"), "{e:#}");
assert_eq!(
mesh.store
.resolve(paired.as_bytes())
.unwrap()
.unwrap()
.last_addr,
Some("hint".into()),
"and the SAS-proven row must be untouched — an upsert would have destroyed its hint"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn unpairing_the_endorser_revokes_their_introductions() {
use mcpmesh_local_api::PeerIntroduceParams;
use mcpmesh_trust::keys::UserKey;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let (carol, _) = UserKey::load_or_generate(&dir.path().join("carol.key")).unwrap();
let carol_uid = mcpmesh_trust::binding::user_id(&carol);
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: [0xC0; 32],
nickname: "carol".into(),
services: vec![],
paired_at: Some("1".into()),
user_id: Some(carol_uid.clone()),
last_addr: None,
})
.unwrap();
let bob_pk = iroh::SecretKey::from_bytes(&[0xBB; 32]).public();
let ev = mcpmesh_trust::binding::endorse(&carol, bob_pk.as_bytes(), None).unwrap();
let p = |n: &str| PeerIntroduceParams {
subject: bob_pk.to_string(),
endorsed_by: carol_uid.clone(),
evidence: ev.clone(),
subject_user_id: None,
subject_binding: None,
nickname: n.to_string(),
};
introduce_peer(&state, p("bob"))
.await
.expect("works while paired");
mesh.store.remove("carol").unwrap();
let e = introduce_peer(&state, p("bob2"))
.await
.expect_err("an endorsement from an UNPAIRED peer must be refused");
assert!(
format!("{e:#}").contains("currently paired"),
"the check must be on the CURRENT store, not on whether the signature is valid — a \
signature stays valid forever, which is exactly why the chain has to be live: {e:#}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn register_service_cannot_uncap_a_service() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "[limits]\nrate_limit_per_min = 5\n").unwrap();
let mesh = hermetic_mesh(config_path).await;
mesh.set_limits(crate::limits::MeshLimiters::from_config(
&crate::config::LimitsCfg {
rate_limit_per_min: 5,
..Default::default()
},
));
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let params = |rate: Option<u32>| RegisterServiceParams {
name: "svc".into(),
backend: mcpmesh_local_api::BackendSpec::Socket {
path: "/run/svc.sock".into(),
},
allow: vec![],
ephemeral: true,
rate_limit_per_min: rate,
};
let e = register_service(&state, params(Some(0)))
.await
.expect_err("rate_limit_per_min = 0 must be refused");
assert!(
format!("{e:#}").contains("at least 1"),
"and say what a valid value is: {e:#}"
);
register_service(&state, params(Some(2)))
.await
.expect("a below-ceiling rate registers");
assert_eq!(
mesh.limits().tracked_rpm("svc"),
Some(Some(2)),
"an EPHEMERAL registration's rate must reach its backend's bucket — dropping it here \
is the #55 shape: a per-service feature that silently does nothing for ephemerals"
);
let persistent = RegisterServiceParams {
name: "persisted".into(),
backend: mcpmesh_local_api::BackendSpec::Socket {
path: "/run/p.sock".into(),
},
allow: vec![],
ephemeral: false,
rate_limit_per_min: Some(3),
};
register_service(&state, persistent)
.await
.expect("a persistent registration with a rate succeeds");
assert_eq!(
mesh.limits().tracked_rpm("persisted"),
Some(Some(3)),
"a PERSISTENT registration's rate must survive the config write and the reload — \
`ephemeral` defaults to false, so this is the default path"
);
register_service(&state, params(Some(1_000_000)))
.await
.expect("an over-ceiling rate is clamped, not rejected");
assert_eq!(
mesh.limits().tracked_rpm("svc"),
Some(Some(5)),
"the control path must clamp to [limits].rate_limit_per_min exactly as config does — \
one call must never be able to uncap a service"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn redeem_validates_as_nickname_before_it_touches_the_invite_line() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh);
let e = redeem(
&state,
"total-garbage-not-an-invite".into(),
Some(" ".into()),
)
.await
.expect_err("a blank as_nickname must be refused");
let msg = format!("{e:#}");
assert!(
msg.contains("as_nickname") && msg.contains("empty"),
"the ALIAS error must win over the invite-decode error — proving validation runs at \
this call site, and runs FIRST: {msg}"
);
let e = redeem(
&state,
"total-garbage-not-an-invite".into(),
Some("a/b".into()),
)
.await
.expect_err("a '/' in as_nickname must be refused");
assert!(format!("{e:#}").contains("as_nickname"), "{e:#}");
let e = redeem(&state, "total-garbage".into(), Some("fine".into()))
.await
.expect_err("the garbage line still fails");
assert!(
!format!("{e:#}").contains("as_nickname"),
"a valid alias must not be reported as an alias problem: {e:#}"
);
}
#[test]
fn an_alias_is_validated_the_same_way_a_nickname_is() {
for field in ["as_nickname", "peer_nickname"] {
assert_eq!(
validated_alias(field, None).unwrap(),
None,
"absent is fine"
);
assert_eq!(
validated_alias(field, Some(" alice ".into())).unwrap(),
Some("alice".into()),
"a valid alias is TRIMMED — otherwise ' alice ' evades the exact-byte collision \
check and renders as a duplicate of 'alice'"
);
for bad in ["", " ", "\t\n"] {
let e = validated_alias(field, Some(bad.into()))
.unwrap_err()
.to_string();
assert!(
e.contains(field) && e.contains("empty"),
"blank must be a clean error naming the field, never a silent fallback to the \
name the caller was trying to avoid: {e}"
);
}
let e = validated_alias(field, Some("alice/notes".into()))
.unwrap_err()
.to_string();
assert!(
e.contains('/') && e.contains(field),
"'/' must be refused — the porcelain splits <peer>/<service> at the first one, so \
the peer would be permanently unmountable: {e}"
);
validated_alias(field, Some("line1\nline2".into()))
.expect_err("control characters must be refused");
validated_alias(field, Some("a".repeat(MAX_ALIAS_CHARS + 1)))
.expect_err("an over-long alias must be refused");
validated_alias(field, Some("a".repeat(MAX_ALIAS_CHARS)))
.expect("exactly the cap is allowed — both sides of the boundary");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn an_enrolled_device_cannot_endorse_or_enroll() {
use mcpmesh_local_api::PeerEndorseParams;
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
mesh.set_user_key_path(dir.path().join("user.key"));
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let subject = iroh::SecretKey::from_bytes(&[0x91; 32]).public();
endorse_peer(
&state,
PeerEndorseParams {
subject: subject.to_string(),
subject_user_id: None,
},
)
.await
.expect("a device holding its own key can endorse");
mesh.set_self_binding_live(Some(crate::pairing::rendezvous::SelfBinding {
user_pk: "b64u:someone-elses".into(),
sig: "b64u:sig".into(),
}));
let e = endorse_peer(
&state,
PeerEndorseParams {
subject: subject.to_string(),
subject_user_id: None,
},
)
.await
.expect_err("an enrolled device must refuse to endorse");
assert!(
format!("{e:#}").contains("does not hold that user key"),
"and say WHY, since the caller's request looks reasonable: {e:#}"
);
assert!(
(mesh.inviter_ctx().sign_binding)(subject.as_bytes()).is_none(),
"an enrolled device must not sign a binding for a THIRD device — it would be for an \
identity no peer has seen"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn the_adopt_hook_persists_the_binding_and_installs_it_live() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
mesh.set_user_key_path(dir.path().join("user.key"));
let binding = crate::pairing::rendezvous::SelfBinding {
user_pk: "b64u:someone-elses-identity".into(),
sig: "b64u:sig".into(),
};
adopt_hook(&mesh)(binding.clone())
.await
.expect("the hook persists and installs");
let path = mesh.adopted_binding_path();
let from_disk: crate::pairing::rendezvous::SelfBinding =
serde_json::from_slice(&std::fs::read(&path).expect("the binding was written"))
.expect("and is readable");
assert_eq!(from_disk, binding, "the file must round-trip the binding");
assert_eq!(
mesh.self_binding().expect("a binding").user_pk,
binding.user_pk,
"the adopted binding must take effect immediately, not only after a restart"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_self_invite_must_be_single_use_and_grant_nothing() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.notes]\nsocket = \"/run/n.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
let svc = || vec!["notes".to_string()];
let e = mint_invite(vec![], None, Some(3), None, true, &mesh)
.await
.expect_err("a multi-use SELF invite must be refused");
let msg = format!("{e:#}");
assert!(
msg.contains("as_self") && msg.contains('3'),
"the error must name the flag and the max_uses: {msg}"
);
assert!(
msg.contains("become this person"),
"and say WHY, since the caller asked for something reasonable-looking: {msg}"
);
let e = mint_invite(svc(), None, None, None, true, &mesh)
.await
.expect_err("a SELF invite that grants services must be refused");
assert!(format!("{e:#}").contains("grants nothing"), "{e:#}");
let minted = mint_invite(vec![], None, None, None, true, &mesh)
.await
.expect("a single-use, grant-free self invite is legal");
let decoded = crate::pairing::Invite::decode(&minted.invite_line).unwrap();
assert!(
decoded.as_self,
"as_self must ride the invite line, or the redeemer pairs instead of enrolling"
);
let ordinary = mint_invite(svc(), None, None, None, false, &mesh)
.await
.expect("an ordinary invite still mints");
assert!(
!crate::pairing::Invite::decode(&ordinary.invite_line)
.unwrap()
.as_self
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_peer_nickname_is_stored_but_never_travels_on_the_line() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.notes]\nsocket = \"/run/notes.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
let svc = || vec!["notes".to_string()];
let minted = mint_invite(svc(), None, None, Some("their-laptop".into()), false, &mesh)
.await
.expect("an alias on a single-use invite is fine");
let decoded = crate::pairing::Invite::decode(&minted.invite_line).expect("line decodes");
assert_eq!(
decoded.peer_nickname, None,
"the inviter's local alias must be STRIPPED from the invite line: {decoded:?}"
);
assert!(
!minted.invite_line.contains("their-laptop"),
"the alias must not be recoverable from the line at all"
);
let held = mesh
.invites
.peek_live_alias(&decoded.secret, crate::util::epoch_now_u64())
.expect("the invite is live");
assert_eq!(
held.as_deref(),
Some("their-laptop"),
"the alias must be retained daemon-side — it is stripped from the line, so this is \
the only place it can live"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn peer_nickname_is_refused_with_a_multi_use_invite_and_when_blank() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.notes]\nsocket = \"/run/notes.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
let svc = || vec!["notes".to_string()];
let e = mint_invite(svc(), None, Some(3), Some("them".into()), false, &mesh)
.await
.expect_err("an alias on a multi-use invite must be refused at MINT");
let msg = format!("{e:#}");
assert!(
msg.contains("peer_nickname") && msg.contains('3'),
"the error must name the field and the max_uses it conflicts with: {msg}"
);
assert!(
msg.contains("peer_rename"),
"and point at the recovery, or the caller has to guess: {msg}"
);
for blank in ["", " "] {
mint_invite(svc(), None, None, Some(blank.into()), false, &mesh)
.await
.expect_err("a blank alias must be a clean error, not a silent fallback");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn max_uses_is_clamped_and_zero_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.notes]\nsocket = \"/run/notes.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
let svc = || vec!["notes".to_string()];
let one = mint_invite(svc(), None, None, None, false, &mesh)
.await
.unwrap();
assert_eq!(one.uses_remaining, 1, "absent means single-use");
let three = mint_invite(svc(), None, Some(3), None, false, &mesh)
.await
.unwrap();
assert_eq!(three.uses_remaining, 3);
let capped = mint_invite(svc(), None, Some(10_000), None, false, &mesh)
.await
.unwrap();
assert_eq!(
capped.uses_remaining,
mcpmesh_local_api::MAX_INVITE_USES,
"over the cap is clamped, and the caller is told the value it ACTUALLY got"
);
let err = mint_invite(svc(), None, Some(0), None, false, &mesh)
.await
.expect_err("zero redemptions is a caller bug, not a valid invite");
assert!(
err.downcast_ref::<crate::control::InvalidParams>()
.is_some(),
"and it must be branchable as -32602 invalid params, not a generic failure: {err}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn minting_an_invite_writes_it_to_the_configured_file() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.notes]
socket = \"/run/notes.sock\"
allow = []
",
)
.unwrap();
let invites_path = dir.path().join("invites.json");
let mesh = crate::daemon::testutil::hermetic_mesh_with_invites(
config_path,
Arc::new(crate::pairing::LiveInvites::load(
invites_path.clone(),
crate::util::epoch_now_u64(),
)),
)
.await;
let res = mint_invite(vec!["notes".into()], None, None, None, false, &mesh)
.await
.expect("mint");
assert!(res.invite_line.starts_with("mcpmesh-invite:"));
let on_disk = crate::pairing::persist::InviteFile::new(&invites_path).load(0);
assert_eq!(
on_disk.len(),
1,
"the `invite` verb must leave the invite ON DISK — otherwise the 24h TTL it just \
advertised is a promise the next restart breaks (#87b)"
);
assert_eq!(
on_disk[0].expires_at_epoch, res.expires_at_epoch,
"and it must be THE invite that was handed out"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn peer_diagnostics_never_dials() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
for (i, nick) in [(21u8, "jetson"), (22u8, "studio")] {
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: *iroh::SecretKey::from_bytes(&[i; 32]).public().as_bytes(),
nickname: nick.into(),
services: vec![],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
}
let before = mesh.probe_seq_for_test();
let d = peer_diagnostics(&state, "jetson").await.unwrap();
assert_eq!(
mesh.probe_seq_for_test(),
before,
"peer_diagnostics took a probe ticket — it is dialing, and the peer it is meant to be \
observing is now being perturbed by the act of observing it"
);
assert_eq!(
d.reachability, None,
"never probed is NOT unreachable — reporting a fabricated `reachable: false` row on a \
fresh daemon would read as a real verdict in a capture"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn peer_diagnostics_joins_the_live_row_by_id_not_nickname() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let a = *iroh::SecretKey::from_bytes(&[31u8; 32]).public().as_bytes();
let b = *iroh::SecretKey::from_bytes(&[32u8; 32]).public().as_bytes();
for eid in [a, b] {
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: eid,
nickname: "jetson".into(), services: vec![],
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
}
mesh.reachability.lock().unwrap().insert(
b,
crate::daemon::ReachEntry {
reachable: true,
rtt_ms: Some(9),
probed_at: crate::util::epoch_now_i64(),
meta: String::new(),
services: Vec::new(),
seq: 1,
path: mcpmesh_local_api::PeerPath::Direct,
},
);
let d = peer_diagnostics(&state, &mcpmesh_net::EndpointId::from_bytes(a).principal())
.await
.unwrap();
assert_eq!(
d.reachability, None,
"peer A has no live row of its OWN; borrowing its namesake's would report a direct, \
9ms link for a peer that has never been probed"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_exact_literal_revokes_a_bare_allow_entry() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = [\"legacy-nickname\", \"eid:beef\", \"ops-team\"]\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path.clone()).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let allow = || {
crate::config::Config::load(&config_path)
.unwrap()
.services
.get("kb")
.unwrap()
.allow
.clone()
};
service_allow_revoke(&state, "kb".into(), "legacy-nickname".into())
.await
.expect("a bare entry is a valid revoke target");
assert_eq!(
allow(),
vec!["eid:beef".to_string(), "ops-team".to_string()],
"the exact literal is stripped and nothing else is"
);
service_allow_revoke(&state, "kb".into(), "legacy-nickname".into())
.await
.expect("revoking an absent entry is a no-op");
assert_eq!(
allow(),
vec!["eid:beef".to_string(), "ops-team".to_string()]
);
mesh.register_ephemeral(
"tmp".to_string(),
crate::daemon::EphemeralService {
backend: mcpmesh_local_api::BackendSpec::Socket {
path: "/run/tmp.sock".into(),
},
allow: vec!["legacy-nickname".to_string(), "eid:beef".to_string()],
rate_limit_per_min: None,
},
);
service_allow_revoke(&state, "tmp".into(), "legacy-nickname".into())
.await
.expect("a bare entry in an ephemeral allow is a valid target");
assert_eq!(
mesh.ephemeral_services
.lock()
.unwrap()
.get("tmp")
.unwrap()
.allow,
vec!["eid:beef".to_string()],
"the ephemeral allow lost the bare entry and kept the principal"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_exact_b64u_revoke_has_no_multi_device_protection() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = [\"b64u:alice\"]\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path.clone()).await;
for (i, nick) in [(7u8, "alice-laptop"), (8u8, "alice-phone")] {
mesh.store
.add(crate::allowlist::PeerEntry {
endpoint_id: [i; 32],
nickname: nick.into(),
services: vec![],
paired_at: Some("1".into()),
user_id: Some("b64u:alice".into()),
last_addr: None,
})
.unwrap();
}
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let allow = || {
crate::config::Config::load(&config_path)
.unwrap()
.services
.get("kb")
.unwrap()
.allow
.clone()
};
revoke_service_access(&mesh, "alice-laptop").await.unwrap();
assert_eq!(
allow(),
vec!["b64u:alice".to_string()],
"unpairing one device must never revoke the PERSON — the guard this verb lacks"
);
service_allow_revoke(&state, "kb".into(), "b64u:alice".into())
.await
.unwrap();
assert!(
allow().is_empty(),
"an exact literal revoke is exactly as literal as it sounds, shared or not"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn service_allow_grant_and_revoke_toggle_one_principal() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path.clone()).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let allow = || {
crate::config::Config::load(&config_path)
.unwrap()
.services
.get("kb")
.unwrap()
.allow
.clone()
};
service_allow_grant(&state, "kb".into(), "eid:beef".into())
.await
.unwrap();
assert_eq!(allow(), vec!["eid:beef".to_string()]);
service_allow_grant(&state, "kb".into(), "eid:beef".into())
.await
.unwrap();
assert_eq!(allow(), vec!["eid:beef".to_string()]);
service_allow_revoke(&state, "kb".into(), "eid:beef".into())
.await
.unwrap();
assert!(allow().is_empty());
service_allow_revoke(&state, "kb".into(), "eid:beef".into())
.await
.unwrap();
assert!(allow().is_empty());
let grant_err = service_allow_grant(&state, "ghost".into(), "eid:beef".into())
.await
.expect_err("an unknown service must not report success");
assert!(
grant_err.downcast_ref::<NoSuchService>().is_some(),
"the grant error must be branchable as NoSuchService, got: {grant_err}"
);
let revoke_err = service_allow_revoke(&state, "ghost".into(), "eid:beef".into())
.await
.expect_err("an unknown service must not report success");
assert!(
revoke_err.downcast_ref::<NoSuchService>().is_some(),
"the revoke error must be branchable as NoSuchService, got: {revoke_err}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_unknown_service_answers_the_no_such_service_code_on_the_wire() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh);
let req = |method: &str| {
serde_json::json!({
"jsonrpc": "2.0", "id": 1, "method": method,
"params": {"service": "ghost", "principal": "eid:beef"}
})
};
for method in ["service_allow_grant", "service_allow_revoke"] {
let r = crate::control::handle_request(&req(method), &state).await;
assert_eq!(
r["error"]["code"],
mcpmesh_local_api::ERR_NO_SUCH_SERVICE,
"{method} must answer -32040 for an unknown service, got: {r}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn revoking_a_shadowed_name_strips_the_config_allow_too() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.room]\nsocket = \"/run/room.sock\"\nallow = [\"eid:beef\"]\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path.clone()).await;
mesh.register_ephemeral(
"room".to_string(),
crate::daemon::EphemeralService {
backend: mcpmesh_local_api::BackendSpec::Socket {
path: "/run/room.sock".into(),
},
allow: vec!["eid:beef".to_string()],
rate_limit_per_min: None,
},
);
revoke_service_allow(&mesh, "room".into(), "eid:beef".into())
.await
.unwrap();
assert!(
mesh.ephemeral_services
.lock()
.unwrap()
.get("room")
.unwrap()
.allow
.is_empty(),
"the ephemeral allow is stripped"
);
assert!(
crate::config::Config::load(&config_path)
.unwrap()
.services
.get("room")
.unwrap()
.allow
.is_empty(),
"the SHADOWED config allow must be stripped too — otherwise it goes live with a \
revoked principal the moment the ephemeral registration is dropped"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_failed_config_write_leaves_the_ephemeral_allow_untouched() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = \"not-an-array\"\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
mesh.register_ephemeral(
"room".to_string(),
crate::daemon::EphemeralService {
backend: mcpmesh_local_api::BackendSpec::Socket {
path: "/run/room.sock".into(),
},
allow: vec![],
rate_limit_per_min: None,
},
);
let r = grant_service_access(
&mesh,
"eid:beef",
"eid:beef",
&["room".to_string(), "kb".to_string()],
)
.await;
assert!(r.is_err(), "the malformed config must fail the grant");
assert!(
mesh.ephemeral_services
.lock()
.unwrap()
.get("room")
.unwrap()
.allow
.is_empty(),
"a FAILED grant must not have applied the in-memory half"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn allow_verbs_mutate_an_ephemeral_registration() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
mesh.register_ephemeral(
"room".to_string(),
crate::daemon::EphemeralService {
backend: mcpmesh_local_api::BackendSpec::Socket {
path: "/run/room.sock".into(),
},
allow: vec![],
rate_limit_per_min: None,
},
);
let allow = || {
mesh.ephemeral_services
.lock()
.unwrap()
.get("room")
.unwrap()
.allow
.clone()
};
service_allow_grant(&state, "room".into(), "eid:beef".into())
.await
.unwrap();
assert_eq!(allow(), vec!["eid:beef".to_string()], "granted in memory");
service_allow_grant(&state, "room".into(), "eid:beef".into())
.await
.unwrap();
assert_eq!(allow(), vec!["eid:beef".to_string()], "grant is idempotent");
service_allow_revoke(&state, "room".into(), "eid:beef".into())
.await
.unwrap();
assert!(allow().is_empty(), "revoked in memory");
service_allow_revoke(&state, "room".into(), "eid:beef".into())
.await
.unwrap();
assert!(allow().is_empty(), "revoke is idempotent");
assert!(
crate::config::Config::load(&mesh.config_path)
.unwrap()
.services
.get("kb")
.unwrap()
.allow
.is_empty(),
"an ephemeral grant must not write the config"
);
}
#[test]
fn unregistered_service_error_message_shapes() {
let s = |names: &[&str]| -> Vec<String> { names.iter().map(|n| n.to_string()).collect() };
assert_eq!(
unregistered_service_error(&s(&["notes"]), &s(&["notes", "kb"])),
None
);
assert_eq!(
unregistered_service_error(&s(&["nosuchsvc"]), &s(&["notes", "code"])).unwrap(),
"no service named 'nosuchsvc' — you serve: notes, code (see 'mcpmesh status')"
);
assert_eq!(
unregistered_service_error(&s(&["a", "notes", "b"]), &s(&["notes"])).unwrap(),
"no services named 'a', 'b' — you serve: notes (see 'mcpmesh status')"
);
assert_eq!(
unregistered_service_error(&s(&["nosuchsvc"]), &[]).unwrap(),
"no service named 'nosuchsvc' — nothing is served yet; register one with \
'mcpmesh serve <name> -- <command>'"
);
}
async fn blob_mesh() -> (tempfile::TempDir, Arc<MeshState>, Arc<DaemonState>) {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "").unwrap();
let mesh = hermetic_mesh(config_path).await;
let provider = crate::blobs::provider::AppBlobs::open_fetcher_with_progress(
dir.path().join("blobs"),
mesh.endpoint.clone(),
Some(mesh.blob_bcast_for_test().clone()),
)
.await
.expect("fetcher opens");
mesh.set_app_blobs(provider).await;
let state = Arc::new(crate::control::DaemonState::with_mesh("test", mesh.clone()));
(dir, mesh, state)
}
async fn test_provider(mesh: &Arc<MeshState>) -> Arc<crate::blobs::provider::AppBlobs> {
mesh.app_blobs().await.expect("provider installed")
}
fn hex_of(seed: &[u8]) -> String {
iroh_blobs::Hash::new(seed).to_hex().to_string()
}
#[tokio::test(flavor = "multi_thread")]
async fn a_cancel_for_a_hash_nothing_is_fetching_is_not_an_error() {
let (_dir, _mesh, state) = blob_mesh().await;
let r = blob_fetch_cancel(&state, &hex_of(b"absent")).expect("cancel answers");
assert!(!r.cancelled, "nothing was in flight");
assert!(blob_fetch_cancel(&state, "not-a-hash").is_err());
assert!(blob_fetch_cancel(&DaemonState::new("test"), &hex_of(b"absent")).is_err());
}
#[tokio::test(flavor = "multi_thread")]
async fn a_cancel_does_not_touch_other_in_flight_fetches() {
let (_dir, mesh, state) = blob_mesh().await;
let provider = test_provider(&mesh).await;
let (one, two) = (hex_of(b"one"), hex_of(b"two"));
let a = FetchGuard::register(&mesh, one.clone(), provider.clone());
let provider2 = provider.clone();
let b = FetchGuard::register(&mesh, two.clone(), provider);
assert!(blob_fetch_cancel(&state, &one).unwrap().cancelled);
assert!(a.token.is_cancelled(), "the named hash is cancelled");
assert!(
!b.token.is_cancelled(),
"an unrelated in-flight fetch must be untouched"
);
assert!(
blob_fetch_cancel(&state, &two).unwrap().cancelled,
"and it is still cancellable on its own"
);
let three = hex_of(b"three");
let c = FetchGuard::register(&mesh, three, provider2);
assert!(
!blob_fetch_cancel(&state, &hex_of(b"absent"))
.unwrap()
.cancelled
);
assert!(
!c.token.is_cancelled(),
"a cancel for an absent hash must trip nothing"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_fetches_of_one_hash_share_a_token_and_the_last_one_deregisters() {
let (_dir, mesh, state) = blob_mesh().await;
let provider = test_provider(&mesh).await;
let hash = hex_of(b"shared");
let a = FetchGuard::register(&mesh, hash.clone(), provider.clone());
let b = FetchGuard::register(&mesh, hash.clone(), provider);
assert!(
!a.token.is_cancelled() && !b.token.is_cancelled(),
"a fresh registration starts live"
);
drop(a);
let r = blob_fetch_cancel(&state, &hash).expect("cancel answers");
assert!(r.cancelled, "the surviving fetch is still cancellable");
assert!(b.token.is_cancelled(), "the shared token was tripped");
drop(b);
let r = blob_fetch_cancel(&state, &hash).expect("cancel answers");
assert!(
!r.cancelled,
"the last fetch to finish deregisters the hash"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_retry_started_before_the_cancelled_fetch_unwinds_gets_a_live_token() {
let (_dir, mesh, state) = blob_mesh().await;
let provider = test_provider(&mesh).await;
let hash = hex_of(b"retry-me");
let cancelled = FetchGuard::register(&mesh, hash.clone(), provider.clone());
assert!(blob_fetch_cancel(&state, &hash).unwrap().cancelled);
assert!(cancelled.token.is_cancelled());
let retry = FetchGuard::register(&mesh, hash.clone(), provider);
assert!(
!retry.token.is_cancelled(),
"a retry must not inherit a tripped token"
);
drop(cancelled);
assert!(
blob_fetch_cancel(&state, &hash).unwrap().cancelled,
"the retry is still registered and cancellable"
);
assert!(retry.token.is_cancelled());
}
#[tokio::test(flavor = "multi_thread")]
async fn a_stopped_fetch_emits_a_terminal_aborted_frame() {
let (_dir, mesh, _state) = blob_mesh().await;
let provider = test_provider(&mesh).await;
let mut rx = mesh.blob_bcast_for_test().subscribe();
let hash = hex_of(b"stopped");
let guard = FetchGuard::register(&mesh, hash.clone(), provider.clone());
drop(guard);
let frame = rx.try_recv().expect("a terminal frame was broadcast");
assert_eq!(frame.hash, hash);
assert_eq!(frame.state, mcpmesh_local_api::BlobTransferState::Aborted);
assert_eq!(frame.direction, mcpmesh_local_api::BlobDirection::Fetch);
let mut finished = FetchGuard::register(&mesh, hash.clone(), provider);
finished.finished = true;
drop(finished);
assert!(
rx.try_recv().is_err(),
"a finished fetch must not also report Aborted"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_cancelled_blob_fetch_answers_cancelled_rather_than_hanging() {
let (dir, mesh, state) = blob_mesh().await;
let nowhere = iroh::SecretKey::from_bytes(&[9u8; 32]).public();
let hash = iroh_blobs::Hash::new(b"never-fetched");
let addr = iroh::EndpointAddr::from_parts(
nowhere,
[iroh::TransportAddr::Ip(std::net::SocketAddr::from((
[203, 0, 113, 1],
44444,
)))],
);
let ticket = iroh_blobs::ticket::BlobTicket::new(addr, hash, iroh_blobs::BlobFormat::Raw)
.to_string();
let hash_hex = hash.to_hex().to_string();
let dest = dir.path().join("out.bin");
let fetch_state = state.clone();
let fetching = tokio::spawn(async move {
blob_fetch(&fetch_state, ticket, dest.to_string_lossy().into_owned()).await
});
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20);
loop {
if mesh
.fetches
.lock()
.expect("fetches lock not poisoned")
.contains_key(&hash_hex)
{
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"the fetch never registered itself"
);
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
}
let r = blob_fetch_cancel(&state, &hash_hex).expect("cancel answers");
assert!(r.cancelled, "the in-flight fetch was found and tripped");
let outcome = tokio::time::timeout(std::time::Duration::from_secs(30), fetching)
.await
.expect("a cancelled fetch answers promptly rather than running to completion")
.expect("fetch task not panicked");
let err = outcome.expect_err("a cancelled fetch is an Err, not a silent success");
assert!(
err.downcast_ref::<Cancelled>().is_some(),
"it must be the CANCELLED error, so `respond` codes it ERR_CANCELLED rather than \
-32000: {err:#}"
);
assert!(!blob_fetch_cancel(&state, &hash_hex).unwrap().cancelled);
}
#[tokio::test]
async fn blob_ops_error_without_a_mesh() {
let st = DaemonState::new("test");
assert!(blob_list(&st, Default::default()).await.is_err());
assert!(
blob_publish(&st, "scope".into(), "/tmp/x".into())
.await
.is_err()
);
assert!(blob_grant(&st, "scope".into(), "bob".into()).await.is_err());
assert!(
blob_fetch(&st, "ticket".into(), "/tmp/dst".into())
.await
.is_err()
);
}
fn rename_params(user_id: Option<&str>, to: &str) -> PeerRenameParams {
PeerRenameParams {
user_id: user_id.map(str::to_string),
nickname: None,
to: to.into(),
}
}
#[tokio::test]
async fn rename_peer_renames_all_devices_and_leaves_grants_untouched() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = [\"b64u:BOB\"]\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path.clone()).await;
mesh.store
.add(rename_entry(1, "bob-old", Some("b64u:BOB")))
.unwrap();
mesh.store
.add(rename_entry(2, "bob-old", Some("b64u:BOB")))
.unwrap();
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let config_before = std::fs::read_to_string(&config_path).unwrap();
rename_peer(&state, rename_params(Some("b64u:BOB"), "Bobby"))
.await
.unwrap();
let names: Vec<String> = mesh
.store
.list()
.unwrap()
.into_iter()
.map(|e| e.nickname)
.collect();
assert!(
names.iter().all(|n| n == "Bobby"),
"all devices renamed, got {names:?}"
);
let config_after = std::fs::read_to_string(&config_path).unwrap();
assert_eq!(
config_before, config_after,
"rename must not rewrite the config"
);
let doc: toml::Table = toml::from_str(&config_after).unwrap();
let allow = doc["services"]["kb"]["allow"].as_array().unwrap();
assert_eq!(allow.len(), 1);
assert_eq!(allow[0].as_str(), Some("b64u:BOB"));
}
#[tokio::test]
async fn rename_peer_guards_bad_requests_and_collisions() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config.toml");
std::fs::write(
&config_path,
"[services.kb]\nsocket = \"/run/kb.sock\"\nallow = []\n",
)
.unwrap();
let mesh = hermetic_mesh(config_path).await;
mesh.store
.add(rename_entry(1, "alice", Some("b64u:ALICE")))
.unwrap();
mesh.store
.add(rename_entry(2, "bob", Some("b64u:BOB")))
.unwrap();
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
assert!(
rename_peer(&state, rename_params(Some("b64u:ALICE"), " "))
.await
.is_err()
);
assert!(rename_peer(&state, rename_params(None, "X")).await.is_err());
assert!(
rename_peer(&state, rename_params(Some("b64u:NOBODY"), "X"))
.await
.is_err()
);
assert!(
rename_peer(&state, rename_params(Some("b64u:ALICE"), "bob"))
.await
.is_err()
);
let names: std::collections::BTreeSet<String> = mesh
.store
.list()
.unwrap()
.into_iter()
.map(|e| e.nickname)
.collect();
assert!(
names.contains("alice") && names.contains("bob"),
"no rename should have occurred: {names:?}"
);
}
fn rename_entry(id: u8, nickname: &str, user_id: Option<&str>) -> PeerEntry {
PeerEntry {
endpoint_id: [id; 32],
nickname: nickname.into(),
services: Vec::new(),
paired_at: None,
user_id: user_id.map(str::to_string),
last_addr: None,
}
}
#[test]
fn rename_plan_groups_by_user_id_and_guards_collisions() {
let dir = tempfile::tempdir().unwrap();
let store = PeerStore::open(&dir.path().join("s.redb")).unwrap();
store
.add(rename_entry(1, "bob-phone", Some("b64u:BOB")))
.unwrap();
store
.add(rename_entry(2, "bob-laptop", Some("b64u:BOB")))
.unwrap();
store
.add(rename_entry(3, "carol", Some("b64u:CAROL")))
.unwrap();
let plan = rename_plan(&store, Some("b64u:BOB"), None, "Bobby")
.unwrap()
.unwrap();
assert_eq!(plan.targets.len(), 2);
assert!(rename_plan(&store, Some("b64u:BOB"), None, "carol").is_err());
store.add(rename_entry(4, "dave", None)).unwrap();
assert_eq!(
rename_plan(&store, None, Some("dave"), "Dave")
.unwrap()
.unwrap()
.targets
.len(),
1
);
assert!(
rename_plan(&store, Some("b64u:CAROL"), None, "carol")
.unwrap()
.is_none()
);
assert!(rename_plan(&store, Some("b64u:NOBODY"), None, "x").is_err());
}
}