/// One live QUIC connection the broker holds.
struct ConnEntry {
    /// The connection itself — kept alive by ownership here; dropping it closes
    /// the QUIC connection. Cloned (cheap handle) for stream opens.
    conn: Connection,
    /// The remote node's stable name (hex pubkey), proven by the QUIC handshake.
    remote_id_hex: String,
}

/// One live QUIC connection the broker holds.
struct ConnEntry {
    /// The connection itself — kept alive by ownership here; dropping it closes
    /// the QUIC connection. Cloned (cheap handle) for stream opens.
    conn: Connection,
    /// The remote node's stable name (hex pubkey), proven by the QUIC handshake.
    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>,
}

+    /// 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).

/// A bounded, sequenced read log for one QUIC stream — the PTY `OutputLog`


Now `register_conn` signature + body:


Edit

fn register_conn(shared: &Arc<NetShared>, conn: Connection) -> (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(),
        },
    );

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,
        },
    );


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

fn register_conn(shared: &Arc<NetShared>, conn: Connection) -> (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(),
        },
    );

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,
        },
    );

/// 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) -> (u64, String) {


Now NetHost struct + start() (store membership, run proof in accept loop):


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

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


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

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

+    /// This node's pubkey (the seed-proof's local channel-binding identity).

+    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables

+    /// the proof (see [`NetConfig::membership`]).

+    membership: Option<MembershipSource>,


Now the accept loop — run proof before `register_conn`:


        let ep = Arc::clone(&endpoint);
        let sh = Arc::clone(&shared);
        let rate = Arc::clone(&pair_rate);
        runtime.spawn(async move {
            while let Some(incoming) = ep.accept().await {
                let sh = Arc::clone(&sh);
                let rate = Arc::clone(&rate);
                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 {
                        register_conn(&sh, conn);
                    }
                });
            }
        });

        let ep = Arc::clone(&endpoint);
        let sh = Arc::clone(&shared);
        let rate = Arc::clone(&pair_rate);
        let accept_membership = cfg.membership.clone();
        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();
                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.
                        let proven = match membership {
                            Some(src) => {
                                let creds = src();
                                match prove_membership(
                                    &conn,
                                    local_pub,
                                    ProofRole::Acceptor,
                                    &creds,
                                )
                                .await
                                {
                                    Some(set) => set,
                                    None => return, // drop the connection
                                }
                            }
                            None => HashSet::new(),
                        };
                        register_conn(&sh, conn, proven);
                    }
                });
            }
        });


        let ep = Arc::clone(&endpoint);
        let sh = Arc::clone(&shared);
        let rate = Arc::clone(&pair_rate);
        runtime.spawn(async move {
            while let Some(incoming) = ep.accept().await {
                let sh = Arc::clone(&sh);
                let rate = Arc::clone(&rate);
                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 {
                        register_conn(&sh, conn);
                    }
                });
            }
        });

        let ep = Arc::clone(&endpoint);
        let sh = Arc::clone(&shared);
        let rate = Arc::clone(&pair_rate);
        let accept_membership = cfg.membership.clone();
        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();
                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.
                        let proven = match membership {
                            Some(src) => {
                                let creds = src();
                                match prove_membership(
                                    &conn,
                                    local_pub,
                                    ProofRole::Acceptor,
                                    &creds,
                                )
                                .await
                                {
                                    Some(set) => set,
                                    None => return, // drop the connection
                                }
                            }
                            None => HashSet::new(),
                        };
                        register_conn(&sh, conn, proven);
                    }
                });
            }
        });

+        let accept_membership = cfg.membership.clone();

+                let membership = accept_membership.clone();

-                        register_conn(&sh, conn);

+                        // 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.

+                        let proven = match membership {

+                            Some(src) => {

+                                let creds = src();

+                                match prove_membership(

+                                    &conn,

+                                    local_pub,

+                                    ProofRole::Acceptor,

+                                    &creds,

+                                    Some(set) => set,

+                                    None => return, // drop the connection

+                            None => HashSet::new(),

+                        register_conn(&sh, conn, proven);


Now the `Ok(NetHost { ... })` construction in start() needs the new fields:


        Ok(NetHost {
            runtime,
            endpoint,
            shared,
            dial_ops: Mutex::new(HashMap::new()),
            stream_ops: Mutex::new(HashMap::new()),
        })
    }

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


        Ok(NetHost {
            runtime,
            endpoint,
            shared,
            dial_ops: Mutex::new(HashMap::new()),
            stream_ops: Mutex::new(HashMap::new()),
        })
    }

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

+            membership: cfg.membership,


`local_pub` is captured by the `async move` accept-loop closure and also used after in the struct. Need to confirm `PublicKey: Copy`.


Bash

grep -n "pub struct PublicKey\|derive.*PublicKey\|#\[derive" crates/spt-proto/src/identity.rs | grep -i -B1 "struct PublicKey"; echo "---"; grep -n "struct PublicKey" -A1 crates/spt-proto/src/identity.rs; grep -n "derive" crates/spt-proto/src/identity.rs | head

Check PublicKey Copy derive


102:#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103:pub struct PublicKey(VerifyingKey);
---
103:pub struct PublicKey(VerifyingKey);
104-
26:#[derive(Debug, Clone, PartialEq, Eq)]
53:#[derive(Clone)]
102:#[derive(Debug, Clone, Copy, PartialEq, Eq)]


`PublicKey: Copy` ✓. Now `dial()` — run proof before register_conn:


Edit

    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {
        let endpoint = Arc::clone(&self.endpoint);
        let shared = Arc::clone(&self.shared);
        self.runtime.handle().block_on(async move {
            let conn = endpoint
                .connect(addr)
                .await
                .map_err(|e| io::Error::other(e.to_string()))?;
            Ok(register_conn(&shared, conn))
        })
    }

    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();
        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).
            let proven = match membership {
                Some(src) => {
                    let creds = src();
                    prove_membership(&conn, local_id, ProofRole::Dialer, &creds)
                        .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))
        })
    }


    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {
        let endpoint = Arc::clone(&self.endpoint);
        let shared = Arc::clone(&self.shared);
        self.runtime.handle().block_on(async move {
            let conn = endpoint
                .connect(addr)
                .await
                .map_err(|e| io::Error::other(e.to_string()))?;
            Ok(register_conn(&shared, conn))
        })
    }

    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();
        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).
            let proven = match membership {
                Some(src) => {
                    let creds = src();
                    prove_membership(&conn, local_id, ProofRole::Dialer, &creds)
                        .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))
        })
    }

+        let membership = self.membership.clone();

-            Ok(register_conn(&shared, conn))

+            // Mesh-D2: prove membership in-band before the connection is usable.

+            // A failed/empty proof drops the dial (the conn never registers).

