270	    /// it holds locally-opened outbound streams too (gotcha: a dispatcher
271	    /// that served its own requester stream would deadlock on itself).
272	    initiated_locally: bool,
273	    /// The send half, serialized behind an async mutex (writes happen on the
274	    /// host's runtime via `block_on`).
275	    send: tokio::sync::Mutex<SendStream>,
276	    log: Arc<Mutex<StreamLog>>,
277	}
278
279	/// Tables shared between the host's sync surface and its runtime tasks
280	/// (accept loop, per-conn stream acceptors, per-stream read pumps).
281	struct NetShared {
282	    next_conn_id: AtomicU64,
283	    conns: Mutex<HashMap<u64, ConnEntry>>,
284	    next_stream_id: AtomicU64,
285	    streams: Mutex<HashMap<u64, Arc<StreamEntry>>>,
286	    ring_cap: usize,
287	    /// The conn-liveness event log (D4c) — one per host, all conns interleaved.
288	    presence: Mutex<PresenceLog>,
289	}
290
291	impl NetShared {
292	    fn new() -> Self {
293	        NetShared {
294	            next_conn_id: AtomicU64::new(1),
295	            conns: Mutex::new(HashMap::new()),
296	            next_stream_id: AtomicU64::new(1),
297	            streams: Mutex::new(HashMap::new()),
298	            ring_cap: DEFAULT_STREAM_RING_CHUNKS,
299	            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),
300	        }
301	    }
302	}
303
304	/// Register a live connection: mint its broker id and spawn its inbound-stream
305	/// acceptor. Must run inside the host's runtime context (the accept loop, or a
306	/// `block_on` future) — it spawns tasks.
307	fn register_conn(
308	    shared: &Arc<NetShared>,
309	    conn: Connection,
310	    proven_subnets: HashSet<String>,
311	) -> (u64, String) {
312	    let remote_id_hex = PublicKey::from_bytes(conn.remote_id().as_bytes())
313	        .expect("an iroh EndpointId is always a valid ed25519 public key")
314	        .to_hex();
315	    let id = shared.next_conn_id.fetch_add(1, Ordering::Relaxed);
316	    shared.conns.lock().unwrap().insert(
317	        id,
318	        ConnEntry {
319	            conn: conn.clone(),
320	            remote_id_hex: remote_id_hex.clone(),
321	            proven_subnets,
322	        },
323	    );
324
325	    // Presence: announce the row BEFORE spawning the closed-watcher, so the
326	    // `connected` event can never race behind its own `disconnected` (D4c).
327	    shared
328	        .presence
329	        .lock()
330	        .unwrap()
331	        .append(PRESENCE_CONNECTED, id, &remote_id_hex);
332
333	    // Closed-watcher: the row's exit path. Awaits the connection's end (clean
334	    // close, conn loss, or our own endpoint close), removes the row — conns
335	    // finally LEAVE the table — then appends the `disconnected` event (row
336	    // gone before the event is observable).
337	    // [impl->REQ-EP-4]
338	    let sh_close = Arc::clone(shared);
339	    let conn_close = conn.clone();
340	    let hex_close = remote_id_hex.clone();
341	    tokio::spawn(async move {
342	        conn_close.closed().await;
343	        sh_close.conns.lock().unwrap().remove(&id);
344	        sh_close
345	            .presence
346	            .lock()
347	            .unwrap()
348	            .append(PRESENCE_DISCONNECTED, id, &hex_close);
349	    });
350
351	    // Inbound-stream acceptor: every peer-opened bidi stream joins the table
352	    // and gets a read pump. (QUIC streams are lazy — the peer's open becomes
353	    // visible here on its first write.) Ends when the connection does.
354	    let sh = Arc::clone(shared);
355	    let hex = remote_id_hex.clone();
356	    tokio::spawn(async move {
357	        while let Ok((send, recv)) = conn.accept_bi().await {
358	            register_stream(&sh, id, &hex, send, recv, false);
359	        }
360	    });
361
362	    (id, remote_id_hex)
363	}
364
365	/// Register a live stream (opened or accepted): mint its broker id and spawn
366	/// its read pump. Must run inside the host's runtime context.
367	/// `initiated_locally` records the direction fact for the table (true only
368	/// from [`NetHost::open_stream`]).
369	fn register_stream(
370	    shared: &Arc<NetShared>,
371	    conn_id: u64,
372	    remote_id_hex: &str,
373	    send: SendStream,
374	    recv: RecvStream,
375	    initiated_locally: bool,
376	) -> u64 {
377	    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);
378	    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));
379	    shared.streams.lock().unwrap().insert(
380	        id,
381	        Arc::new(StreamEntry {
382	            conn_id,
383	            remote_id_hex: remote_id_hex.to_string(),
384	            initiated_locally,
385	            send: tokio::sync::Mutex::new(send),
386	            log: Arc::clone(&log),
387	        }),
388	    );
389

    /// 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<SendStream>,
    log: Arc<Mutex<StreamLog>>,
}

/// 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 {
            conn: 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, send, 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: SendStream,
    recv: RecvStream,
    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)));
    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),
        }),
    );


