1	//! The broker-owned network host (D4a) — ADR-0004 §B made real for the wire.
2	//!
3	//! [`NetHost`] is the broker's ownership of the node's live network presence:
4	//! the iroh endpoint ([`spt_net::net::endpoint::NetEndpoint`]) and every
5	//! accepted/dialed QUIC connection. A live cross-node connection is exactly the
6	//! kind of resource ADR-0004 §B assigns to the broker — a consumer would lose
7	//! continuity on brain restart — so the endpoint is constructed *here*, in the
8	//! stable kernel, never in the brain. The brain drives net operations over the
9	//! versioned IPC ([`crate::msg::KIND_NET_STATUS`] / [`crate::msg::KIND_NET_DIAL`])
10	//! and may detach/reattach freely; connections persist in the broker's table.
11	//!
12	//! ## Runtime placement (M4-D4-PLAN locked decision)
13	//!
14	//! The broker is otherwise sync/thread-based; iroh requires tokio. So the host
15	//! owns a **dedicated multi-thread tokio runtime** on named worker threads
16	//! (`spt-broker-net`): QUIC objects live in structures only the runtime's tasks
17	//! and the host's own `block_on` bridges touch, and the runtime drops with the
18	//! host (broker teardown stops all net tasks). Spike #3's broker binary proved
19	//! exactly this shape — a broker-owned endpoint surviving brain kill/restart.
20	//!
21	//! ## Ownership closes two §B rows by construction
22	//!
23	//! `MdnsAddressLookup` and the relay session are both constructed *inside* the
24	//! iroh endpoint, so broker ownership of the endpoint closes the ADR-0004 §B
25	//! "mDNS socket / relay session" row with no extra machinery: there is no
26	//! separate socket to own.
27	// [impl->REQ-DAEMON-2]
28
29	use std::collections::{HashMap, HashSet, VecDeque};
30	use std::io;
31	use std::sync::atomic::{AtomicU64, Ordering};
32	use std::sync::{Arc, Mutex};
33
34	use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream, ReadHalf, WriteHalf};
35
36	use spt_net::net::endpoint::{
37	    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,
38	    SendStream, SPT_NET_ALPN,
39	};
40	use spt_net::net::mesh::seedproof::ProofRole;
41	use spt_proto::identity::{Identity, PublicKey};
42
43	use crate::broker::SharedSend;
44	use crate::codec::write_frame;
45	use crate::seedproofx::{prove_membership, MembershipSource, RosterExchange};
46	use crate::msg::{
47	    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,
48	    NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DISCONNECTED,
49	};
50
51	/// The reserved [`crate::effect::EffectKey`] session namespace for net-scoped
52	/// effects (a dial has no PTY session). Broker session ids are minted from 1
53	/// (`Broker::next_id` starts at 1), so `0` can never collide with a real session.
54	pub const NET_EFFECT_SESSION: u64 = 0;
55
56	/// Everything needed to stand the broker's endpoint up: the node's identity
57	/// (the endpoint *is* the node's name — EndpointId == pubkey), the relay stance
58	/// (from `daemon.json` in production; [`RelayPolicy::Disabled`] in hermetic
59	/// tests), and whether mDNS LAN discovery rides along.
60	pub struct NetConfig {
61	    /// The node identity the endpoint binds as (seed → same ed25519 keypair).
62	    pub identity: Identity,
63	    /// Relay stance (ADR-0002): n0 default / self-host / disabled.
64	    pub relay: RelayPolicy,
65	    /// mDNS LAN discovery on/off (off in hermetic tests).
66	    pub discovery: LocalDiscovery,
67	    /// Socket bind scope: [`BindScope::All`] in production;
68	    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,
69	    /// no per-test-binary Windows firewall prompt).
70	    pub scope: BindScope,
71	    /// Live source of this node's subnet credentials for the connect-time
72	    /// seed-proof (Mesh-D2, REQ-MESH-1). `Some` enables the mutual proof at
73	    /// every dial + accept (fail → drop); `None` disables it entirely — both
74	    /// peers must agree, or the side expecting the control stream would hang.
75	    /// Production wires `Some(SubnetStore::load …)`; mechanics-only tests that
76	    /// don't exercise membership pass `None`.
77	    pub membership: Option<MembershipSource>,
78	}
79
80	/// Default bounded stream-read ring depth (chunks) — same sizing rationale as
81	/// the PTY `OutputLog`: large enough that a brain-restart window never evicts
82	/// unreplayed chunks in practice; bounded so an unsubscribed stream can't
83	/// buffer without limit.
84	const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;
85
86	/// Max bytes pulled per stream read (one ring chunk).
87	const STREAM_READ_CHUNK: usize = 64 * 1024;
88
89	/// Transport backing a held connection. A real `Quic` connection (the only
90	/// kind an inbound handshake can produce), or a broker-minted `Loopback` —
91	/// an in-process connection with NO iroh handshake whose identity IS this node,
92	/// established BY CONSTRUCTION. A loopback conn can ONLY be minted broker-side
93	/// via [`NetHost::dial_loopback`]; it can never arise from an inbound handshake
94	/// (a remote always presents its own key and cannot claim to be us). This is
95	/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12
96	/// W1.5, B1) — QUIC self-dial is refused by iroh ("Connecting to ourself is not
97	/// supported"), so local attach rides a broker loopback conn instead.
98	enum ConnKind {
99	    /// A real QUIC connection — kept alive by ownership here; dropping it closes
100	    /// the connection. Cloned (cheap handle) for stream opens.
101	    Quic(Connection),
102	    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its
103	    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];
104	    /// there is no accept loop and no closed-watcher (it lives for the host).
105	    Loopback,
106	}
107
108	/// One live connection the broker holds (QUIC or loopback).
109	struct ConnEntry {
110	    /// The transport backing this connection.
111	    kind: ConnKind,
112	    /// The remote node's stable name (hex pubkey). For QUIC, proven by the
113	    /// handshake; for loopback, THIS node's own id (the broker-minted identity).
114	    remote_id_hex: String,
115	    /// The subnets the peer proved current-epoch seed-knowledge of at connect
116	    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:
117	    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;
118	    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run
119	    /// in parallel — belt + suspenders, no regression).
120	    proven_subnets: HashSet<String>,
121	}
122
123	/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`
124	/// discipline applied to the network (D4b): the stream's single read pump is
125	/// the only producer; a brain connection attaches as the subscriber; every
126	/// subscriber send happens under the enclosing `Mutex`, so live chunks and a
127	/// replay can never interleave out of `seq` order. Chunks produced while no
128	/// brain is attached sit in the ring and replay on the next subscribe — a
129	/// brain restart loses nothing (Spike #3 invariant B/D).
130	// [impl->REQ-DAEMON-2]
131	struct StreamLog {
132	    stream_id: u64,
133	    next_seq: u64,
134	    ring: VecDeque<(u64, Vec<u8>)>,
135	    cap_chunks: usize,
136	    subscriber: Option<SharedSend>,
137	    /// Read side finished cleanly (peer called finish / conn ended).
138	    finished: bool,
139	    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not
140	    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must
141	    /// NOT drop bytes — CONTEXT:262 "tunneled protocols cannot drop frames, so
142	    /// congestion surfaces as lag, never loss." So a retentive log never cap-evicts
143	    /// on append (no loss), and the read pump REFUSES to pull the next chunk once
144	    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the
145	    /// transport's native flow-control then lags the *sender*. A chunk leaves the
146	    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is
147	    /// woken to resume via the entry's `room` notifier. Ordinary streams keep
148	    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an
149	    /// unbounded ring) and are drained by subscribe, never the sync cursor.
150	    retentive_cap: usize,
151	}
152
153	impl StreamLog {
154	    fn new(stream_id: u64, cap_chunks: usize) -> Self {
155	        StreamLog {
156	            stream_id,
157	            next_seq: 0,
158	            ring: VecDeque::new(),
159	            cap_chunks,
160	            subscriber: None,
161	            finished: false,
162	            retentive_cap: 0,
163	        }
164	    }
165
166	    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.
167	    fn set_retentive(&mut self, cap: usize) {
168	        self.retentive_cap = cap;
169	    }
170
171	    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,
172	    /// live-send to the current subscriber. Read-pump only. A non-retentive log
173	    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log
174	    /// never evicts — bytes leave only via [`drain`](Self::drain).
175	    fn append(&mut self, bytes: &[u8]) {
176	        let seq = self.next_seq;
177	        self.next_seq += 1;
178	        self.ring.push_back((seq, bytes.to_vec()));
179	        if self.retentive_cap == 0 {
180	            while self.ring.len() > self.cap_chunks {
181	                self.ring.pop_front();
182	            }
183	        }
184	        if let Some(sub) = &self.subscriber {
185	            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);
186	            if let Ok(mut s) = sub.lock() {
187	                let _ = write_frame(&mut *s, &frame);
188	            }
189	        }
190	    }
191
192	    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).
193	    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the
194	    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an
195	    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,
196	    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so
197	    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing
198	    /// pending yet.
199	    fn drain(&mut self) -> (Vec<u8>, bool) {
200	        let mut out = Vec::new();
201	        while let Some((_, chunk)) = self.ring.pop_front() {
202	            out.extend_from_slice(&chunk);
203	        }
204	        (out, self.finished)
205	    }
206
207	    /// Mark the read side finished and tell the subscriber (with the final
208	    /// cursor, so it can verify completeness).
209	    fn finish(&mut self) {
210	        self.finished = true;
211	        if let Some(sub) = &self.subscriber {
212	            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);
213	            if let Ok(mut s) = sub.lock() {
214	                let _ = write_frame(&mut *s, &frame);
215	            }
216	        }
217	    }
218
219	    /// Attach `sub` as the subscriber, replaying every retained chunk with
220	    /// `seq >= from_seq` (then eof, if already finished) before live streaming.
221	    fn attach(&mut self, sub: SharedSend, from_seq: u64) {
222	        self.subscriber = Some(Arc::clone(&sub));
223	        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {
224	            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);
225	            if let Ok(mut s) = sub.lock() {
226	                let _ = write_frame(&mut *s, &frame);
227	            }
228	        }
229	        if self.finished {
230	            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);
231	            if let Ok(mut s) = sub.lock() {
232	                let _ = write_frame(&mut *s, &frame);
233	            }
234	        }
235	    }
236
237	    /// Detach `sub` iff it is the current subscriber (a late disconnect must
238	    /// not unsubscribe a newer brain).
239	    fn detach_if(&mut self, sub: &SharedSend) {
240	        if let Some(cur) = &self.subscriber {
241	            if Arc::ptr_eq(cur, sub) {
242	                self.subscriber = None;
243	            }
244	        }
245	    }
246	}
247
248	/// A bounded, sequenced log of connection liveness events — the broker-owned
249	/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline
250	/// applied to the conn table itself. `register_conn` appends `connected`; each
251	/// connection's closed-watcher appends `disconnected` after removing the row.
252	/// A brain subscribes with a cursor and replays events produced while it was
253	/// down, so presence survives a brain restart exactly like stream reads do.
254	// [impl->REQ-EP-4]
255	struct PresenceLog {
256	    next_seq: u64,
257	    ring: VecDeque<NetPresenceEvent>,
258	    cap_events: usize,
259	    subscriber: Option<SharedSend>,
260	}
261
262	impl PresenceLog {
263	    fn new(cap_events: usize) -> Self {
264	        PresenceLog {
265	            next_seq: 0,
266	            ring: VecDeque::new(),
267	            cap_events,
268	            subscriber: None,
269	        }
270	    }
271
272	    /// Append a liveness event: assign the next `seq`, retain it in the bounded
273	    /// ring, live-send to the current subscriber. Producers: `register_conn`
274	    /// (connected) and the per-conn closed-watchers (disconnected) — every
275	    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive
276	    /// in strict `seq` order.
277	    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {
278	        let ev = NetPresenceEvent {
279	            seq: self.next_seq,
280	            kind: kind.to_string(),
281	            conn_id,
282	            remote_id_hex: remote_id_hex.to_string(),
283	        };
284	        self.next_seq += 1;
285	        self.ring.push_back(ev.clone());
286	        while self.ring.len() > self.cap_events {
287	            self.ring.pop_front();
288	        }
289	        if let Some(sub) = &self.subscriber {
290	            let frame = net_presence_event_envelope(&ev);
291	            if let Ok(mut s) = sub.lock() {
292	                let _ = write_frame(&mut *s, &frame);
293	            }
294	        }
295	    }
296
297	    /// Attach `sub` as the subscriber, replaying every retained event with
298	    /// `seq >= from_seq` before live streaming (the gapless-resume attach).
299	    fn attach(&mut self, sub: SharedSend, from_seq: u64) {
300	        self.subscriber = Some(Arc::clone(&sub));
301	        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {
302	            let frame = net_presence_event_envelope(ev);
303	            if let Ok(mut s) = sub.lock() {
304	                let _ = write_frame(&mut *s, &frame);
305	            }
306	        }
307	    }
308
309	    /// Detach `sub` iff it is the current subscriber (a late disconnect must
310	    /// not unsubscribe a newer brain).
311	    fn detach_if(&mut self, sub: &SharedSend) {
312	        if let Some(cur) = &self.subscriber {
313	            if Arc::ptr_eq(cur, sub) {
314	                self.subscriber = None;
315	            }
316	        }
317	    }
318	}
319
320	/// A stream's send half — the same two transports as [`ConnKind`]. The
321	/// `serve_attach`/operator pump and the `StreamLog` read discipline are
322	/// IDENTICAL across both; only this leaf write differs (QUIC stream vs the
323	/// write end of an in-process duplex). That is the M12 W1.5 one-pump invariant
324	/// made structural: a loopback attach and a cross-node attach diverge at this
325	/// leaf and nowhere above it.
326	enum SendHalf {
327	    Quic(SendStream),
328	    Loopback(WriteHalf<DuplexStream>),
329	}
330
331	impl SendHalf {
332	    /// Write all `bytes` (no-op on empty). Runs on the host runtime.
333	    async fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
334	        match self {
335	            SendHalf::Quic(s) => s
336	                .write_all(bytes)
337	                .await
338	                .map_err(|e| io::Error::other(e.to_string())),
339	            SendHalf::Loopback(w) => w.write_all(bytes).await,
340	        }
341	    }
342
343	    /// Close the send side so the peer's read pump sees a clean end (QUIC
344	    /// `finish` / duplex `shutdown`).
345	    async fn finish(&mut self) -> io::Result<()> {
346	        match self {
347	            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),
348	            SendHalf::Loopback(w) => w.shutdown().await,
349	        }
350	    }
351	}
352
353	/// A stream's read half — fed into the single read pump in [`register_stream`].
354	/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.
355	/// Both append to the same [`StreamLog`], so the subscriber (and everything
356	/// above it) cannot tell the transports apart.
357	enum RecvHalf {
358	    Quic(RecvStream),
359	    Loopback(ReadHalf<DuplexStream>),
360	}
361
362	/// One live stream the broker holds (QUIC or loopback): the send half (brain
363	/// writes ride it over IPC) + the sequenced read log.
364	struct StreamEntry {
365	    conn_id: u64,
366	    remote_id_hex: String,
367	    /// Whether THIS node opened the stream (`open_stream`) vs the peer
368	    /// (accept loop). The inbound dispatcher (D9-1) claims only
369	    /// peer-initiated streams; without this fact the table is ambiguous —
370	    /// it holds locally-opened outbound streams too (gotcha: a dispatcher
371	    /// that served its own requester stream would deadlock on itself).
372	    initiated_locally: bool,
373	    /// The send half, serialized behind an async mutex (writes happen on the
374	    /// host's runtime via `block_on`).
375	    send: tokio::sync::Mutex<SendHalf>,
376	    log: Arc<Mutex<StreamLog>>,
377	    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below
378	    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].
379	    /// Unused by ordinary (non-retentive) streams.
380	    room: Arc<tokio::sync::Notify>,
381	}
382
383	/// Tables shared between the host's sync surface and its runtime tasks
384	/// (accept loop, per-conn stream acceptors, per-stream read pumps).
385	struct NetShared {
386	    next_conn_id: AtomicU64,
387	    conns: Mutex<HashMap<u64, ConnEntry>>,
388	    next_stream_id: AtomicU64,
389	    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,
390	    ring_cap: usize,
391	    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.
392	    presence: Mutex<PresenceLog>,
393	}
394
395	impl NetShared {
396	    fn new() -> Self {
397	        NetShared {
398	            next_conn_id: AtomicU64::new(1),
399	            conns: Mutex::new(HashMap::new()),
400	            next_stream_id: AtomicU64::new(1),
401	            streams: Mutex::new(HashMap::new()),
402	            ring_cap: DEFAULT_STREAM_RING_CHUNKS,
403	            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),
404	        }
405	    }
406	}
407
408	/// Register a live connection: mint its broker id and spawn its inbound-stream
409	/// acceptor. Must run inside the host's runtime context (the accept loop, or a
410	/// `block_on` future) — it spawns tasks.
411	fn register_conn(
412	    shared: &Arc<NetShared>,
413	    conn: Connection,
414	    proven_subnets: HashSet<String>,
415	) -> (u64, String) {
416	    let remote_id_hex = PublicKey::from_bytes(conn.remote_id().as_bytes())
417	        .expect("an iroh EndpointId is always a valid ed25519 public key")
418	        .to_hex();
419	    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);
420	    shared.conns.lock().unwrap().insert(
421	        id,
422	        ConnEntry {
423	            kind: ConnKind::Quic(conn.clone()),
424	            remote_id_hex: remote_id_hex.clone(),
425	            proven_subnets,
426	        },
427	    );
428
429	    // Presence: announce the row BEFORE spawning the closed-watcher, so the
430	    // `connected` event can never race behind its own `disconnected` (D4c).
431	    shared
432	        .presence
433	        .lock()
434	        .unwrap()
435	        .append(PRESENCE_CONNECTED, id, &remote_id_hex);
436
437	    // Closed-watcher: the row's exit path. Awaits the connection's end (clean
438	    // close, conn loss, or our own endpoint close), removes the row — conns
439	    // finally LEAVE the table — then appends the `disconnected` event (row
440	    // gone before the event is observable).
441	    // [impl->REQ-EP-4]
442	    let sh_close = Arc::clone(shared);
443	    let conn_close = conn.clone();
444	    let hex_close = remote_id_hex.clone();
445	    tokio::spawn(async move {
446	        conn_close.closed().await;
447	        sh_close.conns.lock().unwrap().remove(&id);
448	        sh_close
449	            .presence
450	            .lock()
451	            .unwrap()
452	            .append(PRESENCE_DISCONNECTED, id, &hex_close);
453	    });
454
455	    // Inbound-stream acceptor: every peer-opened bidi stream joins the table
456	    // and gets a read pump. (QUIC streams are lazy — the peer's open becomes
457	    // visible here on its first write.) Ends when the connection does.
458	    let sh = Arc::clone(shared);
459	    let hex = remote_id_hex.clone();
460	    tokio::spawn(async move {
461	        while let Ok((send, recv)) = conn.accept_bi().await {
462	            register_stream(
463	                &sh,
464	                id,
465	                &hex,
466	                SendHalf::Quic(send),
467	                RecvHalf::Quic(recv),
468	                false,
469	            );
470	        }
471	    });
472
473	    (id, remote_id_hex)
474	}
475
476	/// Register a live stream (opened or accepted): mint its broker id and spawn
477	/// its read pump. Must run inside the host's runtime context.
478	/// `initiated_locally` records the direction fact for the table (true only
479	/// from [`NetHost::open_stream`]).
480	fn register_stream(
481	    shared: &Arc<NetShared>,
482	    conn_id: u64,
483	    remote_id_hex: &str,
484	    send: SendHalf,
485	    recv: RecvHalf,
486	    initiated_locally: bool,
487	) -> u64 {
488	    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);
489	    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));
490	    let room = Arc::new(tokio::sync::Notify::new());
491	    shared.streams.lock().unwrap().insert(
492	        id,
493	        Arc::new(StreamEntry {
494	            conn_id,
495	            remote_id_hex: remote_id_hex.to_string(),
496	            initiated_locally,
497	            send: tokio::sync::Mutex::new(send),
498	            log: Arc::clone(&log),
499	            room: Arc::clone(&room),
500	        }),
501	    );
502
503	    // The read pump — the stream's single producer. A clean end (peer finish)
504	    // and a torn end (conn lost) both finish the log; the buffered chunks stay
505	    // replayable either way. The transport differs only at this read leaf
506	    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the
507	    // same `StreamLog`, so nothing above the pump can tell them apart.
508	    //
509	    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,
510	    // so before pulling the next chunk the pump WAITS while the undrained ring is
511	    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so
512	    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the
513	    // SENDER — "lag, never loss" (CONTEXT:262). A `drain` wakes `room`. Ordinary
514	    // streams have `retentive_cap == 0`, so this gate is a no-op for them.
515	    tokio::spawn(async move {
516	        match recv {
517	            RecvHalf::Quic(mut recv) => loop {
518	                await_ring_room(&log, &room).await;
519	                match recv.read_chunk(STREAM_READ_CHUNK).await {
520	                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),
521	                    Ok(None) | Err(_) => {
522	                        log.lock().unwrap().finish();
523	                        break;
524	                    }
525	                }
526	            },
527	            RecvHalf::Loopback(mut recv) => {
528	                let mut buf = vec![0u8; STREAM_READ_CHUNK];
529	                loop {
530	                    await_ring_room(&log, &room).await;
531	                    match recv.read(&mut buf).await {
532	                        Ok(0) | Err(_) => {
533	                            log.lock().unwrap().finish();
534	                            break;
535	                        }
536	                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
537	                    }
538	                }
539	            }
540	        }
541	    });
542
543	    id
544	}
545
546	/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained
547	/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for
548	/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.
549	// [impl->REQ-SHELL-4]
550	async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {
551	    loop {
552	        {
553	            let l = log.lock().unwrap();
554	            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {
555	                return;
556	            }
557	        }
558	        room.notified().await;
559	    }
560	}
561
562	/// The broker-owned net endpoint + live connection/stream tables (see module
563	/// docs).
564	pub struct NetHost {
565	    /// The dedicated tokio runtime hosting the endpoint and its accept loop.
566	    /// Dropping the host drops the runtime, stopping every net task with it.
567	    runtime: tokio::runtime::Runtime,
568	    endpoint: Arc<NetEndpoint>,
569	    shared: Arc<NetShared>,
570	    /// `op_id → conn_id` for exactly-once dial replies: a deduped replay of a
571	    /// journaled dial is answered with the connection the first apply opened.
572	    dial_ops: Mutex<HashMap<u64, u64>>,
573	    /// `op_id → stream_id` for exactly-once stream-open replies (D4b), same
574	    /// discipline as `dial_ops`. Shares the one net op-id namespace.
575	    stream_ops: Mutex<HashMap<u64, u64>>,
576	    /// This node's pubkey (the seed-proof's local channel-binding identity).
577	    local_id: PublicKey,
578	    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables
579	    /// the proof (see [`NetConfig::membership`]).
580	    membership: Option<MembershipSource>,
581	    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live
582	    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`
583	    /// so the many hermetic test configs need no change — a roster exchange is a
584	    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +
585	    /// cloned per connect on the net runtime, so the accept loop (spawned in
586	    /// `start`, before the host exists) shares it through this `Arc`.
587	    roster: Arc<Mutex<Option<RosterExchange>>>,
588	    /// The broker's single in-process loopback connection (M12 W1.5, B1),
589	    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
590	    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
591	    /// row lives for the host (no handshake to lose, no closed-watcher).
592	    loopback_conn: Mutex<Option<u64>>,
593	    /// Serializes [`Self::open_loopback_pair`] (M11-W3) so the peer-row discovery
594	    /// (a before/after diff on the loopback conn's inbound rows) sees exactly the
595	    /// one row this open minted — concurrent pair-opens on the shared loopback conn
596	    /// would otherwise race the diff.
597	    loopback_pair_lock: Mutex<()>,
598	}
599
600	impl NetHost {
601	    /// Bind the broker's endpoint on a fresh dedicated runtime and start the
602	    /// accept loop. The endpoint speaks TWO ALPNs (M7 D3, REQ-SUBNET-2): the
603	    /// trusted [`SPT_NET_ALPN`] (connections join the table as before) and
604	    /// the pre-trust pairing ALPN — routed to the always-on responder
605	    /// ([`crate::pairhost::respond`]), never registered in the stream table.
606	    /// One endpoint for both is deliberate: the pairing transcript binds and
607	    /// pins the REAL node pubkey, and the relay can only route one endpoint
608	    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).
609	    /// The meet-rotation listener loop rides the same runtime.
610	    // [impl->REQ-DAEMON-2]
611	    // [impl->REQ-SUBNET-2]
612	    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {
613	        // Hermeticity guard (REQ-PAIR-8): a loopback-scoped endpoint is the
614	        // "no network reached" promise — the ceremony clock's lazy NTP query
615	        // must honor it (tests must never probe a real NTP server). Real
616	        // scopes keep the decision-18 offset correction.
617	        spt_net::net::pairing::ntp::set_enabled(!matches!(cfg.scope, BindScope::Loopback));
618	        let runtime = tokio::runtime::Builder::new_multi_thread()
619	            .worker_threads(2)
620	            .thread_name("spt-broker-net")
621	            .enable_all()
622	            .build()?;
623	        let endpoint = runtime
624	            .block_on(NetEndpoint::bind_scoped(
625	                &cfg.identity,
626	                cfg.relay.clone(),
627	                cfg.discovery,
628	                vec![
629	                    SPT_NET_ALPN.to_vec(),
630	                    spt_net::net::pairing::wire::SPT_PAIR_ALPN.to_vec(),
631	                ],
632	                cfg.scope,
633	            ))
634	            .map_err(|e| io::Error::other(e.to_string()))?;
635	        let endpoint = Arc::new(endpoint);
636	        let shared = Arc::new(NetShared::new());
637	        let local_pub = cfg.identity.public_key();
638	        let pair_rate = Arc::new(tokio::sync::Mutex::new(
639	            spt_net::net::pairing::ratelimit::PairingRateLimiter::new(),
640	        ));
641
642	        // Accept loop: lives for the endpoint's lifetime on the host's runtime.
643	        // Each handshake completes on its own task so a slow peer can't stall
644	        // the loop; a failed handshake is simply not registered. The pairing
645	        // ALPN routes to the responder; everything else is the trusted table.
646	        let roster = Arc::new(Mutex::new(None::<RosterExchange>));
647	        let ep = Arc::clone(&endpoint);
648	        let sh = Arc::clone(&shared);
649	        let rate = Arc::clone(&pair_rate);
650	        let accept_membership = cfg.membership.clone();
651	        let accept_roster = Arc::clone(&roster);
652	        let accept_ep = Arc::clone(&endpoint);
653	        runtime.spawn(async move {
654	            while let Some(incoming) = ep.accept().await {
655	                let sh = Arc::clone(&sh);
656	                let rate = Arc::clone(&rate);
657	                let membership = accept_membership.clone();
658	                // Snapshot the (opt-in) roster exchange + our live address for
659	                // this connect; the leg runs after the proof verifies.
660	                let roster_rx = accept_roster.lock().unwrap().clone();
661	                let self_addr =
662	                    serde_json::to_value(accept_ep.addr()).unwrap_or(serde_json::Value::Null);
663	                tokio::spawn(async move {
664	                    let Ok(connecting) = incoming.accept() else {
665	                        return;
666	                    };
667	                    let Ok(conn) = connecting.await else { return };
668	                    if conn.alpn() == spt_net::net::pairing::wire::SPT_PAIR_ALPN {
669	                        crate::pairhost::respond(conn, local_pub, rate).await;
670	                    } else {
671	                        // Mesh-D2: prove membership in-band BEFORE the connection
672	                        // joins the table. A non-prover (no shared subnet / bad
673	                        // seed) is dropped — the conn falls out of scope and QUIC
674	                        // closes it. With membership disabled, register as before.
675	                        // Mesh-D4: on success, a roster leg rides the same stream.
676	                        let proven = match membership {
677	                            Some(src) => {
678	                                let creds = src();
679	                                match prove_membership(
680	                                    &conn,
681	                                    local_pub,
682	                                    ProofRole::Acceptor,
683	                                    &creds,
684	                                    roster_rx.as_ref(),
685	                                    &self_addr,
686	                                )
687	                                .await
688	                                {
689	                                    Some(set) => set,
690	                                    None => return, // drop the connection
691	                                }
692	                            }
693	                            None => HashSet::new(),
694	                        };
695	                        register_conn(&sh, conn, proven);
696	                    }
697	                });
698	            }
699	        });
700
701	        // The meet-rotation loop (M7 D3): how a joiner that knows only
702	        // (subnet name, code) finds this responder. Idles with no subnets.
703	        crate::pairhost::spawn_meet_rotation(
704	            runtime.handle(),
705	            Arc::clone(&endpoint),
706	            cfg.relay,
707	            cfg.discovery,
708	            cfg.scope,
709	        );
710
711	        // The limiter Arc lives on in the accept loop; the host keeps no
712	        // handle (per-ceremony state is the responder's, not the table's).
713	        drop(pair_rate);
714
715	        Ok(NetHost {
716	            runtime,
717	            endpoint,
718	            shared,
719	            dial_ops: Mutex::new(HashMap::new()),
720	            stream_ops: Mutex::new(HashMap::new()),
721	            local_id: local_pub,
722	            membership: cfg.membership,
723	            roster,
724	            loopback_conn: Mutex::new(None),
725	            loopback_pair_lock: Mutex::new(()),
726	        })
727	    }
728
729	    /// Wire the on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2) onto this
730	    /// live host. The daemon calls this once after `start` with
731	    /// [`crate::seedproofx::production_roster_exchange`]; tests inject their own.
732	    /// Off `NetConfig` by design — see the `roster` field. Takes effect on the
733	    /// next dial/accept (each connect snapshots it).
734	    // [impl->REQ-MESH-2]
735	    pub fn set_roster_exchange(&self, exchange: RosterExchange) {
736	        *self.roster.lock().unwrap() = Some(exchange);
737	    }
738
739	    /// Run the joiner leg of the pairing ceremony (M7 D3, REQ-SUBNET-2) on
740	    /// the host's runtime — the broker IPC `pair-join` arm. The ceremony
741	    /// must run HERE because it binds and pins this endpoint's real node
742	    /// pubkey. Returns the rendered trust outcome, or the classified failure.
743	    // [impl->REQ-SUBNET-2]
744	    pub fn pair_join(
745	        &self,
746	        subnet: &str,
747	        code: &str,
748	        responder_addr: Option<EndpointAddr>,
749	    ) -> Result<String, crate::pairhost::JoinFail> {
750	        let endpoint = Arc::clone(&self.endpoint);
751	        self.runtime.handle().block_on(async move {
752	            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
753	        })
754	    }
755
756	    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).
757	    pub fn node_id_hex(&self) -> String {
758	        self.endpoint.node_id_hex()
759	    }
760
761	    /// The endpoint's dialable address (id + currently-known direct/relay paths).
762	    pub fn addr(&self) -> EndpointAddr {
763	        self.endpoint.addr()
764	    }
765
766	    /// [`Self::addr`] as the opaque JSON the IPC status reply carries.
767	    pub fn addr_json(&self) -> serde_json::Value {
768	        serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null)
769	    }
770
771	    /// Live connections (accepted + dialed) currently held.
772	    pub fn conn_count(&self) -> u64 {
773	        self.shared.conns.lock().unwrap().len() as u64
774	    }
775
776	    /// Dial a peer over `SPT_NET_ALPN`, registering the connection in the table.
777	    /// Bridges sync→async via the host's own runtime (the broker's dispatch
778	    /// threads block here; the future runs on the net worker threads).
779	    // [impl->REQ-DAEMON-2]
780	    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {
781	        let endpoint = Arc::clone(&self.endpoint);
782	        let shared = Arc::clone(&self.shared);
783	        let local_id = self.local_id;
784	        let membership = self.membership.clone();
785	        // Snapshot the (opt-in) roster exchange + our live address for this dial.
786	        let roster_rx = self.roster.lock().unwrap().clone();
787	        let self_addr = serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null);
788	        self.runtime.handle().block_on(async move {
789	            let conn = endpoint
790	                .connect(addr)
791	                .await
792	                .map_err(|e| io::Error::other(e.to_string()))?;
793	            // Mesh-D2: prove membership in-band before the connection is usable.
794	            // A failed/empty proof drops the dial (the conn never registers).
795	            // Mesh-D4: on success, a roster leg rides the same control stream.
796	            let proven = match membership {
797	                Some(src) => {
798	                    let creds = src();
799	                    prove_membership(
800	                        &conn,
801	                        local_id,
802	                        ProofRole::Dialer,
803	                        &creds,
804	                        roster_rx.as_ref(),
805	                        &self_addr,
806	                    )
807	                    .await
808	                    .ok_or_else(|| {
809	                        io::Error::other("seed-proof failed: peer is not a subnet member")
810	                    })?
811	                }
812	                None => HashSet::new(),
813	            };
814	            Ok(register_conn(&shared, conn, proven))
815	        })
816	    }
817
818	    /// Record that journaled dial `op_id` opened `conn_id` (called inside the
819	    /// `apply_once` closure, so a concurrent deduped replay always finds it).
820	    pub fn record_dial_op(&self, op_id: u64, conn_id: u64) {
821	        self.dial_ops.lock().unwrap().insert(op_id, conn_id);
822	    }
823
824	    /// The connection a journaled dial `op_id` opened, if this broker process
825	    /// still holds it (a broker restart drops connections; the journal alone
826	    /// survives — see `Broker::dispatch_net_dial`).
827	    pub fn dial_op_conn(&self, op_id: u64) -> Option<u64> {
828	        self.dial_ops.lock().unwrap().get(&op_id).copied()
829	    }
830
831	    /// The remote node id (hex) of a held connection.
832	    pub fn conn_remote_hex(&self, conn_id: u64) -> Option<String> {
833	        self.shared
834	            .conns
835	            .lock()
836	            .unwrap()
837	            .get(&conn_id)
838	            .map(|e| e.remote_id_hex.clone())
839	    }
840
841	    /// The subnets the peer of `conn_id` proved current-epoch seed-knowledge of
842	    /// at connect (Mesh-D2, REQ-MESH-1). Empty when seed-proof was disabled or
843	    /// the conn is gone. The seam the D5 gate swap reads in place of
844	    /// `is_trusted(subnet, origin)`; unused by any gate at D2.
845	    // [impl->REQ-MESH-1]
846	    pub fn conn_proven_subnets(&self, conn_id: u64) -> HashSet<String> {
847	        self.shared
848	            .conns
849	            .lock()
850	            .unwrap()
851	            .get(&conn_id)
852	            .map(|e| e.proven_subnets.clone())
853	            .unwrap_or_default()
854	    }
855
856	    /// The OBSERVED peer address of a held connection (remote id + the
857	    /// connection's current path addresses) as opaque JSON — the address-
858	    /// seeding write-back the `net-dialed` reply carries (M8 decision 20,
859	    /// REQ-CONV-1). `null` when the conn is gone, path-less, or fails to
860	    /// serialize: the seed is an optimization, never an error path.
861	    // [impl->REQ-CONV-1]
862	    pub fn conn_remote_addr_json(&self, conn_id: u64) -> serde_json::Value {
863	        let conn = {
864	            let conns = self.shared.conns.lock().unwrap();
865	            match conns.get(&conn_id) {
866	                // A loopback conn has no path to observe — nothing to seed.
867	                Some(e) => match &e.kind {
868	                    ConnKind::Quic(c) => c.clone(),
869	                    ConnKind::Loopback => return serde_json::Value::Null,
870	                },
871	                None => return serde_json::Value::Null,
872	            }
873	        };
874	        let addr = spt_net::net::endpoint::conn_remote_addr(&conn);
875	        if addr.is_empty() {
876	            return serde_json::Value::Null; // id-only adds nothing over discovery
877	        }
878	        serde_json::to_value(addr).unwrap_or(serde_json::Value::Null)
879	    }
880
881	    /// Mint (or reuse) the broker's single in-process loopback connection
882	    /// (M12 W1.5, B1) and return `(conn_id, remote_id_hex)` — `remote_id_hex` is
883	    /// THIS node's own id, so a stream served over it gates same-node (access
884	    /// short-circuit Allow). The conn carries NO iroh handshake: its identity is
885	    /// broker-minted BY CONSTRUCTION and can never arise from an inbound dial.
886	    /// This is what lets a LOCAL `spt rc` ride the byte-identical `serve_attach`
887	    /// pump — QUIC self-dial is refused by iroh, so local attach uses this.
888	    /// Idempotent: every call returns the same singleton conn (one is all a
889	    /// local attach needs); no journaling needed (reuse is the dedup).
890	    // [impl->REQ-RC-1]
891	    pub fn dial_loopback(&self) -> (u64, String) {
892	        let remote_id_hex = self.local_id.to_hex();
893	        let mut slot = self.loopback_conn.lock().unwrap();
894	        if let Some(id) = *slot {
895	            return (id, remote_id_hex);
896	        }
897	        let id = self.shared.next_conn_id.fetch_add(1, Ordering::Relaxed);
898	        self.shared.conns.lock().unwrap().insert(
899	            id,
900	            ConnEntry {
901	                kind: ConnKind::Loopback,
902	                remote_id_hex: remote_id_hex.clone(),
903	                proven_subnets: HashSet::new(),
904	            },
905	        );
906	        *slot = Some(id);
907	        (id, remote_id_hex)
908	    }
909
910	    /// Open a bidi stream on a held connection, registering it (with its read
911	    /// pump) in the stream table (D4b). Note QUIC streams are lazy: the peer
912	    /// sees the stream on its first write.
913	    ///
914	    /// On a [`ConnKind::Loopback`] conn there is no peer to accept the stream,
915	    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row
916	    /// (`initiated_locally = true`, the returned id) and a peer row
917	    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via
918	    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened
919	    /// stream. One `tokio::io::duplex` wires both directions; each row's read
920	    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5
921	    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,
922	    /// so its serve gates same-node.
923	    // [impl->REQ-DAEMON-2]
924	    // [impl->REQ-RC-1]
925	    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {
926	        enum Backing {
927	            Quic(Connection),
928	            Loopback,
929	        }
930	        let (backing, remote_id_hex) = {
931	            let conns = self.shared.conns.lock().unwrap();
932	            let e = conns.get(&conn_id).ok_or_else(|| {
933	                io::Error::new(io::ErrorKind::NotFound, format!("no such conn {conn_id}"))
934	            })?;
935	            let backing = match &e.kind {
936	                ConnKind::Quic(c) => Backing::Quic(c.clone()),
937	                ConnKind::Loopback => Backing::Loopback,
938	            };
939	            (backing, e.remote_id_hex.clone())
940	        };
941	        let shared = Arc::clone(&self.shared);
942	        match backing {
943	            Backing::Quic(conn) => self.runtime.handle().block_on(async move {
944	                let (send, recv) = conn
945	                    .open_bi()
946	                    .await
947	                    .map_err(|e| io::Error::other(e.to_string()))?;
948	                Ok(register_stream(
949	                    &shared,
950	                    conn_id,
951	                    &remote_id_hex,
952	                    SendHalf::Quic(send),
953	                    RecvHalf::Quic(recv),
954	                    true,
955	                ))
956	            }),
957	            Backing::Loopback => self.runtime.handle().block_on(async move {
958	                // One duplex carries BOTH directions: operator writes reach the
959	                // peer's read pump and vice-versa. Split each end into a read +
960	                // write half so each row owns exactly its leg.
961	                let (op_io, peer_io) = tokio::io::duplex(STREAM_READ_CHUNK);
962	                let (op_r, op_w) = tokio::io::split(op_io);
963	                let (peer_r, peer_w) = tokio::io::split(peer_io);
964	                // Register the peer (inbound) row first so it is in the table
965	                // before the operator can write its Request onto the wire — the
966	                // dispatcher must never miss the stream.
967	                register_stream(
968	                    &shared,
969	                    conn_id,
970	                    &remote_id_hex,
971	                    SendHalf::Loopback(peer_w),
972	                    RecvHalf::Loopback(peer_r),
973	                    false,
974	                );
975	                Ok(register_stream(
976	                    &shared,
977	                    conn_id,
978	                    &remote_id_hex,
979	                    SendHalf::Loopback(op_w),
980	                    RecvHalf::Loopback(op_r),
981	                    true,
982	                ))
983	            }),
984	        }
985	    }
986
987	    /// Record that journaled open `op_id` minted `stream_id` (inside the
988	    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).
989	    pub fn record_stream_op(&self, op_id: u64, stream_id: u64) {
990	        self.stream_ops.lock().unwrap().insert(op_id, stream_id);
991	    }
992
993	    /// The stream a journaled open `op_id` minted, if still held.
994	    pub fn stream_op_id(&self, op_id: u64) -> Option<u64> {
995	        self.stream_ops.lock().unwrap().get(&op_id).copied()
996	    }
997
998	    /// A held stream's `(conn_id, remote_id_hex)`.
999	    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {
1000	        self.shared
1001	            .streams
1002	            .lock()
1003	            .unwrap()
1004	            .get(&stream_id)
1005	            .map(|e| (e.conn_id, e.remote_id_hex.clone()))
1006	    }
1007
1008	    /// Write `bytes` to a held stream's send side; `finish` closes the send
1009	    /// side after the write (the peer's read pump then sees a clean end).
1010	    // [impl->REQ-DAEMON-2]
1011	    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {
1012	        let entry = {
1013	            let streams = self.shared.streams.lock().unwrap();
1014	            Arc::clone(streams.get(&stream_id).ok_or_else(|| {
1015	                io::Error::new(
1016	                    io::ErrorKind::NotFound,
1017	                    format!("no such stream {stream_id}"),
1018	                )
1019	            })?)
1020	        };
1021	        let bytes = bytes.to_vec();
1022	        self.runtime.handle().block_on(async move {
1023	            let mut send = entry.send.lock().await;
1024	            if !bytes.is_empty() {
1025	                send.write_all(&bytes).await?;
1026	            }
1027	            if finish {
1028	                send.finish().await?;
1029	            }
1030	            Ok(())
1031	        })
1032	    }
1033
1034	    /// Every held stream's info — the query a re-attaching brain uses to find
1035	    /// streams (including peer-initiated ones that arrived while it was down).
1036	    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {
1037	        let streams = self.shared.streams.lock().unwrap();
1038	        streams
1039	            .iter()
1040	            .map(|(id, e)| {
1041	                let log = e.log.lock().unwrap();
1042	                NetStreamInfo {
1043	                    stream_id: *id,
1044	                    conn_id: e.conn_id,
1045	                    remote_id_hex: e.remote_id_hex.clone(),
1046	                    next_seq: log.next_seq,
1047	                    finished: log.finished,
1048	                    initiated_locally: e.initiated_locally,
1049	                }
1050	            })
1051	            .collect()
1052	    }
1053
1054	    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,
1055	    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump
1056	    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).
1057	    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.
1058	    /// No-op if the stream is gone.
1059	    // [impl->REQ-SHELL-4]
1060	    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {
1061	        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {
1062	            e.log.lock().unwrap().set_retentive(cap);
1063	        }
1064	    }
1065
1066	    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):
1067	    /// pops the whole ring in order, concatenated, and wakes the read pump (which
1068	    /// may have been lagging the sender at the backpressure cap). Returns
1069	    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).
1070	    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.
1071	    // [impl->REQ-SHELL-4]
1072	    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {
1073	        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);
1074	        let drained = entry.log.lock().unwrap().drain();
1075	        // The drain freed ring room — wake a pump that was backpressure-parked.
1076	        entry.room.notify_one();
1077	        Some(drained)
1078	    }
1079
1080	    /// A held stream's current undrained ring depth in chunks (test introspection
1081	    /// for the retentive backpressure bound). `None` ⇒ no such stream.
1082	    #[cfg(test)]
1083	    fn ring_len(&self, stream_id: u64) -> Option<usize> {
1084	        self.shared
1085	            .streams
1086	            .lock()
1087	            .unwrap()
1088	            .get(&stream_id)
1089	            .map(|e| e.log.lock().unwrap().ring.len())
1090	    }
1091
1092	    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the
1093	    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just
1094	    /// minted.
1095	    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {
1096	        self.shared
1097	            .streams
1098	            .lock()
1099	            .unwrap()
1100	            .iter()
1101	            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)
1102	            .map(|(id, _)| *id)
1103	            .collect()
1104	    }
1105
1106	    /// Open a same-node loopback tunnel stream PAIR (M11-W3, REQ-SHELL-4) and
1107	    /// return `(owner_stream, shell_stream)` — the operator row the owner side
1108	    /// drives and the cross-wired peer row the shell side drives. The loopback
1109	    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a
1110	    /// write to either send appears on the other's read log). Serialized so the
1111	    /// before/after diff sees exactly the one peer row this open minted.
1112	    // [impl->REQ-SHELL-4]
1113	    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {
1114	        let _guard = self.loopback_pair_lock.lock().unwrap();
1115	        let (conn_id, _) = self.dial_loopback();
1116	        let before = self.inbound_rows_on(conn_id);
1117	        let owner_stream = self.open_stream(conn_id)?;
1118	        let shell_stream = self
1119	            .inbound_rows_on(conn_id)
1120	            .difference(&before)
1121	            .copied()
1122	            .next()
1123	            .ok_or_else(|| io::Error::other("loopback peer row not registered"))?;
1124	        Ok((owner_stream, shell_stream))
1125	    }
1126
1127	    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`
1128	    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).
1129	    // [impl->REQ-DAEMON-2]
1130	    pub(crate) fn subscribe_stream(
1131	        &self,
1132	        stream_id: u64,
1133	        sub: SharedSend,
1134	        from_seq: u64,
1135	    ) -> io::Result<()> {
1136	        let log = {
1137	            let streams = self.shared.streams.lock().unwrap();
1138	            let e = streams.get(&stream_id).ok_or_else(|| {
1139	                io::Error::new(
1140	                    io::ErrorKind::NotFound,
1141	                    format!("no such stream {stream_id}"),
1142	                )
1143	            })?;
1144	            Arc::clone(&e.log)
1145	        };
1146	        log.lock().unwrap().attach(sub, from_seq);
1147	        Ok(())
1148	    }
1149
1150	    /// Detach `sub` from a stream iff it is the current subscriber (brain
1151	    /// disconnect cleanup; the stream + its ring persist).
1152	    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {
1153	        let log = {
1154	            let streams = self.shared.streams.lock().unwrap();
1155	            streams.get(&stream_id).map(|e| Arc::clone(&e.log))
1156	        };
1157	        if let Some(log) = log {
1158	            log.lock().unwrap().detach_if(sub);
1159	        }
1160	    }
1161
1162	    /// Attach `sub` as the presence subscriber, replaying liveness events from
1163	    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.
1164	    // [impl->REQ-EP-4]
1165	    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {
1166	        self.shared.presence.lock().unwrap().attach(sub, from_seq);
1167	    }
1168
1169	    /// Detach `sub` from presence iff it is the current subscriber (brain
1170	    /// disconnect cleanup; the log + its ring persist).
1171	    pub(crate) fn detach_presence_sub(&self, sub: &SharedSend) {
1172	        self.shared.presence.lock().unwrap().detach_if(sub);
1173	    }
1174
1175	    /// Gracefully close the endpoint: every held connection flushes its QUIC
1176	    /// CLOSE so peers' closed-watchers fire promptly (a bare host drop kills
1177	    /// the runtime first and leaves peers to an idle timeout).
1178	    pub fn close(&self) {
1179	        let endpoint = Arc::clone(&self.endpoint);
1180	        self.runtime.handle().block_on(async move {
1181	            endpoint.close().await;
1182	        });
1183	    }
1184	}
1185
1186	#[cfg(test)]
1187	mod tests {
1188	    use super::*;
1189
1190	    fn hermetic(identity: &Identity) -> NetConfig {
1191	        NetConfig {
1192	            identity: Identity::from_seed(&identity.seed()),
1193	            relay: RelayPolicy::Disabled,
1194	            discovery: LocalDiscovery::Off,
1195	            scope: BindScope::Loopback,
1196	            membership: None,
1197	        }
1198	    }
1199
1200	    // [unit->REQ-DAEMON-2] the endpoint is constructed broker-side (inside the
1201	    // host's dedicated runtime) with relays disabled, bound to the node's own
1202	    // identity, with an empty conn table.
1203	    #[test]
1204	    fn host_binds_endpoint_to_node_identity() {
1205	        let id = Identity::generate();
1206	        let host = NetHost::start(hermetic(&id)).expect("host start");
1207	        assert_eq!(host.node_id_hex(), id.public_key().to_hex());
1208	        assert_eq!(host.conn_count(), 0);
1209	        assert!(!host.addr_json().is_null(), "dialable address serializes");
1210	    }
1211
1212	    // [unit->REQ-DAEMON-2] a broker-owned host dials a second in-process host
1213	    // over hermetic loopback QUIC; both sides register the connection — the
1214	    // dialer in its table, the acceptor via its accept loop.
1215	    #[test]
1216	    fn two_hosts_dial_over_loopback() {
1217	        let a = NetHost::start(hermetic(&Identity::generate())).expect("host a");
1218	        let b_id = Identity::generate();
1219	        let b = NetHost::start(hermetic(&b_id)).expect("host b");
1220
1221	        let (conn_id, remote) = a.dial(b.addr()).expect("dial");
1222	        assert_eq!(conn_id, 1, "first broker-minted conn id");
1223	        assert_eq!(
1224	            remote,
1225	            b_id.public_key().to_hex(),
1226	            "handshake proves the peer"
1227	        );
1228	        assert_eq!(a.conn_count(), 1);
1229
1230	        // The acceptor's loop registers the inbound connection (async — poll).
1231	        for _ in 0..400 {
1232	            if b.conn_count() == 1 {
1233	                break;
1234	            }
1235	            std::thread::sleep(std::time::Duration::from_millis(5));
1236	        }
1237	        assert_eq!(b.conn_count(), 1, "accept loop registered the inbound conn");
1238	        assert_eq!(
1239	            b.conn_remote_hex(1).as_deref(),
1240	            Some(a.node_id_hex().as_str()),
1241	            "acceptor sees the dialer's identity"
1242	        );
1243	    }
1244
1245	    // [unit->REQ-RC-1] the broker loopback conn (M12 W1.5, B1): `dial_loopback`
1246	    // mints a REUSED singleton whose origin is this node's OWN id, and
1247	    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a
1248	    // peer row (the one the dispatcher serves) — with the operator's writes
1249	    // crossing to the peer row's read pump. No iroh handshake, no QUIC.
1250	    #[test]
1251	    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {
1252	        let id = Identity::generate();
1253	        let host = NetHost::start(hermetic(&id)).expect("host start");
1254
1255	        // Singleton: reused, own-node origin (never a foreign/handshake id).
1256	        let (c1, hex1) = host.dial_loopback();
1257	        let (c2, hex2) = host.dial_loopback();
1258	        assert_eq!(c1, c2, "the loopback conn is a reused singleton");
1259	        assert_eq!(
1260	            hex1,
1261	            id.public_key().to_hex(),
1262	            "loopback origin is THIS node's own id (broker-minted)"
1263	        );
1264	        assert_eq!(hex2, hex1);
1265	        assert_eq!(host.conn_count(), 1, "one loopback conn held");
1266
1267	        // open_stream mints the cross-wired pair: operator row + peer row.
1268	        let op_stream = host.open_stream(c1).expect("open loopback stream");
1269	        let infos = host.stream_infos();
1270	        assert_eq!(infos.len(), 2, "operator + peer rows");
1271	        let op = infos
1272	            .iter()
1273	            .find(|s| s.stream_id == op_stream)
1274	            .expect("operator row present");
1275	        assert!(op.initiated_locally, "the returned id is the operator row");
1276	        let peer = infos
1277	            .iter()
1278	            .find(|s| !s.initiated_locally)
1279	            .expect("peer row present");
1280	        assert_eq!(peer.conn_id, c1);
1281	        assert_eq!(
1282	            peer.remote_id_hex, hex1,
1283	            "the peer row carries the own-node origin (gates same-node)"
1284	        );
1285
1286	        // Cross-wire: a write on the operator side reaches the peer row's read
1287	        // pump (its log advances) — entirely in-process, no QUIC.
1288	        host.send_stream(op_stream, b"ping", false)
1289	            .expect("write operator side");
1290	        let peer_id = peer.stream_id;
1291	        let mut crossed = false;
1292	        for _ in 0..400 {
1293	            if host
1294	                .stream_infos()
1295	                .iter()
1296	                .any(|s| s.stream_id == peer_id && s.next_seq > 0)
1297	            {
1298	                crossed = true;
1299	                break;
1300	            }
1301	            std::thread::sleep(std::time::Duration::from_millis(5));
1302	        }
1303	        assert!(
1304	            crossed,
1305	            "the operator-side write crossed in-process to the peer row's read pump"
1306	        );
1307	    }
1308
1309	    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell
1310	    // tunnel) never evicts — `drain` returns every appended byte in order even past
1311	    // the would-be cap (lossless, CONTEXT:262 "cannot drop frames"); an ordinary
1312	    // (non-retentive) log keeps bounded-overwrite (a slow brain must not OOM the
1313	    // broker), evicting the oldest chunks past `cap_chunks`.
1314	    #[test]
1315	    fn retentive_log_is_lossless_ordered_while_ordinary_log_overwrites() {
1316	        // Retentive: cap 3, append 8 distinct chunks, NONE evicted — drain returns
1317	        // all 8 concatenated in order.
1318	        let mut ret = StreamLog::new(1, 1);
1319	        ret.set_retentive(3);
1320	        for i in 0..8u8 {
1321	            ret.append(&[i]);
1322	        }
1323	        let (bytes, finished) = ret.drain();
1324	        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), "retentive loses nothing, in order");
1325	        assert!(!finished);
1326	        // …and a second drain is empty (the cursor consumed them exactly once).
1327	        assert_eq!(ret.drain().0, Vec::<u8>::new());
1328
1329	        // Ordinary: cap 3, append 8 — the oldest evict, only the last 3 survive
1330	        // (the bounded-overwrite discipline ordinary streams keep).
1331	        let mut ord = StreamLog::new(2, 3);
1332	        for i in 0..8u8 {
1333	            ord.append(&[i]);
1334	        }
1335	        assert_eq!(ord.drain().0, vec![5, 6, 7], "ordinary keeps only the last cap chunks");
1336	    }
1337
1338	    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,
1339	    // doyle gate condition): a retentive shell-side stream that is not drained while
1340	    // the owner keeps writing must NOT lose bytes AND must NOT grow unbounded — the
1341	    // read pump lags the sender at the cap. After the reader catches up, every byte
1342	    // arrives byte-exact and in order. Proves "lag, never loss".
1343	    #[test]
1344	    fn loopback_tunnel_backpressures_without_loss() {
1345	        use std::sync::atomic::{AtomicUsize, Ordering as AtOrd};
1346	        let id = Identity::generate();
1347	        let host = Arc::new(NetHost::start(hermetic(&id)).expect("host start"));
1348	        let (owner, shell) = host.open_loopback_pair().expect("open tunnel pair");
1349	        const CAP: usize = 4;
1350	        host.mark_stream_retentive(owner, CAP);
1351	        host.mark_stream_retentive(shell, CAP);
1352
1353	        // Distinct, read-chunk-sized frames so each lands as its own ring entry
1354	        // (no coalescing) — lets us observe the cap exactly. 20 frames >> CAP.
1355	        const N: usize = 20;
1356	        let frames: Vec<Vec<u8>> = (0..N).map(|i| vec![i as u8; STREAM_READ_CHUNK]).collect();
1357	        let sent = Arc::new(AtomicUsize::new(0));
1358
1359	        let writer = {
1360	            let host = Arc::clone(&host);
1361	            let frames = frames.clone();
1362	            let sent = Arc::clone(&sent);
1363	            std::thread::spawn(move || {
1364	                for f in &frames {
1365	                    host.send_stream(owner, f, false).expect("send");
1366	                    sent.fetch_add(1, AtOrd::Relaxed);
1367	                }
1368	                host.send_stream(owner, &[], true).expect("finish");
1369	            })
1370	        };
1371
1372	        // Let the pump fill the shell ring to the cap and PARK (backpressure).
1373	        let mut peak = 0usize;
1374	        for _ in 0..200 {
1375	            if let Some(d) = host.ring_len(shell) {
1376	                peak = peak.max(d);
1377	            }
1378	            std::thread::sleep(std::time::Duration::from_millis(5));
1379	            if sent.load(AtOrd::Relaxed) >= N {
1380	                break; // (shouldn't reach N while we never drain — backpressure)
1381	            }
1382	        }
1383	        assert!(peak > 0, "the pump delivered some frames to the shell ring");
1384	        assert!(
1385	            host.ring_len(shell).unwrap() <= CAP,
1386	            "the retentive ring never exceeds the backpressure cap (bounded)"
1387	        );
1388	        assert!(
1389	            sent.load(AtOrd::Relaxed) < N,
1390	            "the un-drained consumer lags the sender (backpressure engaged, not all sent)"
1391	        );
1392
1393	        // Now drain to completion — the sender unblocks, every byte arrives in order.
1394	        let mut got: Vec<u8> = Vec::new();
1395	        let mut finished = false;
1396	        for _ in 0..2000 {
1397	            if let Some((mut bytes, fin)) = host.drain_stream(shell) {
1398	                got.append(&mut bytes);
1399	                if fin {
1400	                    finished = true;
1401	                    break;
1402	                }
1403	            }
1404	            std::thread::sleep(std::time::Duration::from_millis(2));
1405	        }
1406	        writer.join().expect("writer joins");
1407	        assert!(finished, "the tunnel saw EOF after the owner finished");
1408	        let expected: Vec<u8> = frames.concat();
1409	        assert_eq!(got.len(), expected.len(), "no byte lost or duplicated");
1410	        assert_eq!(got, expected, "byte-exact, in order, under backpressure");
1411	    }
1412	}
1413

//! The broker-owned network host (D4a) — ADR-0004 §B made real for the wire.
//!
//! [`NetHost`] is the broker's ownership of the node's live network presence:
//! the iroh endpoint ([`spt_net::net::endpoint::NetEndpoint`]) and every
//! accepted/dialed QUIC connection. A live cross-node connection is exactly the
//! kind of resource ADR-0004 §B assigns to the broker — a consumer would lose
//! continuity on brain restart — so the endpoint is constructed *here*, in the
//! stable kernel, never in the brain. The brain drives net operations over the
//! versioned IPC ([`crate::msg::KIND_NET_STATUS`] / [`crate::msg::KIND_NET_DIAL`])
//! and may detach/reattach freely; connections persist in the broker's table.
//!
//! ## Runtime placement (M4-D4-PLAN locked decision)
//!
//! The broker is otherwise sync/thread-based; iroh requires tokio. So the host
//! owns a **dedicated multi-thread tokio runtime** on named worker threads
//! (`spt-broker-net`): QUIC objects live in structures only the runtime's tasks
//! and the host's own `block_on` bridges touch, and the runtime drops with the
//! host (broker teardown stops all net tasks). Spike #3's broker binary proved
//! exactly this shape — a broker-owned endpoint surviving brain kill/restart.
//!
//! ## Ownership closes two §B rows by construction
//!
//! `MdnsAddressLookup` and the relay session are both constructed *inside* the
//! iroh endpoint, so broker ownership of the endpoint closes the ADR-0004 §B
//! "mDNS socket / relay session" row with no extra machinery: there is no
//! separate socket to own.
// [impl->REQ-DAEMON-2]

use std::collections::{HashMap, HashSet, VecDeque};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream, ReadHalf, WriteHalf};

use spt_net::net::endpoint::{
    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,
    SendStream, SPT_NET_ALPN,
};
use spt_net::net::mesh::seedproof::ProofRole;
use spt_proto::identity::{Identity, PublicKey};

use crate::broker::SharedSend;
use crate::codec::write_frame;
use crate::seedproofx::{prove_membership, MembershipSource, RosterExchange};
use crate::msg::{
    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,
    NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DISCONNECTED,
};

/// The reserved [`crate::effect::EffectKey`] session namespace for net-scoped
/// effects (a dial has no PTY session). Broker session ids are minted from 1
/// (`Broker::next_id` starts at 1), so `0` can never collide with a real session.
pub const NET_EFFECT_SESSION: u64 = 0;

/// Everything needed to stand the broker's endpoint up: the node's identity
/// (the endpoint *is* the node's name — EndpointId == pubkey), the relay stance
/// (from `daemon.json` in production; [`RelayPolicy::Disabled`] in hermetic
/// tests), and whether mDNS LAN discovery rides along.
pub struct NetConfig {
    /// The node identity the endpoint binds as (seed → same ed25519 keypair).
    pub identity: Identity,
    /// Relay stance (ADR-0002): n0 default / self-host / disabled.
    pub relay: RelayPolicy,
    /// mDNS LAN discovery on/off (off in hermetic tests).
    pub discovery: LocalDiscovery,
    /// Socket bind scope: [`BindScope::All`] in production;
    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,
    /// no per-test-binary Windows firewall prompt).
    pub scope: BindScope,
    /// Live source of this node's subnet credentials for the connect-time
    /// seed-proof (Mesh-D2, REQ-MESH-1). `Some` enables the mutual proof at
    /// every dial + accept (fail → drop); `None` disables it entirely — both
    /// peers must agree, or the side expecting the control stream would hang.
    /// Production wires `Some(SubnetStore::load …)`; mechanics-only tests that
    /// don't exercise membership pass `None`.
    pub membership: Option<MembershipSource>,
}

/// Default bounded stream-read ring depth (chunks) — same sizing rationale as
/// the PTY `OutputLog`: large enough that a brain-restart window never evicts
/// unreplayed chunks in practice; bounded so an unsubscribed stream can't
/// buffer without limit.
const DEFAULT_STREAM_RING_CHUNKS: usize = 4096;

/// Max bytes pulled per stream read (one ring chunk).
const STREAM_READ_CHUNK: usize = 64 * 1024;

/// Transport backing a held connection. A real `Quic` connection (the only
/// kind an inbound handshake can produce), or a broker-minted `Loopback` —
/// an in-process connection with NO iroh handshake whose identity IS this node,
/// established BY CONSTRUCTION. A loopback conn can ONLY be minted broker-side
/// via [`NetHost::dial_loopback`]; it can never arise from an inbound handshake
/// (a remote always presents its own key and cannot claim to be us). This is
/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12
/// W1.5, B1) — QUIC self-dial is refused by iroh ("Connecting to ourself is not
/// supported"), so local attach rides a broker loopback conn instead.
enum ConnKind {
    /// A real QUIC connection — kept alive by ownership here; dropping it closes
    /// the connection. Cloned (cheap handle) for stream opens.
    Quic(Connection),
    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its
    /// streams are minted eagerly in cross-wired pairs by [`NetHost::open_stream`];
    /// there is no accept loop and no closed-watcher (it lives for the host).
    Loopback,
}

/// One live connection the broker holds (QUIC or loopback).
struct ConnEntry {
    /// The transport backing this connection.
    kind: ConnKind,
    /// The remote node's stable name (hex pubkey). For QUIC, proven by the
    /// handshake; for loopback, THIS node's own id (the broker-minted identity).
    remote_id_hex: String,
    /// The subnets the peer proved current-epoch seed-knowledge of at connect
    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:
    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;
    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run
    /// in parallel — belt + suspenders, no regression).
    proven_subnets: HashSet<String>,
}

/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`
/// discipline applied to the network (D4b): the stream's single read pump is
/// the only producer; a brain connection attaches as the subscriber; every
/// subscriber send happens under the enclosing `Mutex`, so live chunks and a
/// replay can never interleave out of `seq` order. Chunks produced while no
/// brain is attached sit in the ring and replay on the next subscribe — a
/// brain restart loses nothing (Spike #3 invariant B/D).
// [impl->REQ-DAEMON-2]
struct StreamLog {
    stream_id: u64,
    next_seq: u64,
    ring: VecDeque<(u64, Vec<u8>)>,
    cap_chunks: usize,
    subscriber: Option<SharedSend>,
    /// Read side finished cleanly (peer called finish / conn ended).
    finished: bool,
    /// **Retentive backpressure cap** (M11-W3, REQ-SHELL-4), in chunks; `0` ⇒ not
    /// retentive (ordinary bounded-overwrite stream). A shell-tunnel stream must
    /// NOT drop bytes — CONTEXT:262 "tunneled protocols cannot drop frames, so
    /// congestion surfaces as lag, never loss." So a retentive log never cap-evicts
    /// on append (no loss), and the read pump REFUSES to pull the next chunk once
    /// the undrained ring reaches this cap (no unbounded growth ⇒ no OOM) — the
    /// transport's native flow-control then lags the *sender*. A chunk leaves the
    /// ring ONLY when [`drain`](Self::drain) (the recv cursor) pops it; the pump is
    /// woken to resume via the entry's `room` notifier. Ordinary streams keep
    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an
    /// unbounded ring) and are drained by subscribe, never the sync cursor.
    retentive_cap: usize,
}