+            let proven = match membership {

+                    prove_membership(&conn, local_id, ProofRole::Dialer, &creds)

+                        .ok_or_else(|| {

+                            io::Error::other("seed-proof failed: peer is not a subnet member")

+                None => HashSet::new(),

+            Ok(register_conn(&shared, conn, proven))


Now add the `conn_proven_subnets` accessor (next to `conn_remote_hex`):


    /// 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 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 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 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.

+    pub fn conn_proven_subnets(&self, conn_id: u64) -> HashSet<String> {

+            .map(|e| e.proven_subnets.clone())

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


Now update nethost's own test `hermetic()` helper to add `membership: None`, then build:


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

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


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

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


~16 more `NetConfig` literals need `membership`. Let me see their shape to batch them — and handle production (daemon.rs, wansend.rs) with `Some`.


Bash

for f in crates/spt-daemon/tests/access.rs crates/spt-daemon/tests/attach.rs crates/spt-daemon/tests/dispatch.rs crates/spt-daemon/tests/mesh.rs crates/spt-daemon/tests/netbroker.rs crates/spt-daemon/tests/netstream.rs crates/spt-daemon/tests/notifsync.rs crates/spt-daemon/tests/peerloop.rs crates/spt-daemon/tests/presence.rs crates/spt-daemon/tests/propagate.rs crates/spt-daemon/tests/replicate.rs crates/spt-daemon/tests/sync.rs crates/spt-daemon/tests/wanmsg.rs crates/spt-daemon/tests/xfer.rs crates/spt-daemon/tests/pairjoin.rs crates/spt-daemon/tests/twohost.rs; do echo "== $f =="; grep -n "NetConfig {" -A6 "$f" | head -10; done

Inspect NetConfig literal shapes across test files


== crates/spt-daemon/tests/access.rs ==
51:fn hermetic() -> NetConfig {
52:    NetConfig {
53-        identity: Identity::generate(),
54-        relay: RelayPolicy::Disabled,
55-        discovery: LocalDiscovery::Off,
56-        scope: BindScope::Loopback,
57-    }
58-}
== crates/spt-daemon/tests/attach.rs ==
56:fn hermetic() -> NetConfig {
57:    NetConfig {
58-        identity: Identity::generate(),
59-        relay: RelayPolicy::Disabled,
60-        discovery: LocalDiscovery::Off,
61-        scope: BindScope::Loopback,
62-    }
63-}
== crates/spt-daemon/tests/dispatch.rs ==
64:fn hermetic() -> NetConfig {
65:    NetConfig {
66-        identity: Identity::generate(),
67-        relay: RelayPolicy::Disabled,
68-        discovery: LocalDiscovery::Off,
69-        scope: BindScope::Loopback,
70-    }
71-}
== crates/spt-daemon/tests/mesh.rs ==
80:fn hermetic() -> NetConfig {
81:    NetConfig {
82-        identity: Identity::generate(),
83-        relay: RelayPolicy::Disabled,
84-        discovery: LocalDiscovery::Off,
85-        scope: BindScope::Loopback,
86-    }
87-}
== crates/spt-daemon/tests/netbroker.rs ==
38:fn hermetic(identity: Identity) -> NetConfig {
39:    NetConfig {
40-        identity,
41-        relay: RelayPolicy::Disabled,
42-        discovery: LocalDiscovery::Off,
43-        scope: BindScope::Loopback,
44-    }
45-}
== crates/spt-daemon/tests/netstream.rs ==
35:fn hermetic() -> NetConfig {
36:    NetConfig {
37-        identity: Identity::generate(),
38-        relay: RelayPolicy::Disabled,
39-        discovery: LocalDiscovery::Off,
40-        scope: BindScope::Loopback,
41-    }
42-}
== crates/spt-daemon/tests/notifsync.rs ==
40:fn hermetic() -> NetConfig {
41:    NetConfig {
42-        identity: Identity::generate(),
43-        relay: RelayPolicy::Disabled,
44-        discovery: LocalDiscovery::Off,
45-        scope: BindScope::Loopback,
46-    }
47-}
== crates/spt-daemon/tests/peerloop.rs ==
57:fn hermetic() -> NetConfig {
58:    NetConfig {
59-        identity: Identity::generate(),
60-        relay: RelayPolicy::Disabled,
61-        discovery: LocalDiscovery::Off,
62-        scope: BindScope::Loopback,
63-    }
64-}
== crates/spt-daemon/tests/presence.rs ==
34:fn hermetic() -> NetConfig {
35:    NetConfig {
36-        identity: Identity::generate(),
37-        relay: RelayPolicy::Disabled,
38-        discovery: LocalDiscovery::Off,
39-        scope: BindScope::Loopback,
40-    }
41-}
== crates/spt-daemon/tests/propagate.rs ==
49:fn hermetic() -> NetConfig {
50:    NetConfig {
51-        identity: Identity::generate(),
52-        relay: RelayPolicy::Disabled,
53-        discovery: LocalDiscovery::Off,
54-        scope: BindScope::Loopback,
55-    }
56-}
== crates/spt-daemon/tests/replicate.rs ==
41:fn hermetic() -> NetConfig {
42:    NetConfig {
43-        identity: Identity::generate(),
44-        relay: RelayPolicy::Disabled,
45-        discovery: LocalDiscovery::Off,
46-        scope: BindScope::Loopback,
47-    }
48-}
== crates/spt-daemon/tests/sync.rs ==
71:fn hermetic() -> NetConfig {
72:    NetConfig {
73-        identity: Identity::generate(),
74-        relay: RelayPolicy::Disabled,
75-        discovery: LocalDiscovery::Off,
76-        scope: BindScope::Loopback,
77-    }
78-}
== crates/spt-daemon/tests/wanmsg.rs ==
60:fn hermetic() -> NetConfig {
61:    NetConfig {
62-        identity: Identity::generate(),
63-        relay: RelayPolicy::Disabled,
64-        discovery: LocalDiscovery::Off,
65-        scope: BindScope::Loopback,
66-    }
67-}
== crates/spt-daemon/tests/xfer.rs ==
39:fn hermetic() -> NetConfig {
40:    NetConfig {
41-        identity: Identity::generate(),
42-        relay: RelayPolicy::Disabled,
43-        discovery: LocalDiscovery::Off,
44-        scope: BindScope::Loopback,
45-    }
46-}
== crates/spt-daemon/tests/pairjoin.rs ==
59:    let host = NetHost::start(NetConfig {
60-        identity: id_b,
61-        relay: RelayPolicy::Disabled,
62-        discovery: LocalDiscovery::Off,
63-        scope: BindScope::Loopback,
64-    })
65-    .expect("net host start");
== crates/spt-daemon/tests/twohost.rs ==
224:    let host = NetHost::start(NetConfig {
225-        identity,
226-        relay: RelayPolicy::Disabled,
227-        discovery: LocalDiscovery::Off,
228-        scope: BindScope::Port(port),
229-    })
230-    .expect("net host start");


All test literals end with a `scope:` line. Batch-insert `membership: None,` after it across the test files:


cd crates/spt-daemon/tests
for f in access.rs attach.rs dispatch.rs mesh.rs netbroker.rs netstream.rs notifsync.rs peerloop.rs presence.rs propagate.rs replicate.rs sync.rs wanmsg.rs xfer.rs pairjoin.rs twohost.rs; do
  sed -i -E 's/^(\s*)scope: (BindScope::Loopback|BindScope::Port\(port\)),$/\1scope: \2,\n\1membership: None,/' "$f"
done
echo "== verify a couple =="
grep -n "membership: None" access.rs twohost.rs pairjoin.rs mesh.rs

Batch-insert membership:None into test NetConfig literals


== verify a couple ==
access.rs:57:        membership: None,
twohost.rs:229:        membership: None,
pairjoin.rs:64:        membership: None,
mesh.rs:86:        membership: None,


Now production sites. Add a `production_membership()` helper to seedproofx, then wire it into daemon.rs + wansend.rs. Let me view both sites:


150	            let relay = match cfg.relay {
151	                RelayChoice::N0 => RelayPolicy::N0Default,
152	                RelayChoice::Disabled => RelayPolicy::Disabled,
153	            };
154	            match spt_store::nodeid::load_or_create() {
155	                Ok(identity) => {
156	                    node_hex = Some(identity.public_key().to_hex());
157	                    let net_cfg = NetConfig {
158	                        identity,
159	                        relay,
160	                        discovery: LocalDiscovery::Mdns,
161	                        scope: BindScope::All, // production: real interfaces
162	                    };
163	                    match NetHost::start(net_cfg) {
164	                        Ok(host) => Some(host),
165	                        Err(e) => {
166	                            eprintln!("NET_BIND_FAIL: {e} — broker runs net-less");
167	                            None
168	                        }
169	                    }

            let relay = match cfg.relay {
                RelayChoice::N0 => RelayPolicy::N0Default,
                RelayChoice::Disabled => RelayPolicy::Disabled,
            };
            match spt_store::nodeid::load_or_create() {
                Ok(identity) => {
                    node_hex = Some(identity.public_key().to_hex());
                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                    };
                    match NetHost::start(net_cfg) {
                        Ok(host) => Some(host),
                        Err(e) => {
                            eprintln!("NET_BIND_FAIL: {e} — broker runs net-less");
                            None
                        }
                    }


560	    use spt_store::info::{self, InfoJson};
561	    use spt_store::perch::ParentHint;
562	    use spt_store::spool;
563	    use std::sync::atomic::{AtomicBool, Ordering};
564	    use std::sync::Arc;
565	    use std::time::Duration;
566
567	    fn net_broker(name: &str, dir: &Path) -> Arc<Broker> {
568	        let host = NetHost::start(NetConfig {
569	            identity: Identity::generate(),
570	            relay: RelayPolicy::Disabled,
571	            discovery: LocalDiscovery::Off,
572	            scope: BindScope::Loopback,
573	        })
574	        .expect("net host");
575	        let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))
576	            .expect("bind broker");
577	        let serve = Arc::clone(&broker);
578	        std::thread::spawn(move || {
579	            let _ = serve.serve();

    use spt_store::info::{self, InfoJson};
    use spt_store::perch::ParentHint;
    use spt_store::spool;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    use std::time::Duration;

    fn net_broker(name: &str, dir: &Path) -> Arc<Broker> {
        let host = NetHost::start(NetConfig {
            identity: Identity::generate(),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
        })
        .expect("net host");
        let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))
            .expect("bind broker");
        let serve = Arc::clone(&broker);
        std::thread::spawn(move || {
            let _ = serve.serve();


daemon.rs:157 is the only true production site; wansend.rs:568 is a `#[cfg(test)]` helper (Loopback/Disabled) → `None`. Add the production helper to seedproofx, then wire daemon.rs:


Edit

use spt_proto::identity::{Identity, PublicKey};
use spt_store::subnet::SubnetStore;


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\seedproofx.rs has been updated successfully. (file state is current in your context — no need to Read it back)

use spt_proto::identity::{Identity, PublicKey};
use spt_store::subnet::SubnetStore;

MembershipKey, ProofRole, SeedProofFrame, SeedProofTranscript, NONCE_LEN,

/// One subnet's membership credential as the proof needs it: the human name

/// (goes on the wire), the raw seed bytes, and the current epoch (both local,


/// A live source of this node's subnet credentials, re-read per connection so a
/// rotation (Mesh-D7) is picked up without a restart. Production wires this to
/// `SubnetStore::load()`; `None` on [`NetConfig`](crate::nethost::NetConfig)
/// disables seed-proof entirely (both peers must agree — a mixed pair would
/// deadlock on the missing control stream).
pub type MembershipSource = Arc<dyn Fn() -> Vec<SubnetCred> + Send + Sync>;

/// A live source of this node's subnet credentials, re-read per connection so a
/// rotation (Mesh-D7) is picked up without a restart. Production wires this to
/// `SubnetStore::load()`; `None` on [`NetConfig`](crate::nethost::NetConfig)
/// disables seed-proof entirely (both peers must agree — a mixed pair would
/// deadlock on the missing control stream).
pub type MembershipSource = Arc<dyn Fn() -> Vec<SubnetCred> + Send + Sync>;

/// The production membership source: load the node's [`SubnetStore`] fresh on
/// every connect and project each subnet to its [`SubnetCred`]. Fresh-load (not
/// a snapshot) so a future seed rotation is honored without a daemon restart;
/// connects are rare enough that the file read is negligible. A subnet whose
/// stored seed hex is malformed is skipped (it can't prove anyway).
// [impl->REQ-MESH-1]
pub fn production_membership() -> MembershipSource {
    Arc::new(|| {
        SubnetStore::load()
            .subnets
            .iter()
            .filter_map(|r| {
                r.seed_bytes().map(|seed| SubnetCred {
                    name: r.name.clone(),
                    seed: seed.to_vec(),
                    epoch: r.epoch,
                })
            })
            .collect()
    })
}


/// A live source of this node's subnet credentials, re-read per connection so a
/// rotation (Mesh-D7) is picked up without a restart. Production wires this to
/// `SubnetStore::load()`; `None` on [`NetConfig`](crate::nethost::NetConfig)
/// disables seed-proof entirely (both peers must agree — a mixed pair would
/// deadlock on the missing control stream).
pub type MembershipSource = Arc<dyn Fn() -> Vec<SubnetCred> + Send + Sync>;

/// The production membership source: load the node's [`SubnetStore`] fresh on
/// every connect and project each subnet to its [`SubnetCred`]. Fresh-load (not
/// a snapshot) so a future seed rotation is honored without a daemon restart;
/// connects are rare enough that the file read is negligible. A subnet whose
/// stored seed hex is malformed is skipped (it can't prove anyway).
// [impl->REQ-MESH-1]
pub fn production_membership() -> MembershipSource {
    Arc::new(|| {
        SubnetStore::load()
            .subnets
            .iter()
            .filter_map(|r| {
                r.seed_bytes().map(|seed| SubnetCred {
                    name: r.name.clone(),
                    seed: seed.to_vec(),
                    epoch: r.epoch,
                })
            })
            .collect()
    })
}

+/// The production membership source: load the node's [`SubnetStore`] fresh on

+/// every connect and project each subnet to its [`SubnetCred`]. Fresh-load (not

+/// a snapshot) so a future seed rotation is honored without a daemon restart;

+/// connects are rare enough that the file read is negligible. A subnet whose

+/// stored seed hex is malformed is skipped (it can't prove anyway).

+pub fn production_membership() -> MembershipSource {

+                r.seed_bytes().map(|seed| SubnetCred {

+                    name: r.name.clone(),

+                    seed: seed.to_vec(),

/// Cap on the seed-proof control-stream framing — a single small handshake; a

/// length claiming more is a malformed/hostile peer, dropped.


                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                    };

                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                        // Mesh-D2: enforce seed-proof on every dial + accept,
                        // reading the live subnet store each connect.
                        membership: Some(crate::seedproofx::production_membership()),
                    };


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\daemon.rs has been updated successfully. (file state is current in your context — no need to Read it back)

                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                    };

                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                        // Mesh-D2: enforce seed-proof on every dial + accept,
                        // reading the live subnet store each connect.
                        membership: Some(crate::seedproofx::production_membership()),
                    };

+                        // Mesh-D2: enforce seed-proof on every dial + accept,

+                        // reading the live subnet store each connect.

+                        membership: Some(crate::seedproofx::production_membership()),


        let host = NetHost::start(NetConfig {
            identity: Identity::generate(),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
        })
        .expect("net host");

        let host = NetHost::start(NetConfig {
            identity: Identity::generate(),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
            membership: None,
        })
        .expect("net host");


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        let host = NetHost::start(NetConfig {
            identity: Identity::generate(),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
        })
        .expect("net host");

        let host = NetHost::start(NetConfig {
            identity: Identity::generate(),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
            membership: None,
        })
        .expect("net host");

let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))


Now build the daemon to catch errors:


Bash

cargo build -p spt-daemon 2>&1 | tail -40

Build spt-daemon


   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
warning: unused variable: `creds`
   --> crates\spt-daemon\src\seedproofx.rs:204:5
    |
204 |     creds: &[SubnetCred],
    |     ^^^^^ help: if this is intentional, prefix it with an underscore: `_creds`
    |
    = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: `spt-daemon` (lib) generated 1 warning (run `cargo fix --lib -p spt-daemon` to apply 1 suggestion)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 10.52s


Builds. Fix the unused `creds` param in `prove_as_acceptor`:


Edit

        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(
                &mut send,
                &mut recv,
                creds,
                &by_name,
                dialer_pub,
                acceptor_pub,
            )
            .await
        }

        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(&mut send, &mut recv, &by_name, dialer_pub, acceptor_pub).await
        }


        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(
                &mut send,
                &mut recv,
                creds,
                &by_name,
                dialer_pub,
                acceptor_pub,
            )
            .await
        }

        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(&mut send, &mut recv, &by_name, dialer_pub, acceptor_pub).await
        }

+            prove_as_acceptor(&mut send, &mut recv, &by_name, dialer_pub, acceptor_pub).await


async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {

async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {


async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {

async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {


Also clean up the dialer send loop (the `let _ = s;` hack):


    // Send all our (dialer-role) proofs first; they buffer while the acceptor
    // reads them, so this ping-pong never deadlocks on the single stream.
    for (s, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
        let _ = s; // name unused here; bound for symmetry/readability
    }

    // Send all our (dialer-role) proofs first; they buffer while the acceptor
    // reads them, so this ping-pong never deadlocks on the single stream.
    for (_name, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
    }


    // Send all our (dialer-role) proofs first; they buffer while the acceptor
    // reads them, so this ping-pong never deadlocks on the single stream.
    for (s, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
        let _ = s; // name unused here; bound for symmetry/readability
    }

    // Send all our (dialer-role) proofs first; they buffer while the acceptor
    // reads them, so this ping-pong never deadlocks on the single stream.
    for (_name, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
    }

+    for (_name, t, mk) in &transcripts {

-        let _ = s; // name unused here; bound for symmetry/readability

// Then read + verify the acceptor's proofs.


Now write the D2 int test. Let me check the existing `mesh.rs` test file structure to append there:


1	//! Mesh-D0 — the staggered + star subnet-mesh repro, RED.
2	//!
3	//! These two tests assert the *desired* full-mesh convergence the subnet does
4	//! NOT yet have (`SUBNET-MESH-PLAN.md`, ADR-0017). They are `#[ignore]`d: the
5	//! acceptance that flips green at **Mesh-D6** when the registry push target
6	//! widens from directly-paired peers to the full roster. No production change
7	//! lands with this file — it locks the bug as an executable repro. See
8	//! `MESH-D0-PLAN.md` for the fidelity decision.
9	//!
10	//! The bug, in one seam: `peerloop.rs` pushes a node's own rows to
11	//! `trust.peers_in(subnet)` — *directly-paired* peers only, no relay. So when
12	//! `C` joins `B` (C pins B, not A) and `A` joins `B`, A and C never pin each
13	//! other; C's pump never targets A and A's never targets C. They stay mutually
14	//! invisible even when both are online. The mesh restores reach by widening
15	//! that push target to every roster member (Mesh-D6).
16	//!
17	//! Fidelity: the **push side is the real loop** (`run_peer_pump` with explicit
18	//! per-node `PumpPaths` whose `trust` is the pairwise pin) — so the D6
19	//! target-widening flips these green untouched. The **gate side runs the real
20	//! dispatch loop** (`run_dispatch_loop`) under a permissive canonical
21	//! `SPT_HOME` trust, isolating the red strictly to the push-target seam (the
22	//! "subnet = pairing graph, not mesh" bug); the gate swap is Mesh-D5's own
23	//! unit. Addresses are omniscient (a node still only *pushes* where its
24	//! push-target says, so this does not mask the bug); address *discovery* is the
25	//! roster's job, tested at D3/D4.
26	//!
27	//! Refinement note: once the roster store exists (D3/D4) this harness seeds
28	//! rosters the way it seeds trust today; the `#[ignore]` lifts at D6 and the
29	//! file becomes the REQ-MESH-3 `int` evidence (tagged then, not now — D0
30	//! activates no REQ).
31
32	use std::collections::HashMap;
33	use std::path::PathBuf;
34	use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
35	use std::sync::{Arc, Mutex, OnceLock};
36	use std::thread;
37	use std::time::Duration;
38
39	use spt_daemon::brain::Brain;
40	use spt_daemon::broker::Broker;
41	use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
42	use spt_daemon::nethost::{NetConfig, NetHost};
43	use spt_daemon::peerloop::{
44	    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,
45	};
46	use spt_daemon::registryhost::RegistryHost;
47	use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
48	use spt_proto::identity::Identity;
49	use spt_store::epoch::EpochSource;
50	use spt_store::info::{self, InfoJson};
51	use spt_store::perch::{self, ParentHint};
52	use spt_store::subnet::SubnetStore;
53	use spt_store::trust::TrustStore;
54	use tempfile::TempDir;
55
56	const SUBNET: &str = "mesh0";
57
58	// One canonical SPT_HOME for the binary; the lock serializes the two tests
59	// (SPT_HOME is process-global). Per-node registries/owleries live under
60	// fresh per-test tempdirs, so the only shared canonical state is the subnet
61	// record + the permissive gate trust — both monotonic supersets, safe to
62	// re-establish per test.
63	static HOME: OnceLock<TempDir> = OnceLock::new();
64	static LOCK: Mutex<()> = Mutex::new(());
65	fn init_home() {
66	    HOME.get_or_init(|| {
67	        let dir = TempDir::new().expect("temp home");
68	        std::env::set_var("SPT_HOME", dir.path());
69	        std::env::set_var("SPT_NTP_SERVER", "off");
70	        dir
71	    });
72	}
73
74	static SEQ: AtomicU32 = AtomicU32::new(0);
75	fn unique_name() -> String {
76	    let n = SEQ.fetch_add(1, Ordering::Relaxed);
77	    format!("spt-daemon-mesh-{}-{}.sock", std::process::id(), n)
78	}
79
80	fn hermetic() -> NetConfig {
81	    NetConfig {
82	        identity: Identity::generate(),
83	        relay: RelayPolicy::Disabled,
84	        discovery: LocalDiscovery::Off,
85	        scope: BindScope::Loopback,
86	        membership: None,
87	    }
88	}
89
90	fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
91	    let host = NetHost::start(hermetic()).expect("net host start");
92	    let broker =
93	        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
94	    let serve = Arc::clone(&broker);
95	    thread::spawn(move || {
96	        let _ = serve.serve();
97	    });
98	    broker
99	}
100
101	fn connect_retry(name: &str) -> Brain {
102	    for _ in 0..300 {
103	        if let Ok(b) = Brain::cold_start(name, 1) {
104	            return b;
105	        }
106	        thread::sleep(Duration::from_millis(10));
107	    }
108	    panic!("brain could not connect");
109	}
110

//! Mesh-D0 — the staggered + star subnet-mesh repro, RED.
//!
//! These two tests assert the *desired* full-mesh convergence the subnet does
//! NOT yet have (`SUBNET-MESH-PLAN.md`, ADR-0017). They are `#[ignore]`d: the
//! acceptance that flips green at **Mesh-D6** when the registry push target
//! widens from directly-paired peers to the full roster. No production change
//! lands with this file — it locks the bug as an executable repro. See
//! `MESH-D0-PLAN.md` for the fidelity decision.
//!
//! The bug, in one seam: `peerloop.rs` pushes a node's own rows to
//! `trust.peers_in(subnet)` — *directly-paired* peers only, no relay. So when
//! `C` joins `B` (C pins B, not A) and `A` joins `B`, A and C never pin each
//! other; C's pump never targets A and A's never targets C. They stay mutually
//! invisible even when both are online. The mesh restores reach by widening
//! that push target to every roster member (Mesh-D6).
//!
//! Fidelity: the **push side is the real loop** (`run_peer_pump` with explicit
//! per-node `PumpPaths` whose `trust` is the pairwise pin) — so the D6
//! target-widening flips these green untouched. The **gate side runs the real
//! dispatch loop** (`run_dispatch_loop`) under a permissive canonical
//! `SPT_HOME` trust, isolating the red strictly to the push-target seam (the
//! "subnet = pairing graph, not mesh" bug); the gate swap is Mesh-D5's own
//! unit. Addresses are omniscient (a node still only *pushes* where its
//! push-target says, so this does not mask the bug); address *discovery* is the
//! roster's job, tested at D3/D4.
//!
//! Refinement note: once the roster store exists (D3/D4) this harness seeds
//! rosters the way it seeds trust today; the `#[ignore]` lifts at D6 and the
//! file becomes the REQ-MESH-3 `int` evidence (tagged then, not now — D0
//! activates no REQ).

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;

use spt_daemon::brain::Brain;
use spt_daemon::broker::Broker;
use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::peerloop::{
    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,
};
use spt_daemon::registryhost::RegistryHost;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::epoch::EpochSource;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use tempfile::TempDir;

const SUBNET: &str = "mesh0";

// One canonical SPT_HOME for the binary; the lock serializes the two tests
// (SPT_HOME is process-global). Per-node registries/owleries live under
// fresh per-test tempdirs, so the only shared canonical state is the subnet
// record + the permissive gate trust — both monotonic supersets, safe to
// re-establish per test.
static HOME: OnceLock<TempDir> = OnceLock::new();
static LOCK: Mutex<()> = Mutex::new(());
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp home");
        std::env::set_var("SPT_HOME", dir.path());
        std::env::set_var("SPT_NTP_SERVER", "off");
        dir
    });
}

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique_name() -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-mesh-{}-{}.sock", std::process::id(), n)
}

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

fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(hermetic()).expect("net host start");
    let broker =
        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });
    broker
}