389
390	    // The read pump — the stream's single producer. A clean end (peer finish)
391	    // and a torn end (conn lost) both finish the log; the buffered chunks stay
392	    // replayable either way.
393	    let mut recv = recv;
394	    tokio::spawn(async move {
395	        loop {
396	            match recv.read_chunk(STREAM_READ_CHUNK).await {
397	                Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),
398	                Ok(None) | Err(_) => {
399	                    log.lock().unwrap().finish();
400	                    break;
401	                }
402	            }
403	        }
404	    });
405
406	    id
407	}
408
409	/// The broker-owned net endpoint + live connection/stream tables (see module
410	/// docs).
411	pub struct NetHost {
412	    /// The dedicated tokio runtime hosting the endpoint and its accept loop.
413	    /// Dropping the host drops the runtime, stopping every net task with it.
414	    runtime: tokio::runtime::Runtime,
415	    endpoint: Arc<NetEndpoint>,
416	    shared: Arc<NetShared>,
417	    /// `op_id → conn_id` for exactly-once dial replies: a deduped replay of a
418	    /// journaled dial is answered with the connection the first apply opened.
419	    dial_ops: Mutex<HashMap<u64, u64>>,
420	    /// `op_id → stream_id` for exactly-once stream-open replies (D4b), same
421	    /// discipline as `dial_ops`. Shares the one net op-id namespace.
422	    stream_ops: Mutex<HashMap<u64, u64>>,
423	    /// This node's pubkey (the seed-proof's local channel-binding identity).
424	    local_id: PublicKey,
425	    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables
426	    /// the proof (see [`NetConfig::membership`]).
427	    membership: Option<MembershipSource>,
428	    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live
429	    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`
430	    /// so the many hermetic test configs need no change — a roster exchange is a
431	    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +
432	    /// cloned per connect on the net runtime, so the accept loop (spawned in
433	    /// `start`, before the host exists) shares it through this `Arc`.
434	    roster: Arc<Mutex<Option<RosterExchange>>>,
435	}
436
437	impl NetHost {
438	    /// Bind the broker's endpoint on a fresh dedicated runtime and start the
439	    /// accept loop. The endpoint speaks TWO ALPNs (M7 D3, REQ-SUBNET-2): the
440	    /// trusted [`SPT_NET_ALPN`] (connections join the table as before) and
441	    /// the pre-trust pairing ALPN — routed to the always-on responder
442	    /// ([`crate::pairhost::respond`]), never registered in the stream table.
443	    /// One endpoint for both is deliberate: the pairing transcript binds and
444	    /// pins the REAL node pubkey, and the relay can only route one endpoint
445	    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).
446	    /// The meet-rotation listener loop rides the same runtime.
447	    // [impl->REQ-DAEMON-2]
448	    // [impl->REQ-SUBNET-2]
449	    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {
450	        // Hermeticity guard (REQ-PAIR-8): a loopback-scoped endpoint is the
451	        // "no network reached" promise — the ceremony clock's lazy NTP query
452	        // must honor it (tests must never probe a real NTP server). Real
453	        // scopes keep the decision-18 offset correction.
454	        spt_net::net::pairing::ntp::set_enabled(!matches!(cfg.scope, BindScope::Loopback));
455	        let runtime = tokio::runtime::Builder::new_multi_thread()
456	            .worker_threads(2)
457	            .thread_name("spt-broker-net")
458	            .enable_all()
459	            .build()?;
460	        let endpoint = runtime
461	            .block_on(NetEndpoint::bind_scoped(
462	                &cfg.identity,
463	                cfg.relay.clone(),
464	                cfg.discovery,
465	                vec![
466	                    SPT_NET_ALPN.to_vec(),
467	                    spt_net::net::pairing::wire::SPT_PAIR_ALPN.to_vec(),
468	                ],
469	                cfg.scope,
470	            ))
471	            .map_err(|e| io::Error::other(e.to_string()))?;
472	        let endpoint = Arc::new(endpoint);
473	        let shared = Arc::new(NetShared::new());
474	        let local_pub = cfg.identity.public_key();
475	        let pair_rate = Arc::new(tokio::sync::Mutex::new(
476	            spt_net::net::pairing::ratelimit::PairingRateLimiter::new(),
477	        ));
478
479	        // Accept loop: lives for the endpoint's lifetime on the host's runtime.
480	        // Each handshake completes on its own task so a slow peer can't stall
481	        // the loop; a failed handshake is simply not registered. The pairing
482	        // ALPN routes to the responder; everything else is the trusted table.
483	        let roster = Arc::new(Mutex::new(None::<RosterExchange>));
484	        let ep = Arc::clone(&endpoint);
485	        let sh = Arc::clone(&shared);
486	        let rate = Arc::clone(&pair_rate);
487	        let accept_membership = cfg.membership.clone();
488	        let accept_roster = Arc::clone(&roster);
489	        let accept_ep = Arc::clone(&endpoint);
490	        runtime.spawn(async move {
491	            while let Some(incoming) = ep.accept().await {
492	                let sh = Arc::clone(&sh);
493	                let rate = Arc::clone(&rate);
494	                let membership = accept_membership.clone();
495	                // Snapshot the (opt-in) roster exchange + our live address for
496	                // this connect; the leg runs after the proof verifies.
497	                let roster_rx = accept_roster.lock().unwrap().clone();
498	                let self_addr =
499	                    serde_json::to_value(accept_ep.addr()).unwrap_or(serde_json::Value::Null);
500	                tokio::spawn(async move {
501	                    let Ok(connecting) = incoming.accept() else {
502	                        return;
503	                    };
504	                    let Ok(conn) = connecting.await else { return };
505	                    if conn.alpn() == spt_net::net::pairing::wire::SPT_PAIR_ALPN {
506	                        crate::pairhost::respond(conn, local_pub, rate).await;
507	                    } else {
508	                        // Mesh-D2: prove membership in-band BEFORE the connection

    // 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.
    let mut recv = recv;
    tokio::spawn(async move {
        loop {
            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;
                }
            }
        }
    });

    id
}

/// 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>>>,
}

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


600
601	    /// [`Self::addr`] as the opaque JSON the IPC status reply carries.
602	    pub fn addr_json(&self) -> serde_json::Value {
603	        serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null)
604	    }
605
606	    /// Live connections (accepted + dialed) currently held.
607	    pub fn conn_count(&self) -> u64 {
608	        self.shared.conns.lock().unwrap().len() as u64
609	    }
610
611	    /// Dial a peer over `SPT_NET_ALPN`, registering the connection in the table.
612	    /// Bridges sync→async via the host's own runtime (the broker's dispatch
613	    /// threads block here; the future runs on the net worker threads).
614	    // [impl->REQ-DAEMON-2]
615	    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {
616	        let endpoint = Arc::clone(&self.endpoint);
617	        let shared = Arc::clone(&self.shared);
618	        let local_id = self.local_id;
619	        let membership = self.membership.clone();
620	        // Snapshot the (opt-in) roster exchange + our live address for this dial.
621	        let roster_rx = self.roster.lock().unwrap().clone();
622	        let self_addr = serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null);
623	        self.runtime.handle().block_on(async move {
624	            let conn = endpoint
625	                .connect(addr)
626	                .await
627	                .map_err(|e| io::Error::other(e.to_string()))?;
628	            // Mesh-D2: prove membership in-band before the connection is usable.
629	            // A failed/empty proof drops the dial (the conn never registers).
630	            // Mesh-D4: on success, a roster leg rides the same control stream.
631	            let proven = match membership {
632	                Some(src) => {
633	                    let creds = src();
634	                    prove_membership(
635	                        &conn,
636	                        local_id,
637	                        ProofRole::Dialer,
638	                        &creds,
639	                        roster_rx.as_ref(),
640	                        &self_addr,
641	                    )
642	                    .await
643	                    .ok_or_else(|| {
644	                        io::Error::other("seed-proof failed: peer is not a subnet member")
645	                    })?
646	                }
647	                None => HashSet::new(),
648	            };
649	            Ok(register_conn(&shared, conn, proven))
650	        })
651	    }
652
653	    /// Record that journaled dial `op_id` opened `conn_id` (called inside the
654	    /// `apply_once` closure, so a concurrent deduped replay always finds it).
655	    pub fn record_dial_op(&self, op_id: u64, conn_id: u64) {
656	        self.dial_ops.lock().unwrap().insert(op_id, conn_id);
657	    }
658
659	    /// The connection a journaled dial `op_id` opened, if this broker process
660	    /// still holds it (a broker restart drops connections; the journal alone
661	    /// survives — see `Broker::dispatch_net_dial`).
662	    pub fn dial_op_conn(&self, op_id: u64) -> Option<u64> {
663	        self.dial_ops.lock().unwrap().get(&op_id).copied()
664	    }
665
666	    /// The remote node id (hex) of a held connection.
667	    pub fn conn_remote_hex(&self, conn_id: u64) -> Option<String> {
668	        self.shared
669	            .conns
670	            .lock()
671	            .unwrap()
672	            .get(&conn_id)
673	            .map(|e| e.remote_id_hex.clone())
674	    }
675
676	    /// The subnets the peer of `conn_id` proved current-epoch seed-knowledge of
677	    /// at connect (Mesh-D2, REQ-MESH-1). Empty when seed-proof was disabled or
678	    /// the conn is gone. The seam the D5 gate swap reads in place of
679	    /// `is_trusted(subnet, origin)`; unused by any gate at D2.
680	    // [impl->REQ-MESH-1]
681	    pub fn conn_proven_subnets(&self, conn_id: u64) -> HashSet<String> {
682	        self.shared
683	            .conns
684	            .lock()
685	            .unwrap()
686	            .get(&conn_id)
687	            .map(|e| e.proven_subnets.clone())
688	            .unwrap_or_default()
689	    }
690
691	    /// The OBSERVED peer address of a held connection (remote id + the
692	    /// connection's current path addresses) as opaque JSON — the address-
693	    /// seeding write-back the `net-dialed` reply carries (M8 decision 20,
694	    /// REQ-CONV-1). `null` when the conn is gone, path-less, or fails to
695	    /// serialize: the seed is an optimization, never an error path.
696	    // [impl->REQ-CONV-1]
697	    pub fn conn_remote_addr_json(&self, conn_id: u64) -> serde_json::Value {
698	        let conn = {
699	            let conns = self.shared.conns.lock().unwrap();
700	            match conns.get(&conn_id) {
701	                Some(e) => e.conn.clone(),
702	                None => return serde_json::Value::Null,
703	            }
704	        };
705	        let addr = spt_net::net::endpoint::conn_remote_addr(&conn);
706	        if addr.is_empty() {
707	            return serde_json::Value::Null; // id-only adds nothing over discovery
708	        }
709	        serde_json::to_value(addr).unwrap_or(serde_json::Value::Null)
710	    }
711
712	    /// Open a bidi stream on a held connection, registering it (with its read
713	    /// pump) in the stream table (D4b). Note QUIC streams are lazy: the peer
714	    /// sees the stream on its first write.
715	    // [impl->REQ-DAEMON-2]
716	    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {
717	        let (conn, remote_id_hex) = {
718	            let conns = self.shared.conns.lock().unwrap();
719	            let e = conns.get(&conn_id).ok_or_else(|| {
720	                io::Error::new(io::ErrorKind::NotFound, format!("no such conn {conn_id}"))
721	            })?;
722	            (e.conn.clone(), e.remote_id_hex.clone())
723	        };
724	        let shared = Arc::clone(&self.shared);
725	        self.runtime.handle().block_on(async move {
726	            let (send, recv) = conn
727	                .open_bi()
728	                .await
729	                .map_err(|e| io::Error::other(e.to_string()))?;
730	            Ok(register_stream(
731	                &shared,
732	                conn_id,
733	                &remote_id_hex,
734	                send,
735	                recv,
736	                true,
737	            ))
738	        })
739	    }
740
741	    /// Record that journaled open `op_id` minted `stream_id` (inside the
742	    /// `apply_once` closure — same discipline as [`Self::record_dial_op`]).
743	    pub fn record_stream_op(&self, op_id: u64, stream_id: u64) {
744	        self.stream_ops.lock().unwrap().insert(op_id, stream_id);
745	    }
746
747	    /// The stream a journaled open `op_id` minted, if still held.
748	    pub fn stream_op_id(&self, op_id: u64) -> Option<u64> {
749	        self.stream_ops.lock().unwrap().get(&op_id).copied()
750	    }
751
752	    /// A held stream's `(conn_id, remote_id_hex)`.
753	    pub fn stream_conn(&self, stream_id: u64) -> Option<(u64, String)> {
754	        self.shared
755	            .streams
756	            .lock()
757	            .unwrap()
758	            .get(&stream_id)
759	            .map(|e| (e.conn_id, e.remote_id_hex.clone()))

    /// [`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) {
                Some(e) => e.conn.clone(),
                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)
    }

    /// 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.
    // [impl->REQ-DAEMON-2]
    pub fn open_stream(&self, conn_id: u64) -> io::Result<u64> {
        let (conn, 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}"))
            })?;
            (e.conn.clone(), e.remote_id_hex.clone())
        };
        let shared = Arc::clone(&self.shared);
        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,
                send,
                recv,
                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()))


