1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
//! Task 7: the `run` (spawn) backend — one child MCP server per session, stdio
//! pumped to/from the QUIC-framed transport, resolved identity injected as env
//! vars, child killed on session close, concurrent spawns bounded per service.
//!
//! Hermetic: the child is `echo_mcp_stub` (a std-only in-crate binary reached via
//! `CARGO_BIN_EXE_echo_mcp_stub`), so the test needs no python3/node. The concrete
//! `SessionTransport` alias is iroh-typed, so `run()` cannot be driven over an
//! in-memory pipe; instead the test drives the generic seam `SpawnBackend::run_over`
//! (which `run()` delegates to) over a `tokio::io::duplex`. This exercises the REAL
//! spawn + env-injection + bidirectional pump — only the transport's byte substrate
//! differs from the iroh path.
use std::sync::Arc;
use std::time::Duration;
use mcpmesh::backends::spawn::SpawnBackend;
use mcpmesh_net::PeerIdentity;
use mcpmesh_net::transport::NdjsonTransport;
use serde_json::json;
use tokio::io::{duplex, split};
use tokio::sync::Semaphore;
use tokio::time::timeout;
const MAX_FRAME: usize = 16 * 1024 * 1024;
const STUB: &str = env!("CARGO_BIN_EXE_echo_mcp_stub");
#[tokio::test]
async fn run_backend_pumps_frames_and_injects_identity() {
timeout(Duration::from_secs(30), async {
// Two ends of one in-memory pipe: the backend consumes the server end; the
// test drives the client end as the AI-side peer would over the mesh.
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend = SpawnBackend {
cmd: vec![STUB.to_string()],
concurrency: Arc::new(Semaphore::new(4)),
service: "test".into(),
audit: mcpmesh::audit::AuditSink::disabled(),
limiter: mcpmesh::limits::RateLimiter::unlimited_shared(),
env: Default::default(),
cwd: None,
};
// The gate-resolved identity, threaded through run_over per-caller (Task 9).
let identity = Some(PeerIdentity {
endpoint: [0u8; 32].into(),
name: "bob".into(),
user_id: None,
groups: vec![],
});
// The reserved-`_meta`-stripped initialize the daemon hands the backend.
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {}}
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
// The forwarded initialize draws the child's InitializeResult back through
// the transport — the first proof the child spawned and the pump is live.
let init_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(init_resp["id"], 1);
assert_eq!(init_resp["result"]["serverInfo"]["name"], "echo-stub");
// Drive a tools/call; assert the echo AND that the child saw MCPMESH_PEER_NAME.
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "hello mesh"}}
}))
.await
.unwrap();
let call_resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(call_resp["id"], 2);
assert_eq!(call_resp["result"]["content"][0]["text"], "hello mesh");
// Env injection proven: the child echoed back the injected identity name.
assert_eq!(call_resp["result"]["peer_name"], "bob");
// Closing the client end EOFs the transport → the backend drops the child
// (kill_on_drop) and returns Ok — the session-close teardown path.
drop(client);
session
.await
.unwrap()
.expect("run_over returns Ok on transport EOF");
})
.await
.expect("spawn backend test timed out");
}
#[tokio::test]
async fn concurrency_cap_is_enforced() {
timeout(Duration::from_secs(30), async {
// Cap of one: the first session takes the only permit; a second overlapping
// session must be refused with the concurrency-cap error (spec §6.2).
let sem = Arc::new(Semaphore::new(1));
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = NdjsonTransport::new(cr, cw, MAX_FRAME);
let backend1 = SpawnBackend {
cmd: vec![STUB.to_string()],
concurrency: sem.clone(),
service: "test".into(),
audit: mcpmesh::audit::AuditSink::disabled(),
limiter: mcpmesh::limits::RateLimiter::unlimited_shared(),
env: Default::default(),
cwd: None,
};
let init = json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}});
let session1 =
tokio::spawn(async move { backend1.run_over(None, init, backend_transport).await });
// Reading the first session's initialize response guarantees it has acquired
// the permit and spawned its child — the two sessions now overlap.
let resp = client.recv_value().await.unwrap().unwrap();
assert_eq!(resp["result"]["serverInfo"]["name"], "echo-stub");
// Second session against the same (exhausted) semaphore → refused before spawn.
let (server_io2, client_io2) = duplex(64 * 1024);
let (sr2, sw2) = split(server_io2);
let backend_transport2 = NdjsonTransport::new(sr2, sw2, MAX_FRAME);
let (cr2, cw2) = split(client_io2);
let mut client2 = NdjsonTransport::new(cr2, cw2, MAX_FRAME);
let backend2 = SpawnBackend {
cmd: vec![STUB.to_string()],
concurrency: sem.clone(),
service: "test".into(),
audit: mcpmesh::audit::AuditSink::disabled(),
limiter: mcpmesh::limits::RateLimiter::unlimited_shared(),
env: Default::default(),
cwd: None,
};
// The backend HANDLES the cap: it sends -32053 + closes and returns Ok(()) — a clean
// refusal, NOT a session error (so `serve` does not warn!("session ended with error")
// for a normal, load-triggered refusal).
backend2
.run_over(
None,
json!({"jsonrpc": "2.0", "id": 7, "method": "initialize", "params": {}}),
backend_transport2,
)
.await
.expect("cap refusal is handled cleanly (Ok), not a session error");
// The caller RECEIVES a well-formed -32053 refusal (echoing the initialize id), not a
// hang — the backend fully answers before returning.
let refusal = client2
.recv_value()
.await
.unwrap()
.expect("a -32053 refusal frame, not EOF");
assert_eq!(refusal["error"]["code"], -32053);
assert_eq!(refusal["error"]["data"]["source"], "mcpmesh");
assert_eq!(refusal["id"], 7, "the refusal echoes the initialize id");
// Release the first session; it returns cleanly.
drop(client);
session1.await.unwrap().expect("first session returns Ok");
})
.await
.expect("concurrency test timed out");
}
/// #51: a `run` backend applies per-service `env` (a normal var reaches the child) while the
/// #60 review: the `MCPMESH_*` strip is the ONLY defense for MCPMESH_ vars the identity injector
/// never sets — above all `MCPMESH_HOME`, which sandboxes all daemon state (keys, config, data,
/// socket). The identity vars are protected by injection-overwrite regardless, so a test that only
/// asserts those passes even with the strip deleted; this one does not.
///
/// The `Mcpmesh_Home` iteration is effectively a WINDOWS-ONLY assertion: Windows environment keys
/// compare case-insensitively, so that key would arrive as `MCPMESH_HOME` under an exact-case
/// filter. On Unix the keys are distinct and the child never reads it, so this iteration passes
/// there whether or not the filter folds case — CI's `windows-latest` job is what actually
/// enforces it.
#[tokio::test(flavor = "multi_thread")]
async fn a_service_env_cannot_set_mcpmesh_home() {
let _ = timeout(Duration::from_secs(30), async {}).await;
for key in ["MCPMESH_HOME", "Mcpmesh_Home"] {
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = NdjsonTransport::new(cr, cw, MAX_FRAME);
let mut env = std::collections::BTreeMap::new();
env.insert(key.to_string(), "/tmp/attacker-controlled".to_string());
let backend = SpawnBackend {
cmd: vec![STUB.to_string()],
concurrency: Arc::new(Semaphore::new(4)),
service: "test".into(),
audit: mcpmesh::audit::AuditSink::disabled(),
limiter: mcpmesh::limits::RateLimiter::unlimited_shared(),
env,
cwd: None,
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32].into(),
name: "bob".into(),
user_id: None,
groups: vec![],
});
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {}}
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let _init = client.recv_value().await.unwrap().unwrap();
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "hi"}}
}))
.await
.unwrap();
let call = client.recv_value().await.unwrap().unwrap();
assert_eq!(
call["result"]["mesh_home"], "",
"a service env must not be able to set MCPMESH_HOME via `{key}` — it sandboxes all \
daemon state, and the identity injector never overwrites it"
);
drop(client);
let _ = session.await.unwrap();
}
}
/// injected `MCPMESH_PEER_*` identity is AUTHORITATIVE — a service env cannot spoof it, even
/// `MCPMESH_PEER_USER` for an UNBOUND caller (identity.user_id = None).
#[tokio::test]
async fn run_backend_env_reaches_child_but_identity_is_not_spoofable() {
timeout(Duration::from_secs(30), async {
let (server_io, client_io) = duplex(64 * 1024);
let (sr, sw) = split(server_io);
let backend_transport = NdjsonTransport::new(sr, sw, MAX_FRAME);
let (cr, cw) = split(client_io);
let mut client = NdjsonTransport::new(cr, cw, MAX_FRAME);
let mut env = std::collections::BTreeMap::new();
env.insert(
"SPAWN_TEST_ENV".to_string(),
"reached-the-child".to_string(),
);
// A malicious service definition trying to forge the caller's user_id (the caller is
// UNBOUND, so the injector would otherwise not overwrite this).
env.insert("MCPMESH_PEER_USER".to_string(), "b64u:FORGED".to_string());
// #60: the same spoof attempt against the new STABLE device principal. This one matters
// more than the others — it is the var a `run` server is now told to key per-caller
// scoping on, so a forgeable value would be an authz bypass by config.
env.insert("MCPMESH_PEER_EID".to_string(), "eid:FORGED".to_string());
let backend = SpawnBackend {
cmd: vec![STUB.to_string()],
concurrency: Arc::new(Semaphore::new(4)),
service: "test".into(),
audit: mcpmesh::audit::AuditSink::disabled(),
limiter: mcpmesh::limits::RateLimiter::unlimited_shared(),
env,
cwd: None,
};
let identity = Some(PeerIdentity {
endpoint: [0u8; 32].into(),
name: "bob".into(),
user_id: None, // UNBOUND — the spoof target
groups: vec![],
});
let initialize = json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {}}
});
let session = tokio::spawn(async move {
backend
.run_over(identity, initialize, backend_transport)
.await
});
let _init = client.recv_value().await.unwrap().unwrap();
client
.send_value(json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"arguments": {"text": "hi"}}
}))
.await
.unwrap();
let call = client.recv_value().await.unwrap().unwrap();
// Per-service env reached the child.
assert_eq!(
call["result"]["test_env"], "reached-the-child",
"per-service env must reach the child (#51)"
);
// The forged MCPMESH_PEER_USER was STRIPPED — the child sees the real (empty) user, not
// the spoof. Identity is authoritative, not settable by a service definition.
assert_eq!(
call["result"]["peer_user"], "",
"a service env must never spoof MCPMESH_PEER_USER (#51 security)"
);
assert_eq!(call["result"]["peer_name"], "bob", "real identity injected");
// #60: the forged eid loses to the real authenticated principal.
//
// NOTE what this does and does not prove. For the vars the injector ALWAYS sets, the
// guarantee is injection-order (the authoritative `env()` overwrites whatever the service
// env put there) — this assertion passes even with the `MCPMESH_*` filter deleted. The
// filter is the ONLY defense for `MCPMESH_*` keys the injector never touches; that case is
// covered separately by `a_service_env_cannot_set_mcpmesh_home`.
assert_eq!(
call["result"]["peer_eid"],
mcpmesh_net::EndpointId::from_bytes([0u8; 32]).principal(),
"a service env must never spoof MCPMESH_PEER_EID — it is the authz key"
);
drop(client);
let _ = session.await.unwrap();
})
.await
.expect("env test timed out");
}