fn connect_retry(name: &str) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, 1) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain could not connect");
}


I'll write the D2 int test as a focused new file (`tests/seedproofx.rs`) using two in-process `NetHost`s — same style as the existing nethost unit test, but with `membership` configured. Keep-alive is already covered by the spt-net unit test.


Write

//! Mesh-D2 — connect-time seed-proof, the integration evidence (REQ-MESH-1 int).
//!
//! Two in-process `NetHost`s dial over hermetic loopback QUIC with the
//! membership source wired. A connection establishes (and caches the proven
//! subnet set on both `ConnEntry`s) **only when both sides prove current-epoch
//! seed-knowledge of a shared subnet**; a non-prover (wrong seed, or no shared
//! subnet) is dropped before the connection ever joins the table. This is the
//! same two-host hermetic shape as `nethost`'s `two_hosts_dial_over_loopback`,
//! plus the seed-proof.

use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{MembershipSource, SubnetCred};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

/// A 20-byte subnet seed from a single filler byte (test material).
fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

/// A membership source returning a fixed credential list (clones per connect,
/// mirroring the live re-read of the production loader).
fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

/// Poll until `f()` holds (the acceptor registers its inbound conn async), or
/// give up after ~2s and return the last value.
fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
    }
}

fn set(names: &[&str]) -> HashSet<String> {
    names.iter().map(|s| s.to_string()).collect()
}