proceed with W1.5 next steps. first verify the plan with doyle


Good grasp of nethost seam. QUIC path uses iroh `Connection`/`SendStream`/`RecvStream` + quinn `read_chunk` deeply — loopback needs transport abstraction (enum or in-proc channel pair feeding same `StreamLog` pump). Now check rc.rs self-dial line + dispatch Attach.


5://! is `serve_attach` on the target side ⇄ this pump on the operator side, both
7://! degenerate single-node case: the operator dials its OWN node (loopback peer)
8://! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`
14://! side ends the viewport with the session still running (`serve_attach` →
29:use spt_daemon::attach::{request_attach, send_attach_input};
187:    // Transport: dial our OWN node (loopback peer) so the inbound stream is
192:    let addr = brain.net_status().map_err(|e| format!("net status: {e}"))?.addr;
194:        .net_dial(addr, Some(ops.next()))
195:        .map_err(|e| format!("loopback dial: {e}"))?;
196:    let stream_id = request_attach(&mut brain, dialed.conn_id, session_id, 0, ops.next())
247:    // Finish our send side so the target's `serve_attach` sees EOF → Detached


180	    .map_err(|e| format!("broker connect: {e}"))?;
181
182	    let session_id = resolve_session(&mut brain, endpoint_id)
183	        .map_err(|e| format!("list sessions: {e}"))?
184	        .ok_or_else(|| format!("no live session for endpoint '{endpoint_id}'"))?;
185
186	    let ops = OpMinter::new();
187	    // Transport: dial our OWN node (loopback peer) so the inbound stream is
188	    // served by the broker's production attach dispatch — local = the degenerate
189	    // cross-node case. (If same-node self-dial misbehaves, the fallback is a
190	    // broker-local stream that surfaces as the same net-stream events; the pump
191	    // below is unchanged either way — the one-path invariant is here, not above.)
192	    let addr = brain.net_status().map_err(|e| format!("net status: {e}"))?.addr;
193	    let dialed = brain
194	        .net_dial(addr, Some(ops.next()))
195	        .map_err(|e| format!("loopback dial: {e}"))?;
196	    let stream_id = request_attach(&mut brain, dialed.conn_id, session_id, 0, ops.next())
197	        .map_err(|e| format!("attach request: {e}"))?;
198	    // Subscribe our side of the stream so the target's `Output` records surface
199	    // as `NetStreamData` events (the open + Request do not auto-subscribe).
200	    brain
201	        .net_stream_subscribe(stream_id, 0)
202	        .map_err(|e| format!("stream subscribe: {e}"))?;
203
204	    let _raw = RawGuard::enable();
205	    let rx = if view {
206	        None
207	    } else {
208	        let (tx, rx) = mpsc::channel();
209	        spawn_stdin_reader(tx);

    .map_err(|e| format!("broker connect: {e}"))?;

    let session_id = resolve_session(&mut brain, endpoint_id)
        .map_err(|e| format!("list sessions: {e}"))?
        .ok_or_else(|| format!("no live session for endpoint '{endpoint_id}'"))?;

    let ops = OpMinter::new();
    // Transport: dial our OWN node (loopback peer) so the inbound stream is
    // served by the broker's production attach dispatch — local = the degenerate
    // cross-node case. (If same-node self-dial misbehaves, the fallback is a
    // broker-local stream that surfaces as the same net-stream events; the pump
    // below is unchanged either way — the one-path invariant is here, not above.)
    let addr = brain.net_status().map_err(|e| format!("net status: {e}"))?.addr;
    let dialed = brain
        .net_dial(addr, Some(ops.next()))
        .map_err(|e| format!("loopback dial: {e}"))?;
    let stream_id = request_attach(&mut brain, dialed.conn_id, session_id, 0, ops.next())
        .map_err(|e| format!("attach request: {e}"))?;
    // Subscribe our side of the stream so the target's `Output` records surface
    // as `NetStreamData` events (the open + Request do not auto-subscribe).
    brain
        .net_stream_subscribe(stream_id, 0)
        .map_err(|e| format!("stream subscribe: {e}"))?;

    let _raw = RawGuard::enable();
    let rx = if view {
        None
    } else {
        let (tx, rx) = mpsc::channel();
        spawn_stdin_reader(tx);


rc.rs:192-195 = the self-dial seam (swap target). Now dispatch Attach routing.


75:use crate::attach::serve_attach;
91:pub enum StreamFamily {
120:pub fn classify_first_line(line: &serde_json::Value) -> StreamFamily {
122:        Some("request") if line.get("sync_id").is_some() => StreamFamily::Sync,
123:        Some("request") if line.get("session_id").is_some() => StreamFamily::Attach,
125:            StreamFamily::Rest
128:            StreamFamily::ShellLink
130:        Some("request") if line.get("serve_probe").is_some() => StreamFamily::ServeProbe,
131:        Some("query") => StreamFamily::Update,
134:        Some("status_query") => StreamFamily::Update,
135:        Some("row") => StreamFamily::Notif,
136:        Some("fetch") | Some("push") => StreamFamily::Xfer,
137:        Some(_) => StreamFamily::Unknown,
140:                StreamFamily::WanMsg
142:                StreamFamily::Registry
150:                StreamFamily::Registry
152:                StreamFamily::Unknown
305:        StreamFamily::Sync => {
324:        StreamFamily::Update => {
332:        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
333:        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),
334:        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
335:        StreamFamily::Attach => {
350:                Some(endpoint) => match serve_attach(&mut brain, stream_id, &endpoint, origin, 0) {
361:        StreamFamily::Xfer => {
386:        StreamFamily::Rest => {
398:        StreamFamily::ShellLink => {
409:        StreamFamily::ServeProbe => {
419:        StreamFamily::Unknown => DispatchOutcome::Unknown,
656:        assert_eq!(classify_first_line(&sync), StreamFamily::Sync);
659:        assert_eq!(classify_first_line(&attach), StreamFamily::Attach);
664:        assert_eq!(classify_first_line(&rest), StreamFamily::Rest);
668:        assert_eq!(classify_first_line(&update), StreamFamily::Update);
671:        assert_eq!(classify_first_line(&notif), StreamFamily::Notif);
674:        assert_eq!(classify_first_line(&fetch), StreamFamily::Xfer);
676:        assert_eq!(classify_first_line(&push), StreamFamily::Xfer);
679:        assert_eq!(classify_first_line(&wan), StreamFamily::WanMsg);
683:        assert_eq!(classify_first_line(&reg), StreamFamily::Registry);
700:            assert_eq!(classify_first_line(&v), StreamFamily::Unknown, "for {v}");


<owl_messages>
<owl_message from="doyle" priority="high">W1.5 PLAN VALIDATED — GO to build. Plan is sound; investigation-TODOs-before-coding is the right call for the 6.7 zone. Answers to your 3:

Q1 (origin) — Use THIS node's own id as origin_node; NO foreign/sentinel origin. KH 7.5 says the serve gate subject must be the REAL identity, never payload-asserted — and for loopback the real identity IS this node, established BY CONSTRUCTION (the broker mints the loopback conn; it's not wire-derivable). That's the key invariant to preserve + document: own-node origin can ONLY arise from a broker-minted loopback conn, never from an inbound handshake (a remote always presents its own key, can't claim to be us). CONFIRM the access_check is default-open for self-attach (own-node → own-node session); if it isn't, that's a small addition, not a redesign.

Q2 (frame KIND vs sentinel addr) — New explicit frame KIND (net_dial_loopback). NOT a sentinel addr — a magic-value in the net_dial hot path is a smell + fragile in the 6.7 zone. B1 already requires broker changes (Conn::Loopback); an explicit loopback-dial frame keeps net_dial pristine and isolates loopback handling. Additive + N-1 safe; do it deliberately.

Q3 (one-pump assertion) — By-construction via a SHARED serve_attach+pump helper that BOTH the cross-node int and the local int call. That makes the single code path visible in the test structure — the strongest form (no second path exists to diverge). Skip a runtime "same instance" assert — across separate test setups that's 1==1 noise. The local int test driving real loopback→serve_attach→pump IS the proof.

Net: own-node origin (broker-minted, document the invariant) + explicit net_dial_loopback frame + by-construction shared-helper assertion. Build it. Ping at the W1.5 gate. — doyle</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


270	                }
271	            }
272	            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
273	                // Finished without a complete line: classify what we have
274	                // (a line without a trailing newline before EOF still counts).
275	                return Ok(serde_json::from_slice(&buf).ok());
276	            }
277	            BrokerEvent::Error { message } => return Err(io::Error::other(message)),
278	            _ => {}
279	        }
280	    }
281	}
282
283	/// One claimed stream's worker: peek → classify → run the family's serve
284	/// loop on a fresh brain connection.
285	fn worker(
286	    broker_name: &str,
287	    stream_id: u64,
288	    origin: &str,
289	    paths: &DispatchPaths,
290	    registry: &RegistryHost,
291	) -> DispatchOutcome {
292	    let line = match peek_first_line(broker_name, stream_id) {
293	        Ok(Some(v)) => v,
294	        Ok(None) => return DispatchOutcome::Unknown,
295	        Err(e) => return DispatchOutcome::Failed(e.to_string()),
296	    };
297	    let family = classify_first_line(&line);
298
299	    let mut brain = match connect(broker_name) {
300	        Ok(b) => b,
301	        Err(e) => return DispatchOutcome::Failed(e.to_string()),
302	    };
303
304	    match family {
305	        StreamFamily::Sync => {
306	            let cs = match ContextStore::open_or_init() {
307	                Ok(cs) => cs,
308	                Err(e) => return DispatchOutcome::Failed(e.to_string()),
309	            };
310	            let policy = SyncPolicy::load();
311	            match serve_sync(
312	                &mut brain,
313	                stream_id,
314	                origin,
315	                0,
316	                &cs,
317	                &policy,
318	                &paths.scratch,
319	            ) {
320	                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
321	                Err(e) => DispatchOutcome::Failed(e.to_string()),
322	            }
323	        }
324	        StreamFamily::Update => {
325	            let cache = ReleaseCache::open(&paths.releases);
326	            let roster = RosterStore::load();
327	            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {
328	                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
329	                Err(e) => DispatchOutcome::Failed(e.to_string()),
330	            }
331	        }
332	        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
333	        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),
334	        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
335	        StreamFamily::Attach => {
336	            // The gate subject is the session's OWNING endpoint, resolved
337	            // from the broker's table (the spawn's label) — never from wire
338	            // bytes. An unlabeled or unknown session refuses, fail-closed.
339	            let session_id = line.get("session_id").and_then(|v| v.as_u64());
340	            let endpoint = session_id.and_then(|sid| {
341	                brain.sessions().ok().and_then(|reply| {
342	                    reply
343	                        .sessions
344	                        .iter()
345	                        .find(|s| s.session_id == sid && !s.endpoint.is_empty())
346	                        .map(|s| s.endpoint.clone())
347	                })
348	            });
349	            match endpoint {
350	                Some(endpoint) => match serve_attach(&mut brain, stream_id, &endpoint, origin, 0) {
351	                    Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
352	                    Err(e) => DispatchOutcome::Failed(e.to_string()),
353	                },
354	                None => {
355	                    // Refuse politely: finish our send side, claim stays.
356	                    let _ = brain.net_stream_send(stream_id, &[], None, true);
357	                    DispatchOutcome::Served("attach refused: unlabeled/unknown session".into())
358	                }
359	            }
360	        }
361	        StreamFamily::Xfer => {
362	            // The wire names whose whitelist gates (the rule set); the origin
363	            // it is checked against stays handshake-proven. Blank refuses.
364	            let endpoint = line
365	                .get("endpoint")
366	                .and_then(|v| v.as_str())
367	                .filter(|e| !e.is_empty())
368	                .map(str::to_string);
369	            match endpoint {

                }
            }
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                // Finished without a complete line: classify what we have
                // (a line without a trailing newline before EOF still counts).
                return Ok(serde_json::from_slice(&buf).ok());
            }
            BrokerEvent::Error { message } => return Err(io::Error::other(message)),
            _ => {}
        }
    }
}

