use std::sync::Arc;
use mcpmesh_local_api::{BackendKind, PeerInfo, PresencePeer, RosterStatus, ServiceInfo};
use mcpmesh_trust::roster::validate::RosterState;
use crate::allowlist::PeerStore;
use crate::config::Config;
use crate::pairing;
use crate::util::epoch_now_i64;
use super::{MeshState, dial};
pub(crate) fn roster_status(mesh: &Arc<MeshState>, cfg: Option<&Config>) -> Option<RosterStatus> {
let org_root_fingerprint = cfg
.and_then(|c| c.identity.org_root_pk.as_deref())
.and_then(|s| crate::roster::parse_org_root_pk(s).ok())
.map(|vk| pairing::sas::fingerprint_words(&vk.to_bytes()))
.unwrap_or_default();
match mesh.roster.view() {
Some(view) => {
let state = match mesh
.roster
.effective_state(epoch_now_i64())
.unwrap_or(RosterState::Approved)
{
RosterState::Approved => "approved",
RosterState::DegradedGrace => "degraded",
RosterState::DegradedStopped => "stopped",
};
Some(RosterStatus {
org_id: view.org_id().to_string(),
serial: view.serial(),
state: state.to_string(),
org_root_fingerprint,
groups: view.groups().to_vec(),
})
}
None => {
let cfg = cfg?;
cfg.identity.org_root_pk.as_deref()?;
Some(RosterStatus {
org_id: cfg.identity.org_id.clone().unwrap_or_default(),
serial: 0,
state: "pending".to_string(),
org_root_fingerprint,
groups: Vec::new(),
})
}
}
}
pub(crate) fn presence_peers(mesh: &Arc<MeshState>) -> Vec<PresencePeer> {
let Some(view) = mesh.roster.view() else {
return Vec::new();
};
let now = epoch_now_i64();
let active: std::collections::HashMap<[u8; 32], crate::roster::presence::PresenceEntry> =
mesh.presence_table.active(now).into_iter().collect();
let self_eid = *mesh.endpoint.id().as_bytes();
let self_meta = mesh.app_metadata();
let mut peers: Vec<PresencePeer> = view
.devices()
.map(|(eid, d)| PresencePeer {
user_id: d.user_id.clone(),
display_name: d.display_name.clone(),
groups: d.groups.clone(),
device_label: d.label.clone(),
role: d.role.clone(),
online: active.contains_key(eid),
meta: if *eid == self_eid {
self_meta.clone()
} else {
active.get(eid).map(|e| e.meta.clone()).unwrap_or_default()
},
})
.collect();
peers.sort_by(|a, b| {
a.user_id
.cmp(&b.user_id)
.then_with(|| dial::dial_role_rank(&a.role).cmp(&dial::dial_role_rank(&b.role)))
.then_with(|| a.device_label.cmp(&b.device_label))
});
peers
}
fn display_principal(principal: &str, peers: &[crate::allowlist::PeerEntry]) -> String {
if principal.starts_with("eid:") {
return peers
.iter()
.find(|p| mcpmesh_net::EndpointId::from_bytes(p.endpoint_id).principal() == principal)
.map(|p| p.nickname.clone())
.unwrap_or_else(|| "unpaired-device".to_owned());
}
if principal.starts_with("b64u:") {
return peers
.iter()
.find(|p| p.user_id.as_deref() == Some(principal))
.map(|p| p.nickname.clone())
.unwrap_or_else(|| "unpaired-peer".to_owned());
}
principal.to_owned()
}
pub(crate) fn service_infos(
live: &mcpmesh_net::Services,
peers: &[crate::allowlist::PeerEntry],
) -> Vec<ServiceInfo> {
let mut out: Vec<ServiceInfo> = live
.iter()
.map(|(name, entry)| ServiceInfo {
name: name.clone(),
allow: entry.allow.clone(),
allow_display: entry
.allow
.iter()
.map(|p| display_principal(p, peers))
.collect(),
backend: match entry.kind {
mcpmesh_net::ServiceKind::Socket => BackendKind::Socket,
_ => BackendKind::Run,
},
ephemeral: entry.ephemeral,
})
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub(crate) fn known_service_names(
cfg: &Config,
ephemeral: &std::collections::HashMap<String, crate::daemon::EphemeralService>,
) -> Vec<String> {
let mut out: Vec<String> = cfg
.services
.iter()
.filter(|(_, svc)| svc.backend_result().is_ok())
.map(|(name, _)| name.clone())
.collect();
for name in ephemeral.keys() {
if !out.contains(name) {
out.push(name.clone());
}
}
out.sort();
out
}
pub(crate) fn peer_infos(store: &PeerStore) -> Vec<PeerInfo> {
store
.list()
.unwrap_or_default()
.into_iter()
.map(|e| PeerInfo {
name: e.nickname,
services: e.services,
user_id: e.user_id,
principal: Some(mcpmesh_net::EndpointId::from_bytes(e.endpoint_id).principal()),
})
.collect()
}
pub(crate) fn roster_members(mesh: &Arc<MeshState>) -> mcpmesh_local_api::RosterMembersResult {
let Some(view) = mesh.roster.view() else {
return mcpmesh_local_api::RosterMembersResult::default();
};
let now = epoch_now_i64();
let active: std::collections::HashMap<[u8; 32], crate::roster::presence::PresenceEntry> =
mesh.presence_table.active(now).into_iter().collect();
let mut by_user: std::collections::BTreeMap<String, mcpmesh_local_api::RosterMember> = view
.users()
.iter()
.map(|u| {
(
u.user_id.clone(),
mcpmesh_local_api::RosterMember {
user_id: u.user_id.clone(),
display_name: u.display_name.clone(),
groups: u.groups.clone(),
devices: Vec::new(),
},
)
})
.collect();
for (eid, d) in view.devices() {
let member =
by_user
.entry(d.user_id.clone())
.or_insert_with(|| mcpmesh_local_api::RosterMember {
user_id: d.user_id.clone(),
display_name: d.display_name.clone(),
groups: d.groups.clone(),
devices: Vec::new(),
});
member.devices.push(mcpmesh_local_api::RosterMemberDevice {
label: d.label.clone(),
role: d.role.clone(),
principal: mcpmesh_net::EndpointId::from_bytes(*eid).principal(),
online: active.contains_key(eid),
});
}
for m in by_user.values_mut() {
m.devices.sort_by(|a, b| {
dial::dial_role_rank(&a.role)
.cmp(&dial::dial_role_rank(&b.role))
.then_with(|| a.label.cmp(&b.label))
});
}
mcpmesh_local_api::RosterMembersResult {
groups: view.groups().to_vec(),
users: by_user.into_values().collect(),
}
}
#[cfg(test)]
mod tests {
use crate::allowlist::PeerEntry;
use crate::daemon::testutil::hermetic_mesh;
#[tokio::test(flavor = "multi_thread")]
async fn status_reads_the_live_config_and_store() {
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());
crate::daemon::grant_service_access(&mesh, "alice", "alice", &["kb".to_string()])
.await
.unwrap();
mesh.store
.add(PeerEntry {
endpoint_id: [9u8; 32],
nickname: "alice".into(),
services: Vec::new(),
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
let status = crate::control::status_result(&state).unwrap();
let kb = status
.services
.iter()
.find(|s| s.name == "kb")
.expect("kb service in status");
assert!(
kb.allow.contains(&"alice".to_string()),
"status must show the live grant, got allow={:?}",
kb.allow
);
let alice = status
.peers
.iter()
.find(|p| p.name == "alice")
.expect("status must show the live peer");
assert_eq!(
alice.principal.as_deref(),
Some(
mcpmesh_net::EndpointId::from_bytes([9u8; 32])
.principal()
.as_str()
),
"peer principal must be the eid: of its endpoint id"
);
}
#[tokio::test]
async fn status_surfaces_self_and_peer_user_ids() {
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;
mesh.set_self_binding(Some(crate::pairing::rendezvous::SelfBinding {
user_pk: "b64u:selfpk".into(),
sig: "b64u:selfsig".into(),
}));
mesh.store
.add(PeerEntry {
endpoint_id: [1u8; 32],
nickname: "alice".into(),
services: Vec::new(),
paired_at: Some("1".into()),
user_id: Some("b64u:alicepk".into()),
last_addr: None,
})
.unwrap();
mesh.store
.add(PeerEntry {
endpoint_id: [2u8; 32],
nickname: "legacy".into(),
services: Vec::new(),
paired_at: None,
user_id: None,
last_addr: None,
})
.unwrap();
let state = crate::control::DaemonState::with_mesh("test", mesh.clone());
let status = crate::control::status_result(&state).unwrap();
assert_eq!(
status.self_user_id.as_deref(),
Some("b64u:selfpk"),
"status must surface this daemon's own self-sovereign user_id"
);
let alice = status
.peers
.iter()
.find(|p| p.name == "alice")
.expect("alice in status");
assert_eq!(
alice.user_id.as_deref(),
Some("b64u:alicepk"),
"a paired peer's PROVEN user_id must be surfaced in status"
);
let legacy = status
.peers
.iter()
.find(|p| p.name == "legacy")
.expect("legacy in status");
assert!(
legacy.user_id.is_none(),
"a nickname-only peer stays user_id: None"
);
}
#[tokio::test]
async fn recent_pairings_ring_is_bounded_newest_first_and_surfaced_by_status() {
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;
for i in 0..10u64 {
mesh.record_pairing(format!("peer{i}"), format!("code-{i}"), i);
}
let recent = mesh.recent_pairings();
assert_eq!(recent.len(), 8, "the ring is capped at 8");
assert_eq!(recent[0].peer_nickname, "peer9", "newest first");
assert_eq!(
recent[7].peer_nickname, "peer2",
"the two oldest were dropped"
);
let state = crate::control::DaemonState::with_mesh("test", mesh);
let status = crate::control::status_result(&state).unwrap();
assert_eq!(status.recent_pairings.len(), 8);
assert_eq!(status.recent_pairings[0].sas_code, "code-9");
assert_eq!(status.recent_pairings[0].paired_at_epoch, 9);
}
}
#[cfg(test)]
mod roster_members_tests {
use super::roster_members;
use crate::daemon::testutil::hermetic_mesh;
use mcpmesh_trust::roster::validate::load_installed;
use mcpmesh_trust::roster::{Roster, RosterDevice, RosterUser, encode_b64u, sign::mint_signed};
fn install_sample(mesh: &std::sync::Arc<crate::daemon::MeshState>) {
let root = mcpmesh_trust::ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
let r = mint_signed(
&root,
Roster {
format: "mcpmesh-roster/1".into(),
org_id: "acme".into(),
serial: 5,
issued_at: "2000-01-01T00:00:00Z".into(),
expires_at: "2999-01-01T00:00:00Z".into(),
groups: vec!["ops".into(), "eng".into()],
users: vec![
RosterUser {
user_id: "alice".into(),
display_name: "Alice Example".into(),
user_pk: encode_b64u(&[1u8; 32]),
groups: vec!["eng".into()],
devices: vec![
RosterDevice {
endpoint_id: encode_b64u(&[0xA3; 32]),
label: "aaa-tablet".into(),
role: "mirror".into(),
},
RosterDevice {
endpoint_id: encode_b64u(&[0xA1; 32]),
label: "laptop".into(),
role: "primary".into(),
},
RosterDevice {
endpoint_id: encode_b64u(&[0xA2; 32]),
label: "old-phone".into(),
role: "mirror".into(),
},
],
},
RosterUser {
user_id: "bob".into(),
display_name: "Bob Example".into(),
user_pk: encode_b64u(&[2u8; 32]),
groups: vec!["ops".into(), "eng".into()],
devices: vec![RosterDevice {
endpoint_id: encode_b64u(&[0xB1; 32]),
label: "desktop".into(),
role: "primary".into(),
}],
},
RosterUser {
user_id: "zoe".into(),
display_name: "Aaron Deviceless".into(),
user_pk: encode_b64u(&[3u8; 32]),
groups: vec!["ops".into()],
devices: vec![RosterDevice {
endpoint_id: encode_b64u(&[0xC1; 32]),
label: "retired".into(),
role: "primary".into(),
}],
},
],
revoked_endpoints: vec![encode_b64u(&[0xA2; 32]), encode_b64u(&[0xC1; 32])],
sig: String::new(),
},
);
mesh.roster
.install(load_installed(&r, &root.verifying_key()).unwrap());
}
#[tokio::test(flavor = "multi_thread")]
async fn the_membership_read_carries_names_groups_and_offline_people() {
let dir = tempfile::tempdir().unwrap();
let cfg = dir.path().join("config.toml");
std::fs::write(&cfg, "").unwrap();
let mesh = hermetic_mesh(cfg).await;
install_sample(&mesh);
let got = roster_members(&mesh);
assert_eq!(
got.groups,
vec!["ops".to_string(), "eng".to_string()],
"the DECLARED group namespace must be reported in document order — it is the set an \
`allow` entry may name, and a UI has nothing else to offer from"
);
assert_eq!(
got.users
.iter()
.map(|u| u.user_id.as_str())
.collect::<Vec<_>>(),
vec!["alice", "bob", "zoe"],
"every person in the roster must appear, ordered by the stable USER_ID. Three things \
ride on this list. bob has no live device, which status.presence cannot express. \
zoe's ONLY device is revoked — the ordinary result of `org_revoke <user>/<device>` on \
someone's last machine, which leaves their user entry in the signed roster; deriving \
members from the device map dropped zoe entirely, reporting 'removed from the org' \
for someone who was not. And zoe sorts LAST by user_id but FIRST by display_name, so \
this pins the ordering key against a rename reshuffling the list"
);
let zoe = &got.users[2];
assert_eq!(zoe.display_name, "Aaron Deviceless");
assert!(
zoe.devices.is_empty(),
"…and they appear with NO devices, which is the honest report: the entry exists, the \
hardware is revoked"
);
assert_eq!(
zoe.groups,
vec!["ops".to_string()],
"group membership survives the revocation of every device"
);
let alice = &got.users[0];
assert_eq!(
alice.display_name, "Alice Example",
"the human name must cross the seam"
);
assert_eq!(alice.groups, vec!["eng".to_string()]);
assert_eq!(
alice
.devices
.iter()
.map(|d| d.label.as_str())
.collect::<Vec<_>>(),
vec!["laptop", "aaa-tablet"],
"TWO properties. (1) The order is primary-before-mirror — NOT document order (the \
tablet is listed first) and NOT label order (`aaa-tablet` sorts first \
alphabetically), so deleting the sort or sorting by label alone both fail here. \
(2) a REVOKED device must be absent, not merely offline — the member list must agree \
with what the gate would authorize"
);
assert_eq!(
alice.devices[0].principal,
mcpmesh_net::EndpointId::from_bytes([0xA1; 32]).principal(),
"each device carries the eid: handle a per-device allow entry names — roster mode has \
no nicknames, so without it one device cannot be addressed"
);
assert!(
!alice.devices[0].online,
"nothing has sent a heartbeat in this hermetic mesh"
);
let bob = &got.users[1];
assert_eq!(bob.display_name, "Bob Example");
assert_eq!(
bob.groups,
vec!["ops".to_string(), "eng".to_string()],
"multi-group membership must survive verbatim"
);
assert_eq!(bob.devices.len(), 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_daemon_with_no_roster_reports_empty_membership() {
let dir = tempfile::tempdir().unwrap();
let cfg = dir.path().join("config.toml");
std::fs::write(&cfg, "").unwrap();
let mesh = hermetic_mesh(cfg).await;
let got = roster_members(&mesh);
assert!(got.users.is_empty() && got.groups.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn presence_rows_carry_the_display_name_and_groups_too() {
let dir = tempfile::tempdir().unwrap();
let cfg = dir.path().join("config.toml");
std::fs::write(&cfg, "").unwrap();
let mesh = hermetic_mesh(cfg).await;
install_sample(&mesh);
let peers = super::presence_peers(&mesh);
let alice = peers
.iter()
.find(|p| p.user_id == "alice")
.expect("alice's active device is projected");
assert_eq!(alice.display_name, "Alice Example");
assert_eq!(alice.groups, vec!["eng".to_string()]);
assert_eq!(
alice.device_label, "laptop",
"the pre-existing fields must survive"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn roster_status_carries_the_declared_groups() {
let dir = tempfile::tempdir().unwrap();
let cfg = dir.path().join("config.toml");
std::fs::write(&cfg, "").unwrap();
let mesh = hermetic_mesh(cfg).await;
install_sample(&mesh);
let st = super::roster_status(&mesh, None).expect("a roster is installed");
assert_eq!(st.groups, vec!["ops".to_string(), "eng".to_string()]);
assert_eq!(st.serial, 5, "the pre-existing fields must survive");
}
}