// [int->REQ-MESH-1] two members sharing a subnet (same seed + epoch) connect:
// the conn registers on BOTH sides and each caches the shared subnet as proven —
// the mesh property (a connection establishes only when both prove).
#[test]
fn both_members_prove_and_cache_the_shared_subnet() {
    let a = host(Identity::generate(), vec![cred("home", 7, 1)]);
    let b_id = Identity::generate();
    let b = host(b_id, vec![cred("home", 7, 1)]);

    let (conn_id, _remote) = a.dial(b.addr()).expect("dial member");
    assert_eq!(
        a.conn_proven_subnets(conn_id),
        set(&["home"]),
        "dialer caches the proven subnet"
    );
    assert!(
        wait_until(|| b.conn_count() == 1),
        "acceptor registered the proven inbound conn"
    );
    assert_eq!(
        b.conn_proven_subnets(1),
        set(&["home"]),
        "acceptor caches the proven subnet"
    );
}

// [int->REQ-MESH-1] the proven verdict is a SET, not a bool: two nodes sharing
// {home, work} but not the dialer-only `solo` prove exactly the intersection on
// both sides.
#[test]
fn proven_verdict_is_the_shared_set() {
    let a = host(
        Identity::generate(),
        vec![cred("home", 1, 1), cred("work", 2, 1), cred("solo", 9, 1)],
    );
    let b = host(
        Identity::generate(),
        vec![cred("home", 1, 1), cred("work", 2, 1)],
    );

    let (conn_id, _) = a.dial(b.addr()).expect("dial");
    assert_eq!(
        a.conn_proven_subnets(conn_id),
        set(&["home", "work"]),
        "dialer proves the intersection, not solo"
    );
    assert!(wait_until(|| b.conn_count() == 1), "acceptor registered");
    assert_eq!(b.conn_proven_subnets(1), set(&["home", "work"]));
}