/// One claimed stream's worker: peek → classify → run the family's serve
/// loop on a fresh brain connection.
fn worker(
    broker_name: &str,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let line = match peek_first_line(broker_name, stream_id) {
        Ok(Some(v)) => v,
        Ok(None) => return DispatchOutcome::Unknown,
        Err(e) => return DispatchOutcome::Failed(e.to_string()),
    };
    let family = classify_first_line(&line);

    let mut brain = match connect(broker_name) {
        Ok(b) => b,
        Err(e) => return DispatchOutcome::Failed(e.to_string()),
    };

    match family {
        StreamFamily::Sync => {
            let cs = match ContextStore::open_or_init() {
                Ok(cs) => cs,
                Err(e) => return DispatchOutcome::Failed(e.to_string()),
            };
            let policy = SyncPolicy::load();
            match serve_sync(
                &mut brain,
                stream_id,
                origin,
                0,
                &cs,
                &policy,
                &paths.scratch,
            ) {
                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
                Err(e) => DispatchOutcome::Failed(e.to_string()),
            }
        }
        StreamFamily::Update => {
            let cache = ReleaseCache::open(&paths.releases);
            let roster = RosterStore::load();
            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {
                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
                Err(e) => DispatchOutcome::Failed(e.to_string()),
            }
        }
        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),
        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
        StreamFamily::Attach => {
            // The gate subject is the session's OWNING endpoint, resolved
            // from the broker's table (the spawn's label) — never from wire
            // bytes. An unlabeled or unknown session refuses, fail-closed.
            let session_id = line.get("session_id").and_then(|v| v.as_u64());
            let endpoint = session_id.and_then(|sid| {
                brain.sessions().ok().and_then(|reply| {
                    reply
                        .sessions
                        .iter()
                        .find(|s| s.session_id == sid && !s.endpoint.is_empty())
                        .map(|s| s.endpoint.clone())
                })
            });
            match endpoint {
                Some(endpoint) => match serve_attach(&mut brain, stream_id, &endpoint, origin, 0) {
                    Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
                    Err(e) => DispatchOutcome::Failed(e.to_string()),
                },
                None => {
                    // Refuse politely: finish our send side, claim stays.
                    let _ = brain.net_stream_send(stream_id, &[], None, true);
                    DispatchOutcome::Served("attach refused: unlabeled/unknown session".into())
                }
            }
        }
        StreamFamily::Xfer => {
            // The wire names whose whitelist gates (the rule set); the origin
            // it is checked against stays handshake-proven. Blank refuses.
            let endpoint = line
                .get("endpoint")
                .and_then(|v| v.as_str())
                .filter(|e| !e.is_empty())
                .map(str::to_string);
            match endpoint {


doyle GO. All 3 answered: own-node origin (broker-minted, document invariant) + explicit `net_dial_loopback` frame + shared-helper by-construction assertion.

Ack doyle, then build.


Bash

$OWL send doyle todlando <<'EOF'
ACK — W1.5 GO. Locked your 3:
1. origin_node = THIS node's own id, arises ONLY from broker-minted loopback conn (never wire-derivable; remote always presents own key). Will confirm access_check default-open for self-attach; small add if not, not redesign. Documenting the invariant.
2. Explicit new frame KIND net_dial_loopback — net_dial stays pristine, loopback isolated. Additive + N-1 safe.
3. By-construction: shared serve_attach+pump helper called by BOTH cross-node int + local int. No runtime same-instance assert.
Building T1.5.1→T1.5.5. Ping at gate.
EOF

Ack doyle GO


Command running in background with ID: b2vp54pwr. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b2vp54pwr.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>b2vp54pwr</task-id>
<tool-use-id>toolu_0116CRXQK9hjnSaBdTApXay7</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b2vp54pwr.output</output-file>
<status>completed</status>
<summary>Background command "Ack doyle GO" completed (exit code 0)</summary>
</task-notification>


Now confirm self-attach access_check + map brain `net_dial` IPC + broker frame dispatch. Read serve_attach, brain net_dial, broker net handlers in parallel.


1	//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport
2	//! onto a local session for an operator on another node.
3	//!
4	//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent
5	//! [`AttachRecord::Request`]; this module is the target-side pump between that
6	//! stream and the local broker-hosted session:
7	//!
8	//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire
9	//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);
10	//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the
11	//!   operator-minted `op_id` — exactly-once at the PTY write through the B5
12	//!   journal (a replayed input record cannot double-type);
13	//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.
14	//!
15	//! ## Exactly-once lives at the EDGES, not the wire sends
16	//!
17	//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a
18	//! journaled [`Brain::net_stream_send`] blocks reading events until its ack
19	//! and *discards* everything interleaved — inside a full-duplex pump loop that
20	//! eats the operator's `Input` records (both sides starve; found the hard way
21	//! in `tests/attach.rs`). Idempotency holds without it because each direction
22	//! dedups at its **effect**: the PTY write is journaled by the operator-minted
23	//! `Input.op_id`, and the operator's viewport dedups `Output` records by their
24	//! session-log `seq` (the render cursor). A post-restart re-serve may
25	//! re-transmit output records on the wire — duplicates are dropped at the
26	//! render cursor, the same discipline the local `OutputLog` replay uses
27	//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).
28	//!
29	//! **Compute + files stay here**; the operator is a viewport (CONTEXT
30	//! §Remote-control vs local operation). The access gate runs before anything
31	//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so
32	//! [`access_check`] sees the **handshake-proven** origin from the broker's
33	//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of
34	//! your own running instance is ungated by default (ADR-0009 default-open);
35	//! the D5d whitelist drops in at this same call.
36	//!
37	//! Restart survival composes from D4/B5 with **no new machinery**: the broker
38	//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.
39	//! A restarted target brain re-attaches the session from its output cursor,
40	//! resubscribes the stream from its net cursor, and re-drives journaled sends;
41	//! a restarted operator brain resubscribes and dedups rendered chunks by
42	//! `Output.seq`.
43
44	use std::io;
45
46	use spt_net::net::attach::{AttachDecoder, AttachRecord};
47	use spt_net::net::ndjson;
48	use spt_store::perch::{resolve_perch_path, ParentHint};
49
50	use crate::access::{access_check, AccessDecision, InboundClass};
51	use crate::brain::{Brain, BrokerEvent};
52	use crate::msg::{decode_bytes, encode_bytes};
53
54	/// Stamp (or clear) the endpoint's `driven_by` remote-drive marker — the
55	/// daemon fact REQ-REACH-1 turns detection on: the agent (and the `spt`
56	/// binary) read it off the perch's `info.json` rather than guessing from
57	/// session heuristics. Best-effort: a session served without a perch (raw
58	/// broker tests) simply has nowhere to record it.
59	fn mark_driven_by(endpoint: &str, origin_node: Option<&str>) {
60	    let perch = resolve_perch_path(endpoint, ParentHint::Infer);
61	    let _ = spt_store::info::set_driven_by(&perch, origin_node);
62	}
63
64	/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
65	/// served viewport is a **driver** — attaching wakes the endpoint, the
66	/// viewport ending detaches it (active → dormant; the transition echo is
67	/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
68	/// [`mark_driven_by`]: a session served without a perch has no resting
69	/// record to move.
70	// [impl->REQ-INST-3]
71	fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {
72	    let _ = crate::resting::daemon_rest_event(endpoint, event, None);
73	}
74
75	/// Why a serve loop returned.
76	#[derive(Debug, Clone, PartialEq, Eq)]
77	pub enum AttachServeOutcome {
78	    /// The access gate refused the requesting origin — nothing was served.
79	    Refused,
80	    /// The session's child exited; the `Exit` record was sent and the stream
81	    /// finished.
82	    Exited(Option<i32>),
83	    /// The operator closed its send side (detached); the viewport ended with
84	    /// the session still running.
85	    Detached,
86	}
87
88	/// Serve one attach stream against this brain's broker, blocking until the
89	/// session exits, the operator detaches, or the gate refuses.
90	///
91	/// `origin_node` MUST be the handshake-proven remote id from the broker's
92	/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
93	/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session
94	/// is being attached, for the gate. `net_from_seq` is this brain's resume
95	/// cursor on the attach stream (0 cold — also the safe post-restart choice:
96	/// the replayed `Request`/`Input` records re-gate and dedup at the PTY
97	/// journal, and re-pumped output dedups at the operator's render cursor).
98	// [impl->REQ-INST-8]
99	// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
100	pub fn serve_attach(
101	    brain: &mut Brain,
102	    stream_id: u64,
103	    endpoint: &str,
104	    origin_node: &str,
105	    net_from_seq: u64,
106	) -> io::Result<AttachServeOutcome> {
107	    brain.net_stream_subscribe(stream_id, net_from_seq)?;
108	    let mut decoder = AttachDecoder::new();
109	    let mut attached = false;
110
111	    loop {
112	        match brain.read_event()? {
113	            // ── Wire → session: requests and input. ────────────────────────
114	            BrokerEvent::NetStreamData {
115	                stream_id: sid,
116	                bytes,
117	                ..
118	            } if sid == stream_id => {
119	                for rec in decoder.push(&bytes) {
120	                    match rec {
121	                        AttachRecord::Request {
122	                            session_id,
123	                            from_seq,
124	                        } => {
125	                            // Unsolicited wire inbound — the ADR-0009 gate,
126	                            // under the transport-proven origin.
127	                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)
128	                                == AccessDecision::Deny
129	                            {
130	                                brain.net_stream_send(stream_id, &[], None, true)?;
131	                                return Ok(AttachServeOutcome::Refused);
132	                            }
133	                            brain.attach(session_id, from_seq)?;
134	                            attached = true;
135	                            // Detection is a daemon fact (REQ-REACH-1): the
136	                            // served endpoint is now driven from origin_node.
137	                            // Re-stamped by a successor's re-serve after a
138	                            // brain crash (the same replayed Request that
139	                            // re-attaches); cleared when the viewport ends.
140	                            // [impl->REQ-REACH-1]
141	                            mark_driven_by(endpoint, Some(origin_node));
142	                            // A driver attached — the wake edge (re-activate
143	                            // in place; idempotent on an already-active seat).
144	                            feed_rest(endpoint, crate::resting::RestEvent::Wake);
145	                        }
146	                        AttachRecord::Input { data_b64, op_id } if attached => {
147	                            let bytes = decode_bytes(&data_b64)
148	                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
149	                            // Exactly-once at the PTY: the operator-minted id
150	                            // rides the B5 journal.
151	                            brain.send_effect(op_id, &bytes)?;
152	                        }
153	                        // Input before Request, or target-direction records
154	                        // echoed back: protocol noise — ignore (degrade).
155	                        _ => {}
156	                    }
157	                }
158	            }
159	            // The operator closed its send side: the viewport is over.
160	            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
161	                brain.net_stream_send(stream_id, &[], None, true)?;
162	                if attached {
163	                    mark_driven_by(endpoint, None);
164	                    // The driver detached — the endpoint rests warm.
165	                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
166	                }
167	                return Ok(AttachServeOutcome::Detached);
168	            }
169
170	            // ── Session → wire: output and exit. ────────────────────────────
171	            BrokerEvent::Output { seq, bytes, .. } if attached => {
172	                let line = ndjson::encode_line(&AttachRecord::Output {
173	                    seq,
174	                    data_b64: encode_bytes(&bytes),
175	                });
176	                // Unjournaled by design (see module docs): `seq` rides inside
177	                // the record, and the operator's render cursor is the dedup.
178	                brain.net_stream_send(stream_id, &line, None, false)?;
179	            }
180	            BrokerEvent::Exit { code, .. } if attached => {
181	                let line = ndjson::encode_line(&AttachRecord::Exit { code });
182	                brain.net_stream_send(stream_id, &line, None, true)?;
183	                mark_driven_by(endpoint, None);
184	                feed_rest(endpoint, crate::resting::RestEvent::Detach);
185	                return Ok(AttachServeOutcome::Exited(code));
186	            }
187
188	            BrokerEvent::Error { message } => {
189	                if attached {
190	                    mark_driven_by(endpoint, None);
191	                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
192	                }
193	                return Err(io::Error::other(message));
194	            }
195	            // Applied/NetSent acks and unrelated events: not ours to handle.
196	            _ => {}
197	        }
198	    }
199	}
200
201	/// Operator-side: open an attach stream on a dialed connection and send the
202	/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes
203	/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes
204	/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the
205	/// durable id for the exactly-once stream open (safe to block for its ack —
206	/// no attach traffic can be in flight before the stream exists); the Request
207	/// send itself is unjournaled (see module docs — a replayed Request re-gates
208	/// and re-attaches idempotently).
209	// [impl->REQ-INST-8]
210	pub fn request_attach(
211	    brain: &mut Brain,
212	    conn_id: u64,
213	    session_id: u64,
214	    from_seq: u64,
215	    open_op: u64,
216	) -> io::Result<u64> {
217	    let opened = brain.net_open_stream(conn_id, Some(open_op))?;
218	    let line = ndjson::encode_line(&AttachRecord::Request {
219	        session_id,
220	        from_seq,
221	    });
222	    brain.net_stream_send(opened.stream_id, &line, None, false)?;
223	    Ok(opened.stream_id)
224	}
225
226	/// Operator-side: send one input record (the keystrokes) onto an attach
227	/// stream. `wire_op` is the operator-minted durable id the TARGET journals the
228	/// PTY write under — exactly-once at the keys, however often the record
229	/// crosses the wire. The send itself is unjournaled (see module docs).
230	// [impl->REQ-INST-8]
231	pub fn send_attach_input(
232	    brain: &mut Brain,
233	    stream_id: u64,
234	    bytes: &[u8],
235	    wire_op: u64,
236	) -> io::Result<()> {
237	    let line = ndjson::encode_line(&AttachRecord::Input {
238	        data_b64: encode_bytes(bytes),
239	        op_id: wire_op,
240	    });
241	    brain.net_stream_send(stream_id, &line, None, false)?;
242	    Ok(())
243	}
244

//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport
//! onto a local session for an operator on another node.
//!
//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent
//! [`AttachRecord::Request`]; this module is the target-side pump between that
//! stream and the local broker-hosted session:
//!
//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire
//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);
//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the
//!   operator-minted `op_id` — exactly-once at the PTY write through the B5
//!   journal (a replayed input record cannot double-type);
//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.
//!
//! ## Exactly-once lives at the EDGES, not the wire sends
//!
//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a
//! journaled [`Brain::net_stream_send`] blocks reading events until its ack
//! and *discards* everything interleaved — inside a full-duplex pump loop that
//! eats the operator's `Input` records (both sides starve; found the hard way
//! in `tests/attach.rs`). Idempotency holds without it because each direction
//! dedups at its **effect**: the PTY write is journaled by the operator-minted
//! `Input.op_id`, and the operator's viewport dedups `Output` records by their
//! session-log `seq` (the render cursor). A post-restart re-serve may
//! re-transmit output records on the wire — duplicates are dropped at the
//! render cursor, the same discipline the local `OutputLog` replay uses
//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).
//!
//! **Compute + files stay here**; the operator is a viewport (CONTEXT
//! §Remote-control vs local operation). The access gate runs before anything
//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so
//! [`access_check`] sees the **handshake-proven** origin from the broker's
//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of
//! your own running instance is ungated by default (ADR-0009 default-open);
//! the D5d whitelist drops in at this same call.
//!
//! Restart survival composes from D4/B5 with **no new machinery**: the broker
//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.
//! A restarted target brain re-attaches the session from its output cursor,
//! resubscribes the stream from its net cursor, and re-drives journaled sends;
//! a restarted operator brain resubscribes and dedups rendered chunks by
//! `Output.seq`.

use std::io;

use spt_net::net::attach::{AttachDecoder, AttachRecord};
use spt_net::net::ndjson;
use spt_store::perch::{resolve_perch_path, ParentHint};

use crate::access::{access_check, AccessDecision, InboundClass};
use crate::brain::{Brain, BrokerEvent};
use crate::msg::{decode_bytes, encode_bytes};

/// Stamp (or clear) the endpoint's `driven_by` remote-drive marker — the
/// daemon fact REQ-REACH-1 turns detection on: the agent (and the `spt`
/// binary) read it off the perch's `info.json` rather than guessing from
/// session heuristics. Best-effort: a session served without a perch (raw
/// broker tests) simply has nowhere to record it.
fn mark_driven_by(endpoint: &str, origin_node: Option<&str>) {
    let perch = resolve_perch_path(endpoint, ParentHint::Infer);
    let _ = spt_store::info::set_driven_by(&perch, origin_node);
}

/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
/// served viewport is a **driver** — attaching wakes the endpoint, the
/// viewport ending detaches it (active → dormant; the transition echo is
/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
/// [`mark_driven_by`]: a session served without a perch has no resting
/// record to move.
// [impl->REQ-INST-3]
fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {
    let _ = crate::resting::daemon_rest_event(endpoint, event, None);
}

/// Why a serve loop returned.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttachServeOutcome {
    /// The access gate refused the requesting origin — nothing was served.
    Refused,
    /// The session's child exited; the `Exit` record was sent and the stream
    /// finished.
    Exited(Option<i32>),
    /// The operator closed its send side (detached); the viewport ended with
    /// the session still running.
    Detached,
}