impl StreamLog {
    fn new(stream_id: u64, cap_chunks: usize) -> Self {
        StreamLog {
            stream_id,
            next_seq: 0,
            ring: VecDeque::new(),
            cap_chunks,
            subscriber: None,
            finished: false,
            retentive_cap: 0,
        }
    }

    /// Mark this log retentive with backpressure `cap` (chunks). Idempotent set.
    fn set_retentive(&mut self, cap: usize) {
        self.retentive_cap = cap;
    }

    /// Append a freshly-read chunk: assign the next `seq`, retain it in the ring,
    /// live-send to the current subscriber. Read-pump only. A non-retentive log
    /// cap-evicts the oldest chunks past `cap_chunks`; a retentive (tunnel) log
    /// never evicts — bytes leave only via [`drain`](Self::drain).
    fn append(&mut self, bytes: &[u8]) {
        let seq = self.next_seq;
        self.next_seq += 1;
        self.ring.push_back((seq, bytes.to_vec()));
        if self.retentive_cap == 0 {
            while self.ring.len() > self.cap_chunks {
                self.ring.pop_front();
            }
        }
        if let Some(sub) = &self.subscriber {
            let frame = net_stream_data_envelope(self.stream_id, seq, bytes);
            if let Ok(mut s) = sub.lock() {
                let _ = write_frame(&mut *s, &frame);
            }
        }
    }