// [int->REQ-MESH-1] a non-prover is dropped: same subnet NAME but a different
// seed (an outsider who can't derive the membership key) fails the mutual proof,
// so the dial errors and the acceptor never registers the connection.
#[test]
fn wrong_seed_is_dropped() {
    let member = host(Identity::generate(), vec![cred("home", 7, 1)]);
    let impostor = host(Identity::generate(), vec![cred("home", 8, 1)]); // wrong seed

    let res = impostor.dial(member.addr());
    assert!(res.is_err(), "dial fails when the peer rejects our proof");
    // The acceptor must not have a usable conn (it dropped the failed proof).
    assert!(
        !wait_until(|| member.conn_count() == 1),
        "acceptor dropped the non-prover; no conn in the table"
    );
}

// [int->REQ-MESH-1] no shared subnet ⇒ empty intersection ⇒ dropped. Two real
// members of DIFFERENT subnets have nothing to mesh, so the connection is not
// established.
#[test]
fn no_shared_subnet_is_dropped() {
    let a = host(Identity::generate(), vec![cred("alpha", 1, 1)]);
    let b = host(Identity::generate(), vec![cred("beta", 2, 1)]);

    let res = a.dial(b.addr());
    assert!(res.is_err(), "dial fails with no shared subnet");
    assert!(
        !wait_until(|| b.conn_count() == 1),
        "acceptor dropped the no-overlap conn"
    );
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\seedproofx.rs (file state is current in your context — no need to Read it back)

//! Mesh-D2 — connect-time seed-proof, the integration evidence (REQ-MESH-1 int).
//!
//! Two in-process `NetHost`s dial over hermetic loopback QUIC with the
//! membership source wired. A connection establishes (and caches the proven
//! subnet set on both `ConnEntry`s) **only when both sides prove current-epoch
//! seed-knowledge of a shared subnet**; a non-prover (wrong seed, or no shared
//! subnet) is dropped before the connection ever joins the table. This is the
//! same two-host hermetic shape as `nethost`'s `two_hosts_dial_over_loopback`,
//! plus the seed-proof.

use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{MembershipSource, SubnetCred};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

/// A 20-byte subnet seed from a single filler byte (test material).
fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

/// A membership source returning a fixed credential list (clones per connect,
/// mirroring the live re-read of the production loader).
fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

/// Poll until `f()` holds (the acceptor registers its inbound conn async), or
/// give up after ~2s and return the last value.
fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
    }
}

fn set(names: &[&str]) -> HashSet<String> {
    names.iter().map(|s| s.to_string()).collect()
}

