#![cfg(unix)]
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use assert_cmd::cargo::cargo_bin;
use ed25519_dalek::VerifyingKey;
use mcpmesh::allowlist::{AllowlistGate, PeerStore};
use mcpmesh::client::connect_control;
use mcpmesh::daemon::{
MeshState, build_services, install_roster_view_and_sever, spawn_accept_loop,
};
use mcpmesh::pairing::LiveInvites;
use mcpmesh::roster::RosterStore;
use mcpmesh::roster::enroll::{JoinCode, OrgInviteCode};
use mcpmesh::roster::gate::{ComposedGate, RosterGate};
use mcpmesh_net::registry::ConnRegistry;
use mcpmesh_net::{ALPN_MCP, ALPN_PAIR, TrustGate, connect};
use mcpmesh_trust::DeviceKey;
use mcpmesh_trust::roster::{Roster, decode_endpoint_id, encode_b64u, sign};
use serde_json::json;
use tokio::time::timeout;
const STUB: &str = env!("CARGO_BIN_EXE_echo_mcp_stub");
fn launch_in(dir: &Path) -> (PathBuf, PathBuf, PathBuf, Vec<(OsString, OsString)>) {
let runtime = dir.join("runtime");
let config = dir.join("config");
let data = dir.join("data");
let config_mcpmesh = config.join("mcpmesh");
std::fs::create_dir_all(&config_mcpmesh).unwrap();
std::fs::write(
config_mcpmesh.join("config.toml"),
"[network]\nrelay_mode = \"disabled\"\n",
)
.unwrap();
let socket = runtime.join("mcpmesh").join("mcpmesh.sock");
let env = vec![
(OsString::from("XDG_RUNTIME_DIR"), runtime.into_os_string()),
(OsString::from("XDG_CONFIG_HOME"), config.into_os_string()),
(OsString::from("XDG_DATA_HOME"), data.into_os_string()),
];
(cargo_bin("mcpmesh"), socket, config_mcpmesh, env)
}
fn run_cmd(exe: &Path, env: &[(OsString, OsString)], args: &[&str]) -> std::process::Output {
let mut cmd = std::process::Command::new(exe);
cmd.args(args);
for (k, v) in env {
cmd.env(k, v);
}
cmd.output().expect("run mcpmesh subcommand")
}
async fn shutdown_daemon(socket: &Path) {
if let Ok(mut client) = connect_control(socket).await {
let _ = client.request_value(&json!({ "method": "shutdown" })).await;
}
let deadline = Instant::now() + Duration::from_secs(5);
while connect_control(socket).await.is_ok() {
assert!(
Instant::now() < deadline,
"daemon still accepting connections after shutdown"
);
tokio::time::sleep(Duration::from_millis(20)).await;
}
}
fn fingerprint_after(label: &str, out: &str) -> String {
out.lines()
.find(|l| l.contains(label))
.and_then(|l| l.split_whitespace().find(|w| w.contains('-')))
.unwrap_or("")
.to_string()
}
fn org_invite_from(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout)
.lines()
.find_map(|l| l.split_whitespace().find(|w| w.starts_with("mcpmesh-org:")))
.expect("an org invite code")
.to_string()
}
fn join_code_from(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout)
.lines()
.find_map(|l| {
l.split_whitespace()
.find(|w| w.starts_with("mcpmesh-join:"))
})
.expect("a join code")
.to_string()
}
fn now_epoch_i64() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn read_roster(config_dir: &Path) -> Roster {
serde_json::from_slice(&std::fs::read(config_dir.join("roster.json")).unwrap()).unwrap()
}
async fn endpoint_from_secret(secret: iroh::SecretKey, alpns: Vec<Vec<u8>>) -> iroh::Endpoint {
iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(iroh::RelayMode::Disabled)
.secret_key(secret)
.alpns(alpns)
.bind()
.await
.expect("bind endpoint from a known device key")
}
async fn dual_alpn_endpoint() -> iroh::Endpoint {
iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.relay_mode(iroh::RelayMode::Disabled)
.alpns(vec![ALPN_MCP.to_vec(), ALPN_PAIR.to_vec()])
.bind()
.await
.expect("bind dual-ALPN endpoint")
}
fn initialize_frame(service: &str) -> serde_json::Value {
json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"_meta": {"mcpmesh/service": service},
"capabilities": {}, "clientInfo": {"name": "tester", "version": "0"}
}
})
}
fn tools_call_frame(text: &str) -> serde_json::Value {
json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "echo", "arguments": {"text": text}}
})
}
async fn wait_for_len(registry: &ConnRegistry, target: usize) {
for _ in 0..50 {
if registry.len() == target {
return;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
panic!(
"conn registry len did not settle to {target} (still {})",
registry.len()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn full_enrollment_via_porcelain_admits_a_group_service_and_revocation_cuts_it() {
timeout(Duration::from_secs(120), async {
let opdir = tempfile::tempdir().unwrap();
let (opexe, opsock, opcfg, openv) = launch_in(opdir.path());
let jdir = tempfile::tempdir().unwrap();
let (jexe, jsock, jcfg, jenv) = launch_in(jdir.path());
let create = run_cmd(&opexe, &openv, &["org", "create", "acme"]);
assert!(
create.status.success(),
"org create failed: {}",
String::from_utf8_lossy(&create.stderr)
);
let invite = org_invite_from(&create);
let fp_op = fingerprint_after(
"Org root fingerprint:",
&String::from_utf8_lossy(&create.stdout),
);
assert!(!fp_op.is_empty(), "operator prints an org-root fingerprint");
let org_root_pub: VerifyingKey = {
let ic = OrgInviteCode::decode(&invite).expect("decode org invite");
let bytes = decode_endpoint_id(&ic.org_root_pk).expect("decode org_root_pk");
VerifyingKey::from_bytes(&bytes).expect("org root pk is a valid key")
};
let join = run_cmd(
&jexe,
&jenv,
&[
"join", &invite, "--name", "Alice", "--user-id", "alice", "--label", "laptop",
],
);
assert!(
join.status.success(),
"join failed: {}",
String::from_utf8_lossy(&join.stderr)
);
let join_code = join_code_from(&join);
assert_eq!(
fingerprint_after("Org root fingerprint:", &String::from_utf8_lossy(&join.stdout)),
fp_op,
"the joiner's org-root fingerprint must match the operator's (the §4.4 ceremony)"
);
let ukey = jcfg.join("user.key");
assert!(ukey.exists(), "the user key is minted");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(&ukey).unwrap().permissions().mode() & 0o777,
0o600,
"the user key must be 0600"
);
}
let jcfg_txt = std::fs::read_to_string(jcfg.join("config.toml")).unwrap();
assert!(jcfg_txt.contains("org_id = \"acme\""), "config pins org_id");
assert!(jcfg_txt.contains("org_root_pk"), "config pins org_root_pk");
assert!(jcfg_txt.contains("user_id = \"alice\""), "config pins user_id");
assert!(jcfg_txt.contains("user_key"), "config pins the user key path");
let jc = JoinCode::decode(&join_code).expect("decode join code");
let joiner_device_b64u = jc.device_endpoint_id.clone();
let approve = run_cmd(
&opexe,
&openv,
&["org", "approve", &join_code, "--groups", "team-eng"],
);
assert!(
approve.status.success(),
"org approve failed: {}",
String::from_utf8_lossy(&approve.stderr)
);
let roster2 = read_roster(&opcfg);
assert_eq!(roster2.serial, 2, "approve bumps the roster to serial 2");
let alice = roster2
.users
.iter()
.find(|u| u.user_id == "alice")
.expect("alice enrolled by the porcelain");
assert!(
alice.groups.contains(&"team-eng".to_string()),
"alice is in the team-eng group: {:?}",
alice.groups
);
assert_eq!(alice.devices.len(), 1, "alice has the one enrolled device");
assert_eq!(
alice.devices[0].endpoint_id, joiner_device_b64u,
"the roster device record == the join code's device endpoint"
);
sign::verify(&roster2, &org_root_pub)
.expect("the porcelain-produced serial-2 roster verifies against the org root");
let serial2_file = opdir.path().join("roster-serial2.json");
std::fs::copy(opcfg.join("roster.json"), &serial2_file).unwrap();
let revoke = run_cmd(&opexe, &openv, &["org", "revoke", "alice/laptop"]);
assert!(
revoke.status.success(),
"org revoke failed: {}",
String::from_utf8_lossy(&revoke.stderr)
);
let roster3 = read_roster(&opcfg);
assert_eq!(roster3.serial, 3, "revoke bumps the roster to serial 3");
assert_eq!(
roster3.revoked_endpoints,
vec![joiner_device_b64u.clone()],
"the joiner's device is the sole revoked endpoint"
);
assert!(
roster3
.users
.iter()
.find(|u| u.user_id == "alice")
.map(|u| u.devices.is_empty())
.unwrap_or(true),
"alice's device list is emptied by the revoke"
);
sign::verify(&roster3, &org_root_pub)
.expect("the porcelain-produced serial-3 roster verifies against the org root");
let serial3_file = opdir.path().join("roster-serial3.json");
std::fs::copy(opcfg.join("roster.json"), &serial3_file).unwrap();
shutdown_daemon(&opsock).await;
shutdown_daemon(&jsock).await;
let (joiner_dk, _) =
DeviceKey::load_or_generate(&jcfg.join("device.key")).expect("load joiner device key");
assert_eq!(
encode_b64u(&joiner_dk.public_bytes()),
joiner_device_b64u,
"the joiner's device.key pubkey == the join code's device endpoint == the roster record"
);
let joiner_secret = iroh::SecretKey::from_bytes(&joiner_dk.secret_bytes());
let joiner_client = endpoint_from_secret(joiner_secret, vec![ALPN_MCP.to_vec()]).await;
assert_eq!(
encode_b64u(joiner_client.id().as_bytes()),
joiner_device_b64u,
"the in-process joiner endpoint id == the roster device record (the identity flows through)"
);
let bdir = tempfile::tempdir().unwrap();
let cfg = mcpmesh::config::Config::from_toml_str(&format!(
"[services.echo]\nrun = ['{STUB}']\nallow = [\"team-eng\"]\n"
))
.expect("parse operator config");
let store = Arc::new(PeerStore::open(&bdir.path().join("state.redb")).unwrap());
let pairs = Arc::new(AllowlistGate::new(store.clone()));
let roster = Arc::new(RosterGate::empty());
let gate: Arc<dyn TrustGate> = Arc::new(ComposedGate::new(roster.clone(), pairs));
let conn_registry = Arc::new(ConnRegistry::new());
let server = dual_alpn_endpoint().await;
let addr = server.addr();
let mesh = MeshState::new(
server,
gate,
store,
Arc::new(LiveInvites::new()),
"operator".into(),
bdir.path().join("config.toml"),
roster.clone(),
conn_registry.clone(),
None,
None,
None,
None,
);
let _task = spawn_accept_loop(mesh.clone(), Arc::new(build_services(&cfg)));
let rstore = RosterStore::new(bdir.path().join("installed-roster.json"));
let now = now_epoch_i64();
let view2 = rstore
.install_from_file(&serial2_file, &org_root_pub, now)
.expect("install the porcelain-produced serial-2 roster");
assert_eq!(
install_roster_view_and_sever(&mesh, view2),
0,
"installing the first roster severs nothing (no live sessions yet)"
);
let mut joiner_t = connect(&joiner_client, addr.clone(), "echo")
.await
.expect("the rostered joiner dials echo").0;
joiner_t.send_value(initialize_frame("echo")).await.unwrap();
let init = joiner_t.recv_value().await.unwrap().unwrap();
assert_eq!(
init["result"]["serverInfo"]["name"], "echo-stub",
"group-based allow must admit the porcelain-enrolled joiner to a live session: {init}"
);
joiner_t.send_value(tools_call_frame("group-admit")).await.unwrap();
let reply = timeout(Duration::from_secs(5), joiner_t.recv_value())
.await
.expect("echo reply must arrive promptly")
.expect("transport ok")
.expect("reply frame");
assert_eq!(reply["result"]["content"][0]["text"], "group-admit");
assert_eq!(
reply["result"]["peer_user"], "alice",
"MCPMESH_PEER_USER = the roster user_id: {reply}"
);
let groups = reply["result"]["peer_groups"].as_str().unwrap_or_default();
assert!(
groups.split(',').any(|g| g == "team-eng"),
"MCPMESH_PEER_GROUPS must contain the admitting group `team-eng`, got {groups:?}: {reply}"
);
wait_for_len(&conn_registry, 1).await;
let view3 = rstore
.install_from_file(&serial3_file, &org_root_pub, now)
.expect("install the porcelain-produced serial-3 (revoking) roster");
let severed = install_roster_view_and_sever(&mesh, view3);
assert_eq!(
severed, 1,
"exactly the revoked joiner's live session is severed"
);
let after = timeout(Duration::from_secs(5), joiner_t.recv_value())
.await
.expect("the severed session must close promptly, not hang");
assert!(
!matches!(after, Ok(Some(_))),
"the revoked joiner's live session must be severed, got: {after:?}"
);
match connect(&joiner_client, addr.clone(), "echo").await.map(|(t, _)| t) {
Err(_) => {} Ok(mut t) => {
let _ = t.send_value(initialize_frame("echo")).await;
let res = timeout(Duration::from_secs(5), t.recv_value())
.await
.expect("a revoked re-dial must close promptly, not hang");
assert!(
!matches!(res, Ok(Some(_))),
"a revoked endpoint must be refused pre-MCP, got: {res:?}"
);
}
}
wait_for_len(&conn_registry, 0).await;
})
.await
.expect("enrollment E2E timed out");
}