    /// Drain every undrained chunk in order (the shell-tunnel recv cursor, W3).
    /// Pops the whole ring front-to-back (FIFO by `seq`) and concatenates the
    /// bytes — chunk boundaries are arbitrary read-pump splits, irrelevant to an
    /// opaque byte stream. Lossless + ordered: a byte leaves the ring only here,
    /// once. Returns `(bytes, finished)`; `finished` reflects a clean/torn end so
    /// the poller sees EOF and closes. Empty bytes with `finished=false` ⇒ nothing
    /// pending yet.
    fn drain(&mut self) -> (Vec<u8>, bool) {
        let mut out = Vec::new();
        while let Some((_, chunk)) = self.ring.pop_front() {
            out.extend_from_slice(&chunk);
        }
        (out, self.finished)
    }

    /// Mark the read side finished and tell the subscriber (with the final
    /// cursor, so it can verify completeness).
    fn finish(&mut self) {
        self.finished = true;
        if let Some(sub) = &self.subscriber {
            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);
            if let Ok(mut s) = sub.lock() {
                let _ = write_frame(&mut *s, &frame);
            }
        }
    }

    /// Attach `sub` as the subscriber, replaying every retained chunk with
    /// `seq >= from_seq` (then eof, if already finished) before live streaming.
    fn attach(&mut self, sub: SharedSend, from_seq: u64) {
        self.subscriber = Some(Arc::clone(&sub));
        for (seq, bytes) in self.ring.iter().filter(|(s, _)| *s >= from_seq) {
            let frame = net_stream_data_envelope(self.stream_id, *seq, bytes);
            if let Ok(mut s) = sub.lock() {
                let _ = write_frame(&mut *s, &frame);
            }
        }
        if self.finished {
            let frame = net_stream_eof_envelope(self.stream_id, self.next_seq);
            if let Ok(mut s) = sub.lock() {
                let _ = write_frame(&mut *s, &frame);
            }
        }
    }

    /// Detach `sub` iff it is the current subscriber (a late disconnect must
    /// not unsubscribe a newer brain).
    fn detach_if(&mut self, sub: &SharedSend) {
        if let Some(cur) = &self.subscriber {
            if Arc::ptr_eq(cur, sub) {
                self.subscriber = None;
            }
        }
    }
}