// [int->REQ-MESH-1] two members sharing a subnet (same seed + epoch) connect:
// the conn registers on BOTH sides and each caches the shared subnet as proven —
// the mesh property (a connection establishes only when both prove).
#[test]
fn both_members_prove_and_cache_the_shared_subnet() {
    let a = host(Identity::generate(), vec![cred("home", 7, 1)]);
    let b_id = Identity::generate();
    let b = host(b_id, vec![cred("home", 7, 1)]);

    let (conn_id, _remote) = a.dial(b.addr()).expect("dial member");
    assert_eq!(
        a.conn_proven_subnets(conn_id),
        set(&["home"]),
        "dialer caches the proven subnet"
    );
    assert!(
        wait_until(|| b.conn_count() == 1),
        "acceptor registered the proven inbound conn"
    );
    assert_eq!(
        b.conn_proven_subnets(1),
        set(&["home"]),
        "acceptor caches the proven subnet"
    );
}

// [int->REQ-MESH-1] the proven verdict is a SET, not a bool: two nodes sharing
// {home, work} but not the dialer-only `solo` prove exactly the intersection on
// both sides.
#[test]
fn proven_verdict_is_the_shared_set() {
    let a = host(
        Identity::generate(),
        vec![cred("home", 1, 1), cred("work", 2, 1), cred("solo", 9, 1)],
    );
    let b = host(
        Identity::generate(),
        vec![cred("home", 1, 1), cred("work", 2, 1)],
    );

    let (conn_id, _) = a.dial(b.addr()).expect("dial");
    assert_eq!(
        a.conn_proven_subnets(conn_id),
        set(&["home", "work"]),
        "dialer proves the intersection, not solo"
    );
    assert!(wait_until(|| b.conn_count() == 1), "acceptor registered");
    assert_eq!(b.conn_proven_subnets(1), set(&["home", "work"]));
}

// [int->REQ-MESH-1] a non-prover is dropped: same subnet NAME but a different
// seed (an outsider who can't derive the membership key) fails the mutual proof,
// so the dial errors and the acceptor never registers the connection.
#[test]
fn wrong_seed_is_dropped() {
    let member = host(Identity::generate(), vec![cred("home", 7, 1)]);
    let impostor = host(Identity::generate(), vec![cred("home", 8, 1)]); // wrong seed

    let res = impostor.dial(member.addr());
    assert!(res.is_err(), "dial fails when the peer rejects our proof");
    // The acceptor must not have a usable conn (it dropped the failed proof).
    assert!(
        !wait_until(|| member.conn_count() == 1),
        "acceptor dropped the non-prover; no conn in the table"
    );
}

// [int->REQ-MESH-1] no shared subnet ⇒ empty intersection ⇒ dropped. Two real
// members of DIFFERENT subnets have nothing to mesh, so the connection is not
// established.
#[test]
fn no_shared_subnet_is_dropped() {
    let a = host(Identity::generate(), vec![cred("alpha", 1, 1)]);
    let b = host(Identity::generate(), vec![cred("beta", 2, 1)]);

    let res = a.dial(b.addr());
    assert!(res.is_err(), "dial fails with no shared subnet");
    assert!(
        !wait_until(|| b.conn_count() == 1),
        "acceptor dropped the no-overlap conn"
    );
}


Bash

cargo test -p spt-daemon --test seedproofx 2>&1 | tail -35

Run the D2 seedproof integration tests


   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 11.84s
     Running tests\seedproofx.rs (C:\Users\decid\Documents\projects\spt-core\target\debug\deps\seedproofx-f675754b303e7255.exe)

running 4 tests
test both_members_prove_and_cache_the_shared_subnet ... ok
test proven_verdict_is_the_shared_set ... ok
test no_shared_subnet_is_dropped ... ok
test wrong_seed_is_dropped ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.17s


All 4 int tests green. Now confirm no regression — spt-net full, daemon lib units, and a representative sample of the `membership: None` integration tests.


cargo test -p spt-net 2>&1 | tail -6; echo "=== daemon lib units ==="; cargo test -p spt-daemon --lib 2>&1 | tail -8

Run spt-net and daemon lib tests


   Doc-tests spt_net

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

=== daemon lib units ===
test seedmap::tests::take_absent_is_none ... ok
test serveprobe::tests::is_serving_subnet_honors_membership_and_detach ... ok
test shellwake::tests::resolve_wake_revives_a_suspended_owner_without_double_launch ... ok
test shellwake::tests::watcher_crash_exits_respawn_then_give_up ... ok
test sync::tests::select_refs_scopes_two_tiers ... ok

test result: ok. 209 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.41s


cargo test -p spt-daemon --test netbroker --test presence --test netstream 2>&1 | tail -20

Run representative None-path integration tests


test brain_dials_second_broker_over_loopback ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s

     Running tests\netstream.rs (C:\Users\decid\Documents\projects\spt-core\target\debug\deps\netstream-4ff2b23e110f436e.exe)

running 2 tests
test sender_brain_restart_redrive_is_exactly_once ... ok
test receiver_brain_restart_is_gapless_and_exactly_once ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

     Running tests\presence.rs (C:\Users\decid\Documents\projects\spt-core\target\debug\deps\presence-88b2ea173e3c0c8a.exe)

running 2 tests
test presence_history_replays_from_zero_for_a_late_subscriber ... ok
test presence_survives_brain_restart_with_cursor_resume ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.16s


No regression. Now confirm `spt-net --no-default-features` (mesh stays net-gated) and the `spt` crate build:


cargo build -p spt-net --no-default-features 2>&1 | tail -4; echo "=== spt crate ==="; cargo build -p spt 2>&1 | tail -4

Build no-default-features and spt crate


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
=== spt crate ===
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 15.91s


Both clean. Now traceability — activate REQ-MESH-1 `int` and update the rationale comment:


Edit

required_stages = ["impl", "unit"]  # ACTIVATED Mesh-D1 (codec slice: MK derivation + mutual channel-bound seed-proof transcript/verify + frame codec). D2 enriches impl (connect-time handshake + ConnEntry flag), D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; D5 adds gate admits-unpaired-member / rejects-non-member.

required_stages = ["impl", "unit", "int"]  # Mesh-D1 activated impl+unit (codec: MK derivation + mutual channel-bound transcript/verify + frame codec). Mesh-D2 enriches impl (connect-time mutual proof in nethost dial+accept, ConnEntry proven-subnet set, QUIC keep-alive) and activates int (a member conn establishes only when both prove; a non-prover / no-shared-subnet is dropped). D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; Hello-frame round-trip/reject; keep-alive < idle; D5 adds gate admits-unpaired-member / rejects-non-member.


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

title = "Membership proof (seed-proof): symmetric current-epoch seed-knowledge replaces is_trusted at EVERY inbound gate (registry apply, WAN receive, sync, notif, connection accept). MK = HKDF(seed, domain ‖ subnet_id ‖ seed_epoch); mutual channel-bound challenge-response at connect (transcript binds both handshake-proven node pubkeys, both nonces, subnet_id, seed_epoch, role); verified once per connection, cached on the broker ConnEntry, kept warm via QUIC keep-alive so re-proof is restart/partition/rotation-only. Exact-epoch match (re-seed is the sole N-1 exception). SECURITY INVARIANTS: channel-bound (no cross-connection replay), mutual, accepts a member it never paired (the mesh property)."

-required_stages = ["impl", "unit"]  # ACTIVATED Mesh-D1 (codec slice: MK derivation + mutual channel-bound seed-proof transcript/verify + frame codec). D2 enriches impl (connect-time handshake + ConnEntry flag), D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; D5 adds gate admits-unpaired-member / rejects-non-member.

+required_stages = ["impl", "unit", "int"]  # Mesh-D1 activated impl+unit (codec: MK derivation + mutual channel-bound transcript/verify + frame codec). Mesh-D2 enriches impl (connect-time mutual proof in nethost dial+accept, ConnEntry proven-subnet set, QUIC keep-alive) and activates int (a member conn establishes only when both prove; a non-prover / no-shared-subnet is dropped). D5 enriches impl (gate swap is_trusted→is_member at the five inbound sites). unit = MK derivation; valid/forged/wrong-subnet/wrong-epoch/cross-connection-replay/mutual-fail; Hello-frame round-trip/reject; keep-alive < idle; D5 adds gate admits-unpaired-member / rejects-non-member.