/// Serve one attach stream against this brain's broker, blocking until the
/// session exits, the operator detaches, or the gate refuses.
///
/// `origin_node` MUST be the handshake-proven remote id from the broker's
/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session
/// is being attached, for the gate. `net_from_seq` is this brain's resume
/// cursor on the attach stream (0 cold — also the safe post-restart choice:
/// the replayed `Request`/`Input` records re-gate and dedup at the PTY
/// journal, and re-pumped output dedups at the operator's render cursor).
// [impl->REQ-INST-8]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
pub fn serve_attach(
    brain: &mut Brain,
    stream_id: u64,
    endpoint: &str,
    origin_node: &str,
    net_from_seq: u64,
) -> io::Result<AttachServeOutcome> {
    brain.net_stream_subscribe(stream_id, net_from_seq)?;
    let mut decoder = AttachDecoder::new();
    let mut attached = false;

    loop {
        match brain.read_event()? {
            // ── Wire → session: requests and input. ────────────────────────
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                for rec in decoder.push(&bytes) {
                    match rec {
                        AttachRecord::Request {
                            session_id,
                            from_seq,
                        } => {
                            // Unsolicited wire inbound — the ADR-0009 gate,
                            // under the transport-proven origin.
                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)
                                == AccessDecision::Deny
                            {
                                brain.net_stream_send(stream_id, &[], None, true)?;
                                return Ok(AttachServeOutcome::Refused);
                            }
                            brain.attach(session_id, from_seq)?;
                            attached = true;
                            // Detection is a daemon fact (REQ-REACH-1): the
                            // served endpoint is now driven from origin_node.
                            // Re-stamped by a successor's re-serve after a
                            // brain crash (the same replayed Request that
                            // re-attaches); cleared when the viewport ends.
                            // [impl->REQ-REACH-1]
                            mark_driven_by(endpoint, Some(origin_node));
                            // A driver attached — the wake edge (re-activate
                            // in place; idempotent on an already-active seat).
                            feed_rest(endpoint, crate::resting::RestEvent::Wake);
                        }
                        AttachRecord::Input { data_b64, op_id } if attached => {
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                            // Exactly-once at the PTY: the operator-minted id
                            // rides the B5 journal.
                            brain.send_effect(op_id, &bytes)?;
                        }
                        // Input before Request, or target-direction records
                        // echoed back: protocol noise — ignore (degrade).
                        _ => {}
                    }
                }
            }
            // The operator closed its send side: the viewport is over.
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                if attached {
                    mark_driven_by(endpoint, None);
                    // The driver detached — the endpoint rests warm.
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Ok(AttachServeOutcome::Detached);
            }

            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });
                // Unjournaled by design (see module docs): `seq` rides inside
                // the record, and the operator's render cursor is the dedup.
                brain.net_stream_send(stream_id, &line, None, false)?;
            }
            BrokerEvent::Exit { code, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Exit { code });
                brain.net_stream_send(stream_id, &line, None, true)?;
                mark_driven_by(endpoint, None);
                feed_rest(endpoint, crate::resting::RestEvent::Detach);
                return Ok(AttachServeOutcome::Exited(code));
            }

            BrokerEvent::Error { message } => {
                if attached {
                    mark_driven_by(endpoint, None);
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Err(io::Error::other(message));
            }
            // Applied/NetSent acks and unrelated events: not ours to handle.
            _ => {}
        }
    }
}