/// A bounded, sequenced log of connection liveness events — the broker-owned
/// PresenceChannel seam (D4c, REQ-EP-4): the StreamLog ring/cursor discipline
/// applied to the conn table itself. `register_conn` appends `connected`; each
/// connection's closed-watcher appends `disconnected` after removing the row.
/// A brain subscribes with a cursor and replays events produced while it was
/// down, so presence survives a brain restart exactly like stream reads do.
// [impl->REQ-EP-4]
struct PresenceLog {
    next_seq: u64,
    ring: VecDeque<NetPresenceEvent>,
    cap_events: usize,
    subscriber: Option<SharedSend>,
}

impl PresenceLog {
    fn new(cap_events: usize) -> Self {
        PresenceLog {
            next_seq: 0,
            ring: VecDeque::new(),
            cap_events,
            subscriber: None,
        }
    }

    /// Append a liveness event: assign the next `seq`, retain it in the bounded
    /// ring, live-send to the current subscriber. Producers: `register_conn`
    /// (connected) and the per-conn closed-watchers (disconnected) — every
    /// append serializes on the enclosing `Mutex`, so subscriber frames arrive
    /// in strict `seq` order.
    fn append(&mut self, kind: &str, conn_id: u64, remote_id_hex: &str) {
        let ev = NetPresenceEvent {
            seq: self.next_seq,
            kind: kind.to_string(),
            conn_id,
            remote_id_hex: remote_id_hex.to_string(),
        };
        self.next_seq += 1;
        self.ring.push_back(ev.clone());
        while self.ring.len() > self.cap_events {
            self.ring.pop_front();
        }
        if let Some(sub) = &self.subscriber {
            let frame = net_presence_event_envelope(&ev);
            if let Ok(mut s) = sub.lock() {
                let _ = write_frame(&mut *s, &frame);
            }
        }
    }