title = "Member roster: node-level union-merge grow-set (per member: pubkey, label, machine_id, last-known address, last-seen — NOT the seed), the discovery directory the mesh dials by. Seeded IN FULL at pairing (seed-holder hands joiner the whole current roster, incl. offline members — folds in deferred pairing-time hostname capture + post-join address seeding); each node authors its own entry stamped with its lease_epoch, merged strictly-greater-wins (the node_label lease); exchanged only over seed-proof'd member connections; forgery-inert (a fake entry names a pubkey that still can't seed-proof). Removal needs a TOMBSTONE — a per-pubkey revoked marker that propagates, dominates the entry, gates admission (seed-proof ∧ ¬tombstoned), and prevents reinsert; cleared by a completed re-pair of that pubkey. Persists through silence (offline member keeps its entry)."


Bash

cd C:/Users/decid/Documents/projects/spt-core && traceable-reqs check 2>&1 | tail -30

Run traceability check


  [OK] REQ-SUBNET-4  required: [impl, unit]  stages: +doc +impl +unit -int
       Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)
  [OK] REQ-SUBNET-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Per-subnet serve-state: spt subnet detach <NAME> [--save] / attach <NAME> [--save] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --save persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6, --save renamed from --auto per decision 25 session)
  [OK] REQ-SUBNET-6  required: [impl, unit]  stages: -doc +impl +unit -int
       Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)
  [OK] REQ-SUBNET-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)
  [OK] REQ-SUBNET-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)
  [OK] REQ-TERM-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Process-supervisor terminal wrapper hosting broker PTYs
  [OK] REQ-TERM-2  required: [impl, unit]  stages: -doc +impl +unit -int
       session-surface abstraction; send-keys + send-line injection
  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)


cd C:/Users/decid/Documents/projects/spt-core && traceable-reqs check 2>&1 | grep -iE "MESH-1|FAIL|error|missing|incomplete|✗|\bAll\b|coverage|tags valid|^\[" | head -30; echo "EXIT=$?"; traceable-reqs check >/dev/null 2>&1; echo "check exit=$?"

Filter traceability output for MESH-1 and pass/fail


172 requirements: 172 complete, 0 incomplete, 0 findings
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       One per-machine spt-daemon owning all per-machine state
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
       spt rename <id> rippled to all instances (collision-checked, 6.5-reconciled)
       Two-tier context sync (live to all, project to same-project)
  [OK] REQ-MESH-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Mesh row fan-out: registry rows stay OWN-AUTHORED; the only change is the push target widens from directly-paired peers to ALL roster members (a wider DIRECT fan-out, never a third-party relay). Every row/message still arrives from its author over a handshake → KNOWN-HAZARDS 7.5 (origin = handshake node) and 4.10 (eviction lease: any future update comes from that node itself, alive) PRESERVED VERBATIM. Closes the staggered A→B→C repro: C (roster-seeded with A at pairing) initiates to A, seed-proof admits C unpaired, A learns C, both push directly.
       Concurrent liveness probes: `spt subnet status --nodes` fans out its offline/serve-probes (REQ-SUBNET-5) CONCURRENTLY — total wall-time bounded by the single-probe ceiling (~3s), never k×ceiling. The mesh makes a node see ALL members (many possibly offline), so a serial probe loop would be offline_count×3s. (Planning verifies the current REQ-SUBNET-5 probe loop's behavior and fixes it if serial.)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
       Per-subnet serve-state: spt subnet detach <NAME> [--save] / attach <NAME> [--save] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --save persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6, --save renamed from --auto per decision 25 session)
       All binaries signature-verified before handoff
EXIT=0
check exit=0


Traceability green: **172 complete, 0 findings**, REQ-MESH-1 now `+impl +unit +int`. Let me run the remaining net-dialing integration tests to be thorough about the `None`-path no-regression:


cargo test -p spt-daemon --test peerloop --test propagate --test replicate --test sync --test wanmsg --test xfer --test dispatch --test notifsync --test access --test attach --test pairjoin 2>&1 | grep -E "Running|test result|FAILED|error\[" | tail -40

Run remaining net integration tests


     Running tests\access.rs (target\debug\deps\access-84d0786d0249d742.exe)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s
     Running tests\attach.rs (target\debug\deps\attach-ea7d331574a66ff3.exe)
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s
     Running tests\dispatch.rs (target\debug\deps\dispatch-6370db0e31a26238.exe)
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.77s
     Running tests\notifsync.rs (target\debug\deps\notifsync-6c8768e8b9615aa0.exe)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s
     Running tests\pairjoin.rs (target\debug\deps\pairjoin-a1cf22e8da745a62.exe)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
     Running tests\peerloop.rs (target\debug\deps\peerloop-c3fbcc98b93ab6bc.exe)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.11s
     Running tests\propagate.rs (target\debug\deps\propagate-b447ef5588280d7b.exe)
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.40s
     Running tests\replicate.rs (target\debug\deps\replicate-e9b77b78a079d445.exe)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s
     Running tests\sync.rs (target\debug\deps\sync-a49c0850ee96b3b2.exe)
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.67s
     Running tests\wanmsg.rs (target\debug\deps\wanmsg-5daa95b45e277726.exe)
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
     Running tests\xfer.rs (target\debug\deps\xfer-86841a764bedec6d.exe)
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.74s


All green. Let me run clippy on the touched crates before committing:


cargo clippy -p spt-net -p spt-daemon 2>&1 | grep -E "warning|error" | grep -vi "generated.*warning" | head -20; echo "=== clippy done ($?) ==="

Clippy on spt-net and spt-daemon


=== clippy done (0) ===