/// Operator-side: open an attach stream on a dialed connection and send the
/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes
/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes
/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the
/// durable id for the exactly-once stream open (safe to block for its ack —
/// no attach traffic can be in flight before the stream exists); the Request
/// send itself is unjournaled (see module docs — a replayed Request re-gates
/// and re-attaches idempotently).
// [impl->REQ-INST-8]
pub fn request_attach(
    brain: &mut Brain,
    conn_id: u64,
    session_id: u64,
    from_seq: u64,
    open_op: u64,
) -> io::Result<u64> {
    let opened = brain.net_open_stream(conn_id, Some(open_op))?;
    let line = ndjson::encode_line(&AttachRecord::Request {
        session_id,
        from_seq,
    });
    brain.net_stream_send(opened.stream_id, &line, None, false)?;
    Ok(opened.stream_id)
}

/// Operator-side: send one input record (the keystrokes) onto an attach
/// stream. `wire_op` is the operator-minted durable id the TARGET journals the
/// PTY write under — exactly-once at the keys, however often the record
/// crosses the wire. The send itself is unjournaled (see module docs).
// [impl->REQ-INST-8]
pub fn send_attach_input(
    brain: &mut Brain,
    stream_id: u64,
    bytes: &[u8],
    wire_op: u64,
) -> io::Result<()> {
    let line = ndjson::encode_line(&AttachRecord::Input {
        data_b64: encode_bytes(bytes),
        op_id: wire_op,
    });
    brain.net_stream_send(stream_id, &line, None, false)?;
    Ok(())
}


Prompt:
<task-notification>
<task-id>b2vp54pwr</task-id>
<tool-use-id>toolu_0116CRXQK9hjnSaBdTApXay7</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b2vp54pwr.output</output-file>
<status>completed</status>
<summary>Background command "Ack doyle GO" completed (exit code 0)</summary>
</task-notification>


Ack delivered. Now check `access_check` — confirm default-open covers self-attach (own-node origin).