    /// Attach `sub` as the subscriber, replaying every retained event with
    /// `seq >= from_seq` before live streaming (the gapless-resume attach).
    fn attach(&mut self, sub: SharedSend, from_seq: u64) {
        self.subscriber = Some(Arc::clone(&sub));
        for ev in self.ring.iter().filter(|e| e.seq >= from_seq) {
            let frame = net_presence_event_envelope(ev);
            if let Ok(mut s) = sub.lock() {
                let _ = write_frame(&mut *s, &frame);
            }
        }
    }

    /// Detach `sub` iff it is the current subscriber (a late disconnect must
    /// not unsubscribe a newer brain).
    fn detach_if(&mut self, sub: &SharedSend) {
        if let Some(cur) = &self.subscriber {
            if Arc::ptr_eq(cur, sub) {
                self.subscriber = None;
            }
        }
    }
}

/// A stream's send half — the same two transports as [`ConnKind`]. The
/// `serve_attach`/operator pump and the `StreamLog` read discipline are
/// IDENTICAL across both; only this leaf write differs (QUIC stream vs the
/// write end of an in-process duplex). That is the M12 W1.5 one-pump invariant
/// made structural: a loopback attach and a cross-node attach diverge at this
/// leaf and nowhere above it.
enum SendHalf {
    Quic(SendStream),
    Loopback(WriteHalf<DuplexStream>),
}