Clippy clean. Confirm the D0 mesh test binary still compiles (it's `#[ignore]`d) and the whole workspace builds:


cargo test -p spt-daemon --test mesh --no-run 2>&1 | tail -3; echo "=== workspace build ==="; cargo build --workspace 2>&1 | tail -4

Compile mesh test + full workspace build


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.20s
  Executable tests\mesh.rs (target\debug\deps\mesh-3206d171070508e8.exe)
=== workspace build ===
   Compiling mock-adapter v0.2.0 (C:\Users\decid\Documents\projects\spt-core\adapters\mock)
   Compiling xtask v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.36s


Everything builds and passes. Let me review the change set, then commit.


cd C:/Users/decid/Documents/projects/spt-core && git status --short && echo "=== stat ===" && git diff --stat && echo "=== untracked ===" && git status --short --untracked-files | grep '??'

Review git status and diff stat


 M crates/spt-daemon/src/daemon.rs
 M crates/spt-daemon/src/lib.rs
 M crates/spt-daemon/src/nethost.rs
 M crates/spt-daemon/tests/access.rs
 M crates/spt-daemon/tests/attach.rs
 M crates/spt-daemon/tests/dispatch.rs
 M crates/spt-daemon/tests/mesh.rs
 M crates/spt-daemon/tests/netbroker.rs
 M crates/spt-daemon/tests/netstream.rs
 M crates/spt-daemon/tests/notifsync.rs
 M crates/spt-daemon/tests/pairjoin.rs
 M crates/spt-daemon/tests/peerloop.rs
 M crates/spt-daemon/tests/presence.rs
 M crates/spt-daemon/tests/propagate.rs
 M crates/spt-daemon/tests/replicate.rs
 M crates/spt-daemon/tests/sync.rs
 M crates/spt-daemon/tests/twohost.rs
 M crates/spt-daemon/tests/wanmsg.rs
 M crates/spt-daemon/tests/xfer.rs
 M crates/spt-net/src/net/endpoint.rs
 M crates/spt-net/src/net/mesh/seedproof.rs
 M crates/spt/src/wansend.rs
 M traceable-reqs.toml
?? MESH-D2-PLAN.md
?? crates/spt-daemon/src/seedproofx.rs
?? crates/spt-daemon/tests/seedproofx.rs
=== stat ===
warning: in the working copy of 'crates/spt-daemon/src/daemon.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/nethost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/access.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/attach.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/dispatch.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/mesh.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/netbroker.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/netstream.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/notifsync.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/pairjoin.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/peerloop.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/presence.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/propagate.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/replicate.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/sync.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/twohost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/wanmsg.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/xfer.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/src/net/endpoint.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/src/net/mesh/seedproof.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/wansend.rs', LF will be replaced by CRLF the next time Git touches it
 crates/spt-daemon/src/daemon.rs          |   3 +
 crates/spt-daemon/src/lib.rs             |   1 +
 crates/spt-daemon/src/nethost.rs         |  89 +++++++++++++++++++-
 crates/spt-daemon/tests/access.rs        |   1 +
 crates/spt-daemon/tests/attach.rs        |   1 +
 crates/spt-daemon/tests/dispatch.rs      |   1 +
 crates/spt-daemon/tests/mesh.rs          |   1 +
 crates/spt-daemon/tests/netbroker.rs     |   1 +
 crates/spt-daemon/tests/netstream.rs     |   1 +
 crates/spt-daemon/tests/notifsync.rs     |   1 +
 crates/spt-daemon/tests/pairjoin.rs      |   1 +
 crates/spt-daemon/tests/peerloop.rs      |   1 +
 crates/spt-daemon/tests/presence.rs      |   1 +
 crates/spt-daemon/tests/propagate.rs     |   1 +
 crates/spt-daemon/tests/replicate.rs     |   1 +
 crates/spt-daemon/tests/sync.rs          |   1 +
 crates/spt-daemon/tests/twohost.rs       |   1 +
 crates/spt-daemon/tests/wanmsg.rs        |   1 +
 crates/spt-daemon/tests/xfer.rs          |   1 +
 crates/spt-net/src/net/endpoint.rs       |  55 +++++++++++-
 crates/spt-net/src/net/mesh/seedproof.rs | 138 ++++++++++++++++++++++++++++---
 crates/spt/src/wansend.rs                |   1 +
 traceable-reqs.toml                      |   2 +-
 23 files changed, 287 insertions(+), 18 deletions(-)
=== untracked ===
?? MESH-D2-PLAN.md
?? crates/spt-daemon/src/seedproofx.rs
?? crates/spt-daemon/tests/seedproofx.rs


Scoped cleanly — the five `is_trusted` gate files (registryhost/notifsync/sync) are untouched, as the plan requires (D5 owns the swap). Let me grab the project's commit trailer and commit.


cd C:/Users/decid/Documents/projects/spt-core && git log -1 --format='%(trailers)' fdbfbfd; echo "--- full trailer block of last commit ---"; git log -1 --format='%b' fdbfbfd | tail -3

Get project Co-Authored-By trailer


Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

--- full trailer block of last commit ---

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>


cd C:/Users/decid/Documents/projects/spt-core && git add -A && git commit -q -F - <<'EOF'
feat(mesh): seed-proof at connect + ConnEntry membership set (REQ-MESH-1) — Mesh-D2

Drive the D1 seed-proof codec onto live connections: run the mutual
channel-bound proof on every dial + accept in nethost BEFORE the conn is
usable, caching the proven-subnet set on ConnEntry; a non-prover or a
no-shared-subnet pair is dropped. The old is_trusted gates still run in
parallel (belt + suspenders — the gate swap is D5).

- spt-net seedproof: add the Hello frame (nonce + offered subnet names) so
  the two nodes negotiate which subnets to prove; total decode + unit test.
- spt-net endpoint: enable QUIC keep-alive (15s) < max idle (60s) so meshed
  member conns stay warm and re-proof is restart/partition/rotation-only
  (open item #1). Asserted at the constant seam.
- spt-daemon seedproofx: the daemon-side orchestration — one bidi control
  stream, four-leg ping-pong, prove every shared subnet (each side derives
  MK from its own local seed+epoch → exact-epoch reject on skew). Verdict is
  a HashSet (conns aren't subnet-scoped; D5's gates are per-(subnet,origin)).
- nethost: ConnEntry.proven_subnets + conn_proven_subnets accessor (the D5
  read seam); NetConfig.membership source (None disables, both peers must
  agree — mechanics-only tests pass None; production wires SubnetStore::load).
- int (REQ-MESH-1 activated): both-prove / proven-is-the-shared-set /
  wrong-seed-dropped / no-shared-subnet-dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log -1 --format='%h %s'

Commit Mesh-D2


warning: in the working copy of 'crates/spt-daemon/src/daemon.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/nethost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/access.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/attach.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/dispatch.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/mesh.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/netbroker.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/netstream.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/notifsync.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/pairjoin.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/peerloop.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/presence.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/propagate.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/replicate.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/sync.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/twohost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/wanmsg.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/xfer.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/src/net/endpoint.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/src/net/mesh/seedproof.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/wansend.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'MESH-D2-PLAN.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/seedproofx.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/seedproofx.rs', LF will be replaced by CRLF the next time Git touches it
db9f1bb feat(mesh): seed-proof at connect + ConnEntry membership set (REQ-MESH-1) — Mesh-D2


Mesh-D2 landed clean — commit `db9f1bb`. Caveman summary:

**Done — Mesh-D2: seed-proof at connect.**

What shipped:
- **D1 codec extended** — new `Hello` frame (nonce + offered subnet names). Two nodes negotiate which subnets to prove. Total decode + unit test.
- **QUIC keep-alive** (open item #1 closed) — `endpoint.rs` now sets keep-alive 15s < idle 60s. Meshed member conns stay warm → re-proof only on restart/partition/rotation. iroh default was keep-alive=**off**, idle=30s (would've dropped quiet conns).
- **`seedproofx.rs`** (new daemon module) — drives the mutual proof over one bidi control stream, four-leg ping-pong, before conn usable. Proves **every shared subnet**; each side derives MK from its own local seed+epoch → exact-epoch reject on skew.
- **nethost wiring** — `ConnEntry.proven_subnets: HashSet` + `conn_proven_subnets()` accessor (D5's read seam). `NetConfig.membership` source: `None` disables (mechanics tests), `Some(SubnetStore::load)` in production.

Key design call (forced by codebase, not chosen): verdict is a **set, not a bool** — conns aren't subnet-scoped (`dial` takes only addr; iroh dedups per peer-pair), and the five gates D5 swaps are per-`(subnet, origin)` per-item. Single-subnet-per-conn would need *more* plumbing and still can't answer the gate.

Verification:
- `spt-net mesh::seedproof` + keep-alive unit + 4 new int tests (both-prove / proven-set / wrong-seed-dropped / no-overlap-dropped) — green.
- ~16 None-path integration tests + 209 daemon lib units — no regression.
- `--no-default-features`, workspace build, clippy — clean.
- `traceable-reqs check` — **172 complete, 0 findings**; REQ-MESH-1 now `+impl +unit +int`.
- Five `is_trusted` gate files **untouched** (D5 owns the swap). `is_trusted` still gates in parallel — no regression.

**Next: Mesh-D3** — roster store + tombstones + pairing seed-transfer (`Frame::Seed` gains the full roster). JIT-write `MESH-D3-PLAN.md` first. D2 commits sit unpushed on `dev-freeform` — your push.


<command-message>spt:commune</command-message>
<command-name>/spt:commune</command-name>
<command-args>with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session</command-args>