impl SendHalf {
    /// Write all `bytes` (no-op on empty). Runs on the host runtime.
    async fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
        match self {
            SendHalf::Quic(s) => s
                .write_all(bytes)
                .await
                .map_err(|e| io::Error::other(e.to_string())),
            SendHalf::Loopback(w) => w.write_all(bytes).await,
        }
    }

    /// Close the send side so the peer's read pump sees a clean end (QUIC
    /// `finish` / duplex `shutdown`).
    async fn finish(&mut self) -> io::Result<()> {
        match self {
            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),
            SendHalf::Loopback(w) => w.shutdown().await,
        }
    }
}

/// A stream's read half — fed into the single read pump in [`register_stream`].
/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.
/// Both append to the same [`StreamLog`], so the subscriber (and everything
/// above it) cannot tell the transports apart.
enum RecvHalf {
    Quic(RecvStream),
    Loopback(ReadHalf<DuplexStream>),
}

/// One live stream the broker holds (QUIC or loopback): the send half (brain
/// writes ride it over IPC) + the sequenced read log.
struct StreamEntry {
    conn_id: u64,
    remote_id_hex: String,
    /// Whether THIS node opened the stream (`open_stream`) vs the peer
    /// (accept loop). The inbound dispatcher (D9-1) claims only
    /// peer-initiated streams; without this fact the table is ambiguous —
    /// it holds locally-opened outbound streams too (gotcha: a dispatcher
    /// that served its own requester stream would deadlock on itself).
    initiated_locally: bool,
    /// The send half, serialized behind an async mutex (writes happen on the
    /// host's runtime via `block_on`).
    send: tokio::sync::Mutex<SendHalf>,
    log: Arc<Mutex<StreamLog>>,
    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below
    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].
    /// Unused by ordinary (non-retentive) streams.
    room: Arc<tokio::sync::Notify>,
}

/// Tables shared between the host's sync surface and its runtime tasks
/// (accept loop, per-conn stream acceptors, per-stream read pumps).
struct NetShared {
    next_conn_id: AtomicU64,
    conns: Mutex<HashMap<u64, ConnEntry>>,
    next_stream_id: AtomicU64,
    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,
    ring_cap: usize,
    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.
    presence: Mutex<PresenceLog>,
}

impl NetShared {
    fn new() -> Self {
        NetShared {
            next_conn_id: AtomicU64::new(1),
            conns: Mutex::new(HashMap::new()),
            next_stream_id: AtomicU64::new(1),
            streams: Mutex::new(HashMap::new()),
            ring_cap: DEFAULT_STREAM_RING_CHUNKS,
            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),
        }
    }
}

/// Register a live connection: mint its broker id and spawn its inbound-stream
/// acceptor. Must run inside the host's runtime context (the accept loop, or a
/// `block_on` future) — it spawns tasks.
fn register_conn(
    shared: &Arc<NetShared>,
    conn: Connection,
    proven_subnets: HashSet<String>,
) -> (u64, String) {
    let remote_id_hex = PublicKey::from_bytes(conn.remote_id().as_bytes())
        .expect("an iroh EndpointId is always a valid ed25519 public key")
        .to_hex();
    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);
    shared.conns.lock().unwrap().insert(
        id,
        ConnEntry {
            kind: ConnKind::Quic(conn.clone()),
            remote_id_hex: remote_id_hex.clone(),
            proven_subnets,
        },
    );

    // Presence: announce the row BEFORE spawning the closed-watcher, so the
    // `connected` event can never race behind its own `disconnected` (D4c).
    shared
        .presence
        .lock()
        .unwrap()
        .append(PRESENCE_CONNECTED, id, &remote_id_hex);

    // Closed-watcher: the row's exit path. Awaits the connection's end (clean
    // close, conn loss, or our own endpoint close), removes the row — conns
    // finally LEAVE the table — then appends the `disconnected` event (row
    // gone before the event is observable).
    // [impl->REQ-EP-4]
    let sh_close = Arc::clone(shared);
    let conn_close = conn.clone();
    let hex_close = remote_id_hex.clone();
    tokio::spawn(async move {
        conn_close.closed().await;
        sh_close.conns.lock().unwrap().remove(&id);
        sh_close
            .presence
            .lock()
            .unwrap()
            .append(PRESENCE_DISCONNECTED, id, &hex_close);
    });

    // Inbound-stream acceptor: every peer-opened bidi stream joins the table
    // and gets a read pump. (QUIC streams are lazy — the peer's open becomes
    // visible here on its first write.) Ends when the connection does.
    let sh = Arc::clone(shared);
    let hex = remote_id_hex.clone();
    tokio::spawn(async move {
        while let Ok((send, recv)) = conn.accept_bi().await {
            register_stream(
                &sh,
                id,
                &hex,
                SendHalf::Quic(send),
                RecvHalf::Quic(recv),
                false,
            );
        }
    });

    (id, remote_id_hex)
}

/// Register a live stream (opened or accepted): mint its broker id and spawn
/// its read pump. Must run inside the host's runtime context.
/// `initiated_locally` records the direction fact for the table (true only
/// from [`NetHost::open_stream`]).
fn register_stream(
    shared: &Arc<NetShared>,
    conn_id: u64,
    remote_id_hex: &str,
    send: SendHalf,
    recv: RecvHalf,
    initiated_locally: bool,
) -> u64 {
    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);
    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));
    let room = Arc::new(tokio::sync::Notify::new());
    shared.streams.lock().unwrap().insert(
        id,
        Arc::new(StreamEntry {
            conn_id,
            remote_id_hex: remote_id_hex.to_string(),
            initiated_locally,
            send: tokio::sync::Mutex::new(send),
            log: Arc::clone(&log),
            room: Arc::clone(&room),
        }),
    );

    // The read pump — the stream's single producer. A clean end (peer finish)
    // and a torn end (conn lost) both finish the log; the buffered chunks stay
    // replayable either way. The transport differs only at this read leaf
    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the
    // same `StreamLog`, so nothing above the pump can tell them apart.
    //
    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,
    // so before pulling the next chunk the pump WAITS while the undrained ring is
    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so
    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the
    // SENDER — "lag, never loss" (CONTEXT:262). A `drain` wakes `room`. Ordinary
    // streams have `retentive_cap == 0`, so this gate is a no-op for them.
    tokio::spawn(async move {
        match recv {
            RecvHalf::Quic(mut recv) => loop {
                await_ring_room(&log, &room).await;
                match recv.read_chunk(STREAM_READ_CHUNK).await {
                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),
                    Ok(None) | Err(_) => {
                        log.lock().unwrap().finish();
                        break;
                    }
                }
            },
            RecvHalf::Loopback(mut recv) => {
                let mut buf = vec![0u8; STREAM_READ_CHUNK];
                loop {
                    await_ring_room(&log, &room).await;
                    match recv.read(&mut buf).await {
                        Ok(0) | Err(_) => {
                            log.lock().unwrap().finish();
                            break;
                        }
                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
                    }
                }
            }
        }
    });

    id
}

/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained
/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for
/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.
// [impl->REQ-SHELL-4]
async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {
    loop {
        {
            let l = log.lock().unwrap();
            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {
                return;
            }
        }
        room.notified().await;
    }
}

/// The broker-owned net endpoint + live connection/stream tables (see module
/// docs).
pub struct NetHost {
    /// The dedicated tokio runtime hosting the endpoint and its accept loop.
    /// Dropping the host drops the runtime, stopping every net task with it.
    runtime: tokio::runtime::Runtime,
    endpoint: Arc<NetEndpoint>,
    shared: Arc<NetShared>,
    /// `op_id → conn_id` for exactly-once dial replies: a deduped replay of a
    /// journaled dial is answered with the connection the first apply opened.
    dial_ops: Mutex<HashMap<u64, u64>>,
    /// `op_id → stream_id` for exactly-once stream-open replies (D4b), same
    /// discipline as `dial_ops`. Shares the one net op-id namespace.
    stream_ops: Mutex<HashMap<u64, u64>>,
    /// This node's pubkey (the seed-proof's local channel-binding identity).
    local_id: PublicKey,
    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables
    /// the proof (see [`NetConfig::membership`]).
    membership: Option<MembershipSource>,
    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live
    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`
    /// so the many hermetic test configs need no change — a roster exchange is a
    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +
    /// cloned per connect on the net runtime, so the accept loop (spawned in
    /// `start`, before the host exists) shares it through this `Arc`.
    roster: Arc<Mutex<Option<RosterExchange>>>,
    /// The broker's single in-process loopback connection (M12 W1.5, B1),
    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
    /// row lives for the host (no handshake to lose, no closed-watcher).
    loopback_conn: Mutex<Option<u64>>,
    /// Serializes [`Self::open_loopback_pair`] (M11-W3) so the peer-row discovery
    /// (a before/after diff on the loopback conn's inbound rows) sees exactly the
    /// one row this open minted — concurrent pair-opens on the shared loopback conn
    /// would otherwise race the diff.
    loopback_pair_lock: Mutex<()>,
}

impl NetHost {
    /// Bind the broker's endpoint on a fresh dedicated runtime and start the
    /// accept loop. The endpoint speaks TWO ALPNs (M7 D3, REQ-SUBNET-2): the
    /// trusted [`SPT_NET_ALPN`] (connections join the table as before) and
    /// the pre-trust pairing ALPN — routed to the always-on responder
    /// ([`crate::pairhost::respond`]), never registered in the stream table.
    /// One endpoint for both is deliberate: the pairing transcript binds and
    /// pins the REAL node pubkey, and the relay can only route one endpoint
    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).
    /// The meet-rotation listener loop rides the same runtime.
    // [impl->REQ-DAEMON-2]
    // [impl->REQ-SUBNET-2]
    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {
        // Hermeticity guard (REQ-PAIR-8): a loopback-scoped endpoint is the
        // "no network reached" promise — the ceremony clock's lazy NTP query
        // must honor it (tests must never probe a real NTP server). Real
        // scopes keep the decision-18 offset correction.
        spt_net::net::pairing::ntp::set_enabled(!matches!(cfg.scope, BindScope::Loopback));
        let runtime = tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .thread_name("spt-broker-net")
            .enable_all()
            .build()?;
        let endpoint = runtime
            .block_on(NetEndpoint::bind_scoped(
                &cfg.identity,
                cfg.relay.clone(),
                cfg.discovery,
                vec![
                    SPT_NET_ALPN.to_vec(),
                    spt_net::net::pairing::wire::SPT_PAIR_ALPN.to_vec(),
                ],
                cfg.scope,
            ))
            .map_err(|e| io::Error::other(e.to_string()))?;
        let endpoint = Arc::new(endpoint);
        let shared = Arc::new(NetShared::new());
        let local_pub = cfg.identity.public_key();
        let pair_rate = Arc::new(tokio::sync::Mutex::new(
            spt_net::net::pairing::ratelimit::PairingRateLimiter::new(),
        ));

        // Accept loop: lives for the endpoint's lifetime on the host's runtime.
        // Each handshake completes on its own task so a slow peer can't stall
        // the loop; a failed handshake is simply not registered. The pairing
        // ALPN routes to the responder; everything else is the trusted table.
        let roster = Arc::new(Mutex::new(None::<RosterExchange>));
        let ep = Arc::clone(&endpoint);
        let sh = Arc::clone(&shared);
        let rate = Arc::clone(&pair_rate);
        let accept_membership = cfg.membership.clone();
        let accept_roster = Arc::clone(&roster);
        let accept_ep = Arc::clone(&endpoint);
        runtime.spawn(async move {
            while let Some(incoming) = ep.accept().await {
                let sh = Arc::clone(&sh);
                let rate = Arc::clone(&rate);
                let membership = accept_membership.clone();
                // Snapshot the (opt-in) roster exchange + our live address for
                // this connect; the leg runs after the proof verifies.
                let roster_rx = accept_roster.lock().unwrap().clone();
                let self_addr =
                    serde_json::to_value(accept_ep.addr()).unwrap_or(serde_json::Value::Null);
                tokio::spawn(async move {
                    let Ok(connecting) = incoming.accept() else {
                        return;
                    };
                    let Ok(conn) = connecting.await else { return };
                    if conn.alpn() == spt_net::net::pairing::wire::SPT_PAIR_ALPN {
                        crate::pairhost::respond(conn, local_pub, rate).await;
                    } else {
                        // Mesh-D2: prove membership in-band BEFORE the connection
                        // joins the table. A non-prover (no shared subnet / bad
                        // seed) is dropped — the conn falls out of scope and QUIC
                        // closes it. With membership disabled, register as before.
                        // Mesh-D4: on success, a roster leg rides the same stream.
                        let proven = match membership {
                            Some(src) => {
                                let creds = src();
                                match prove_membership(
                                    &conn,
                                    local_pub,
                                    ProofRole::Acceptor,
                                    &creds,
                                    roster_rx.as_ref(),
                                    &self_addr,
                                )
                                .await
                                {
                                    Some(set) => set,
                                    None => return, // drop the connection
                                }
                            }
                            None => HashSet::new(),
                        };
                        register_conn(&sh, conn, proven);
                    }
                });
            }
        });

        // The meet-rotation loop (M7 D3): how a joiner that knows only
        // (subnet name, code) finds this responder. Idles with no subnets.
        crate::pairhost::spawn_meet_rotation(
            runtime.handle(),
            Arc::clone(&endpoint),
            cfg.relay,
            cfg.discovery,
            cfg.scope,
        );

        // The limiter Arc lives on in the accept loop; the host keeps no
        // handle (per-ceremony state is the responder's, not the table's).
        drop(pair_rate);

        Ok(NetHost {
            runtime,
            endpoint,
            shared,
            dial_ops: Mutex::new(HashMap::new()),
            stream_ops: Mutex::new(HashMap::new()),
            local_id: local_pub,
            membership: cfg.membership,
            roster,
            loopback_conn: Mutex::new(None),
            loopback_pair_lock: Mutex::new(()),
        })
    }

    /// Wire the on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2) onto this
    /// live host. The daemon calls this once after `start` with
    /// [`crate::seedproofx::production_roster_exchange`]; tests inject their own.
    /// Off `NetConfig` by design — see the `roster` field. Takes effect on the
    /// next dial/accept (each connect snapshots it).
    // [impl->REQ-MESH-2]
    pub fn set_roster_exchange(&self, exchange: RosterExchange) {
        *self.roster.lock().unwrap() = Some(exchange);
    }

    /// Run the joiner leg of the pairing ceremony (M7 D3, REQ-SUBNET-2) on
    /// the host's runtime — the broker IPC `pair-join` arm. The ceremony
    /// must run HERE because it binds and pins this endpoint's real node
    /// pubkey. Returns the rendered trust outcome, or the classified failure.
    // [impl->REQ-SUBNET-2]
    pub fn pair_join(
        &self,
        subnet: &str,
        code: &str,
        responder_addr: Option<EndpointAddr>,
    ) -> Result<String, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }

    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).
    pub fn node_id_hex(&self) -> String {
        self.endpoint.node_id_hex()
    }

    /// The endpoint's dialable address (id + currently-known direct/relay paths).
    pub fn addr(&self) -> EndpointAddr {
        self.endpoint.addr()
    }

    /// [`Self::addr`] as the opaque JSON the IPC status reply carries.
    pub fn addr_json(&self) -> serde_json::Value {
        serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null)
    }

    /// Live connections (accepted + dialed) currently held.
    pub fn conn_count(&self) -> u64 {
        self.shared.conns.lock().unwrap().len() as u64
    }

    /// Dial a peer over `SPT_NET_ALPN`, registering the connection in the table.
    /// Bridges sync→async via the host's own runtime (the broker's dispatch
    /// threads block here; the future runs on the net worker threads).
    // [impl->REQ-DAEMON-2]
    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {
        let endpoint = Arc::clone(&self.endpoint);
        let shared = Arc::clone(&self.shared);
        let local_id = self.local_id;
        let membership = self.membership.clone();
        // Snapshot the (opt-in) roster exchange + our live address for this dial.
        let roster_rx = self.roster.lock().unwrap().clone();
        let self_addr = serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null);
        self.runtime.handle().block_on(async move {
            let conn = endpoint
                .connect(addr)
                .await
                .map_err(|e| io::Error::other(e.to_string()))?;
            // Mesh-D2: prove membership in-band before the connection is usable.
            // A failed/empty proof drops the dial (the conn never registers).
            // Mesh-D4: on success, a roster leg rides the same control stream.
            let proven = match membership {
                Some(src) => {
                    let creds = src();
                    prove_membership(
                        &conn,
                        local_id,
                        ProofRole::Dialer,
                        &creds,
                        roster_rx.as_ref(),
                        &self_addr,
                    )
                    .await
                    .ok_or_else(|| {
                        io::Error::other("seed-proof failed: peer is not a subnet member")
                    })?
                }
                None => HashSet::new(),
            };
            Ok(register_conn(&shared, conn, proven))
        })
    }

    /// Record that journaled dial `op_id` opened `conn_id` (called inside the
    /// `apply_once` closure, so a concurrent deduped replay always finds it).
    pub fn record_dial_op(&self, op_id: u64, conn_id: u64) {
        self.dial_ops.lock().unwrap().insert(op_id, conn_id);
    }

    /// The connection a journaled dial `op_id` opened, if this broker process
    /// still holds it (a broker restart drops connections; the journal alone
    /// survives — see `Broker::dispatch_net_dial`).
    pub fn dial_op_conn(&self, op_id: u64) -> Option<u64> {
        self.dial_ops.lock().unwrap().get(&op_id).copied()
    }

    /// The remote node id (hex) of a held connection.
    pub fn conn_remote_hex(&self, conn_id: u64) -> Option<String> {
        self.shared
            .conns
            .lock()
            .unwrap()
            .get(&conn_id)
            .map(|e| e.remote_id_hex.clone())
    }

    /// The subnets the peer of `conn_id` proved current-epoch seed-knowledge of
    /// at connect (Mesh-D2, REQ-MESH-1). Empty when seed-proof was disabled or
    /// the conn is gone. The seam the D5 gate swap reads in place of
    /// `is_trusted(subnet, origin)`; unused by any gate at D2.
    // [impl->REQ-MESH-1]
    pub fn conn_proven_subnets(&self, conn_id: u64) -> HashSet<String> {
        self.shared
            .conns
            .lock()
            .unwrap()
            .get(&conn_id)
            .map(|e| e.proven_subnets.clone())
            .unwrap_or_default()
    }

    /// The OBSERVED peer address of a held connection (remote id + the
    /// connection's current path addresses) as opaque JSON — the address-
    /// seeding write-back the `net-dialed` reply carries (M8 decision 20,
    /// REQ-CONV-1). `null` when the conn is gone, path-less, or fails to
    /// serialize: the seed is an optimization, never an error path.
    // [impl->REQ-CONV-1]
    pub fn conn_remote_addr_json(&self, conn_id: u64) -> serde_json::Value {
        let conn = {
            let conns = self.shared.conns.lock().unwrap();
            match conns.get(&conn_id) {
                // A loopback conn has no path to observe — nothing to seed.
                Some(e) => match &e.kind {
                    ConnKind::Quic(c) => c.clone(),
                    ConnKind::Loopback => return serde_json::Value::Null,
                },
                None => return serde_json::Value::Null,
            }
        };
        let addr = spt_net::net::endpoint::conn_remote_addr(&conn);
        if addr.is_empty() {
            return serde_json::Value::Null; // id-only adds nothing over discovery
        }
        serde_json::to_value(addr).unwrap_or(serde_json::Value::Null)
    }

    /// Mint (or reuse) the broker's single in-process loopback connection
    /// (M12 W1.5, B1) and return `(conn_id, remote_id_hex)` — `remote_id_hex` is
    /// THIS node's own id, so a stream served over it gates same-node (access
    /// short-circuit Allow). The conn carries NO iroh handshake: its identity is
    /// broker-minted BY CONSTRUCTION and can never arise from an inbound dial.
    /// This is what lets a LOCAL `spt rc` ride the byte-identical `serve_attach`
    /// pump — QUIC self-dial is refused by iroh, so local attach uses this.
    /// Idempotent: every call returns the same singleton conn (one is all a
    /// local attach needs); no journaling needed (reuse is the dedup).
    // [impl->REQ-RC-1]
    pub fn dial_loopback(&self) -> (u64, String) {
        let remote_id_hex = self.local_id.to_hex();
        let mut slot = self.loopback_conn.lock().unwrap();
        if let Some(id) = *slot {
            return (id, remote_id_hex);
        }
        let id = self.shared.next_conn_id.fetch_add(1, Ordering::Relaxed);
        self.shared.conns.lock().unwrap().insert(
            id,
            ConnEntry {
                kind: ConnKind::Loopback,
                remote_id_hex: remote_id_hex.clone(),
                proven_subnets: HashSet::new(),
            },
        );
        *slot = Some(id);
        (id, remote_id_hex)
    }

    /// Open a bidi stream on a held connection, registering it (with its read
    /// pump) in the stream table (D4b). Note QUIC streams are lazy: the peer
    /// sees the stream on its first write.
    ///
    /// On a [`ConnKind::Loopback`] conn there is no peer to accept the stream,
    /// so the open mints a CROSS-WIRED PAIR in-process: an operator row
    /// (`initiated_locally = true`, the returned id) and a peer row
    /// (`initiated_locally = false`) the inbound dispatcher claims and serves via
    /// `serve_attach` — exactly as an `accept_bi` would surface a remote-opened
    /// stream. One `tokio::io::duplex` wires both directions; each row's read
    /// pump and `StreamLog` are the SAME machinery the QUIC path uses (the W1.5
    /// one-pump invariant). The peer row's `remote_id_hex` is this node's own id,
    /// so its serve gates same-node.
    // [impl->REQ-DAEMON-2]
    // [impl->REQ-RC-1]
    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {
        enum Backing {
            Quic(Connection),
            Loopback,
        }
        let (backing, remote_id_hex) = {
            let conns = self.shared.conns.lock().unwrap();
            let e = conns.get(&conn_id).ok_or_else(|| {
                io::Error::new(io::ErrorKind::NotFound, format!("no such conn {conn_id}"))
            })?;
            let backing = match &e.kind {
                ConnKind::Quic(c) => Backing::Quic(c.clone()),
                ConnKind::Loopback => Backing::Loopback,
            };
            (backing, e.remote_id_hex.clone())
        };
        let shared = Arc::clone(&self.shared);
        match backing {
            Backing::Quic(conn) => self.runtime.handle().block_on(async move {
                let (send, recv) = conn
                    .open_bi()
                    .await
                    .map_err(|e| io::Error::other(e.to_string()))?;
                Ok(register_stream(
                    &shared,
                    conn_id,
                    &remote_id_hex,
                    SendHalf::Quic(send),
                    RecvHalf::Quic(recv),
                    true,
                ))
            }),
            Backing::Loopback => self.runtime.handle().block_on(async move {
                // One duplex carries BOTH directions: operator writes reach the
                // peer's read pump and vice-versa. Split each end into a read +
                // write half so each row owns exactly its leg.
                let (op_io, peer_io) = tokio::io::duplex(STREAM_READ_CHUNK);
                let (op_r, op_w) = tokio::io::split(op_io);
                let (peer_r, peer_w) = tokio::io::split(peer_io);
                // Register the peer (inbound) row first so it is in the table
                // before the operator can write its Request onto the wire — the
                // dispatcher must never miss the stream.
                register_stream(
                    &shared,
                    conn_id,
                    &remote_id_hex,
                    SendHalf::Loopback(peer_w),
                    RecvHalf::Loopback(peer_r),
                    false,
                );
                Ok(register_stream(
                    &shared,
                    conn_id,
                    &remote_id_hex,
                    SendHalf::Loopback(op_w),
                    RecvHalf::Loopback(op_r),
                    true,
                ))
            }),
        }
    }

    /// Record that journaled open `op_id` minted `stream_id` (inside the
    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).
    pub fn record_stream_op(&self, op_id: u64, stream_id: u64) {
        self.stream_ops.lock().unwrap().insert(op_id, stream_id);
    }

    /// The stream a journaled open `op_id` minted, if still held.
    pub fn stream_op_id(&self, op_id: u64) -> Option<u64> {
        self.stream_ops.lock().unwrap().get(&op_id).copied()
    }

    /// A held stream's `(conn_id, remote_id_hex)`.
    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {
        self.shared
            .streams
            .lock()
            .unwrap()
            .get(&stream_id)
            .map(|e| (e.conn_id, e.remote_id_hex.clone()))
    }

    /// Write `bytes` to a held stream's send side; `finish` closes the send
    /// side after the write (the peer's read pump then sees a clean end).
    // [impl->REQ-DAEMON-2]
    pub fn send_stream(&self, stream_id: u64, bytes: &[u8], finish: bool) -> io::Result<()> {
        let entry = {
            let streams = self.shared.streams.lock().unwrap();
            Arc::clone(streams.get(&stream_id).ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::NotFound,
                    format!("no such stream {stream_id}"),
                )
            })?)
        };
        let bytes = bytes.to_vec();
        self.runtime.handle().block_on(async move {
            let mut send = entry.send.lock().await;
            if !bytes.is_empty() {
                send.write_all(&bytes).await?;
            }
            if finish {
                send.finish().await?;
            }
            Ok(())
        })
    }

    /// Every held stream's info — the query a re-attaching brain uses to find
    /// streams (including peer-initiated ones that arrived while it was down).
    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {
        let streams = self.shared.streams.lock().unwrap();
        streams
            .iter()
            .map(|(id, e)| {
                let log = e.log.lock().unwrap();
                NetStreamInfo {
                    stream_id: *id,
                    conn_id: e.conn_id,
                    remote_id_hex: e.remote_id_hex.clone(),
                    next_seq: log.next_seq,
                    finished: log.finished,
                    initiated_locally: e.initiated_locally,
                }
            })
            .collect()
    }

    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,
    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump
    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).
    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.
    /// No-op if the stream is gone.
    // [impl->REQ-SHELL-4]
    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {
        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {
            e.log.lock().unwrap().set_retentive(cap);
        }
    }

    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):
    /// pops the whole ring in order, concatenated, and wakes the read pump (which
    /// may have been lagging the sender at the backpressure cap). Returns
    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).
    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.
    // [impl->REQ-SHELL-4]
    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {
        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);
        let drained = entry.log.lock().unwrap().drain();
        // The drain freed ring room — wake a pump that was backpressure-parked.
        entry.room.notify_one();
        Some(drained)
    }

    /// A held stream's current undrained ring depth in chunks (test introspection
    /// for the retentive backpressure bound). `None` ⇒ no such stream.
    #[cfg(test)]
    fn ring_len(&self, stream_id: u64) -> Option<usize> {
        self.shared
            .streams
            .lock()
            .unwrap()
            .get(&stream_id)
            .map(|e| e.log.lock().unwrap().ring.len())
    }

    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the
    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just
    /// minted.
    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {
        self.shared
            .streams
            .lock()
            .unwrap()
            .iter()
            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)
            .map(|(id, _)| *id)
            .collect()
    }

    /// Open a same-node loopback tunnel stream PAIR (M11-W3, REQ-SHELL-4) and
    /// return `(owner_stream, shell_stream)` — the operator row the owner side
    /// drives and the cross-wired peer row the shell side drives. The loopback
    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a
    /// write to either send appears on the other's read log). Serialized so the
    /// before/after diff sees exactly the one peer row this open minted.
    // [impl->REQ-SHELL-4]
    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {
        let _guard = self.loopback_pair_lock.lock().unwrap();
        let (conn_id, _) = self.dial_loopback();
        let before = self.inbound_rows_on(conn_id);
        let owner_stream = self.open_stream(conn_id)?;
        let shell_stream = self
            .inbound_rows_on(conn_id)
            .difference(&before)
            .copied()
            .next()
            .ok_or_else(|| io::Error::other("loopback peer row not registered"))?;
        Ok((owner_stream, shell_stream))
    }

    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`
    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).
    // [impl->REQ-DAEMON-2]
    pub(crate) fn subscribe_stream(
        &self,
        stream_id: u64,
        sub: SharedSend,
        from_seq: u64,
    ) -> io::Result<()> {
        let log = {
            let streams = self.shared.streams.lock().unwrap();
            let e = streams.get(&stream_id).ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::NotFound,
                    format!("no such stream {stream_id}"),
                )
            })?;
            Arc::clone(&e.log)
        };
        log.lock().unwrap().attach(sub, from_seq);
        Ok(())
    }

    /// Detach `sub` from a stream iff it is the current subscriber (brain
    /// disconnect cleanup; the stream + its ring persist).
    pub(crate) fn detach_stream_sub(&self, stream_id: u64, sub: &SharedSend) {
        let log = {
            let streams = self.shared.streams.lock().unwrap();
            streams.get(&stream_id).map(|e| Arc::clone(&e.log))
        };
        if let Some(log) = log {
            log.lock().unwrap().detach_if(sub);
        }
    }

    /// Attach `sub` as the presence subscriber, replaying liveness events from
    /// `from_seq` (D4c) — the stream `subscribe` discipline on the conn table.
    // [impl->REQ-EP-4]
    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {
        self.shared.presence.lock().unwrap().attach(sub, from_seq);
    }

    /// Detach `sub` from presence iff it is the current subscriber (brain
    /// disconnect cleanup; the log + its ring persist).
    pub(crate) fn detach_presence_sub(&self, sub: &SharedSend) {
        self.shared.presence.lock().unwrap().detach_if(sub);
    }

    /// Gracefully close the endpoint: every held connection flushes its QUIC
    /// CLOSE so peers' closed-watchers fire promptly (a bare host drop kills
    /// the runtime first and leaves peers to an idle timeout).
    pub fn close(&self) {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            endpoint.close().await;
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn hermetic(identity: &Identity) -> NetConfig {
        NetConfig {
            identity: Identity::from_seed(&identity.seed()),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
            membership: None,
        }
    }

    // [unit->REQ-DAEMON-2] the endpoint is constructed broker-side (inside the
    // host's dedicated runtime) with relays disabled, bound to the node's own
    // identity, with an empty conn table.
    #[test]
    fn host_binds_endpoint_to_node_identity() {
        let id = Identity::generate();
        let host = NetHost::start(hermetic(&id)).expect("host start");
        assert_eq!(host.node_id_hex(), id.public_key().to_hex());
        assert_eq!(host.conn_count(), 0);
        assert!(!host.addr_json().is_null(), "dialable address serializes");
    }

    // [unit->REQ-DAEMON-2] a broker-owned host dials a second in-process host
    // over hermetic loopback QUIC; both sides register the connection — the
    // dialer in its table, the acceptor via its accept loop.
    #[test]
    fn two_hosts_dial_over_loopback() {
        let a = NetHost::start(hermetic(&Identity::generate())).expect("host a");
        let b_id = Identity::generate();
        let b = NetHost::start(hermetic(&b_id)).expect("host b");

        let (conn_id, remote) = a.dial(b.addr()).expect("dial");
        assert_eq!(conn_id, 1, "first broker-minted conn id");
        assert_eq!(
            remote,
            b_id.public_key().to_hex(),
            "handshake proves the peer"
        );
        assert_eq!(a.conn_count(), 1);

        // The acceptor's loop registers the inbound connection (async — poll).
        for _ in 0..400 {
            if b.conn_count() == 1 {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }
        assert_eq!(b.conn_count(), 1, "accept loop registered the inbound conn");
        assert_eq!(
            b.conn_remote_hex(1).as_deref(),
            Some(a.node_id_hex().as_str()),
            "acceptor sees the dialer's identity"
        );
    }

    // [unit->REQ-RC-1] the broker loopback conn (M12 W1.5, B1): `dial_loopback`
    // mints a REUSED singleton whose origin is this node's OWN id, and
    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a
    // peer row (the one the dispatcher serves) — with the operator's writes
    // crossing to the peer row's read pump. No iroh handshake, no QUIC.
    #[test]
    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {
        let id = Identity::generate();
        let host = NetHost::start(hermetic(&id)).expect("host start");

        // Singleton: reused, own-node origin (never a foreign/handshake id).
        let (c1, hex1) = host.dial_loopback();
        let (c2, hex2) = host.dial_loopback();
        assert_eq!(c1, c2, "the loopback conn is a reused singleton");
        assert_eq!(
            hex1,
            id.public_key().to_hex(),
            "loopback origin is THIS node's own id (broker-minted)"
        );
        assert_eq!(hex2, hex1);
        assert_eq!(host.conn_count(), 1, "one loopback conn held");

        // open_stream mints the cross-wired pair: operator row + peer row.
        let op_stream = host.open_stream(c1).expect("open loopback stream");
        let infos = host.stream_infos();
        assert_eq!(infos.len(), 2, "operator + peer rows");
        let op = infos
            .iter()
            .find(|s| s.stream_id == op_stream)
            .expect("operator row present");
        assert!(op.initiated_locally, "the returned id is the operator row");
        let peer = infos
            .iter()
            .find(|s| !s.initiated_locally)
            .expect("peer row present");
        assert_eq!(peer.conn_id, c1);
        assert_eq!(
            peer.remote_id_hex, hex1,
            "the peer row carries the own-node origin (gates same-node)"
        );

        // Cross-wire: a write on the operator side reaches the peer row's read
        // pump (its log advances) — entirely in-process, no QUIC.
        host.send_stream(op_stream, b"ping", false)
            .expect("write operator side");
        let peer_id = peer.stream_id;
        let mut crossed = false;
        for _ in 0..400 {
            if host
                .stream_infos()
                .iter()
                .any(|s| s.stream_id == peer_id && s.next_seq > 0)
            {
                crossed = true;
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }
        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }

    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell
    // tunnel) never evicts — `drain` returns every appended byte in order even past
    // the would-be cap (lossless, CONTEXT:262 "cannot drop frames"); an ordinary
    // (non-retentive) log keeps bounded-overwrite (a slow brain must not OOM the
    // broker), evicting the oldest chunks past `cap_chunks`.
    #[test]
    fn retentive_log_is_lossless_ordered_while_ordinary_log_overwrites() {
        // Retentive: cap 3, append 8 distinct chunks, NONE evicted — drain returns
        // all 8 concatenated in order.
        let mut ret = StreamLog::new(1, 1);
        ret.set_retentive(3);
        for i in 0..8u8 {
            ret.append(&[i]);
        }
        let (bytes, finished) = ret.drain();
        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), "retentive loses nothing, in order");
        assert!(!finished);
        // …and a second drain is empty (the cursor consumed them exactly once).
        assert_eq!(ret.drain().0, Vec::<u8>::new());

        // Ordinary: cap 3, append 8 — the oldest evict, only the last 3 survive
        // (the bounded-overwrite discipline ordinary streams keep).
        let mut ord = StreamLog::new(2, 3);
        for i in 0..8u8 {
            ord.append(&[i]);
        }
        assert_eq!(ord.drain().0, vec![5, 6, 7], "ordinary keeps only the last cap chunks");
    }

    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,
    // doyle gate condition): a retentive shell-side stream that is not drained while
    // the owner keeps writing must NOT lose bytes AND must NOT grow unbounded — the
    // read pump lags the sender at the cap. After the reader catches up, every byte
    // arrives byte-exact and in order. Proves "lag, never loss".
    #[test]
    fn loopback_tunnel_backpressures_without_loss() {
        use std::sync::atomic::{AtomicUsize, Ordering as AtOrd};
        let id = Identity::generate();
        let host = Arc::new(NetHost::start(hermetic(&id)).expect("host start"));
        let (owner, shell) = host.open_loopback_pair().expect("open tunnel pair");
        const CAP: usize = 4;
        host.mark_stream_retentive(owner, CAP);
        host.mark_stream_retentive(shell, CAP);

        // Distinct, read-chunk-sized frames so each lands as its own ring entry
        // (no coalescing) — lets us observe the cap exactly. 20 frames >> CAP.
        const N: usize = 20;
        let frames: Vec<Vec<u8>> = (0..N).map(|i| vec![i as u8; STREAM_READ_CHUNK]).collect();
        let sent = Arc::new(AtomicUsize::new(0));

        let writer = {
            let host = Arc::clone(&host);
            let frames = frames.clone();
            let sent = Arc::clone(&sent);
            std::thread::spawn(move || {
                for f in &frames {
                    host.send_stream(owner, f, false).expect("send");
                    sent.fetch_add(1, AtOrd::Relaxed);
                }
                host.send_stream(owner, &[], true).expect("finish");
            })
        };

        // Let the pump fill the shell ring to the cap and PARK (backpressure).
        let mut peak = 0usize;
        for _ in 0..200 {
            if let Some(d) = host.ring_len(shell) {
                peak = peak.max(d);
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
            if sent.load(AtOrd::Relaxed) >= N {
                break; // (shouldn't reach N while we never drain — backpressure)
            }
        }
        assert!(peak > 0, "the pump delivered some frames to the shell ring");
        assert!(
            host.ring_len(shell).unwrap() <= CAP,
            "the retentive ring never exceeds the backpressure cap (bounded)"
        );
        assert!(
            sent.load(AtOrd::Relaxed) < N,
            "the un-drained consumer lags the sender (backpressure engaged, not all sent)"
        );

        // Now drain to completion — the sender unblocks, every byte arrives in order.
        let mut got: Vec<u8> = Vec::new();
        let mut finished = false;
        for _ in 0..2000 {
            if let Some((mut bytes, fin)) = host.drain_stream(shell) {
                got.append(&mut bytes);
                if fin {
                    finished = true;
                    break;
                }
            }
            std::thread::sleep(std::time::Duration::from_millis(2));
        }
        writer.join().expect("writer joins");
        assert!(finished, "the tunnel saw EOF after the owner finished");
        let expected: Vec<u8> = frames.concat();
        assert_eq!(got.len(), expected.len(), "no byte lost or duplicated");
        assert_eq!(got, expected, "byte-exact, in order, under backpressure");
    }
}