    match responder.confirm(&initiator_tag) {
        Ok(_paired) => {
            // Confirmed live: pin the peer (TOFU). The peer id is the iroh-authenticated
            // remote id; label/user None (v1).
            let outcome = trust.record(p.subnet_name, &p.initiator_pub.to_hex(), None, None);
            write_frame(send, &enc_done(true)).await?;
            // F7: transfer the subnet seed so the new node becomes a full member /
            // future seed-holder ("every trusted node holds the seed", CONTEXT.md). Sent
            // ONLY on confirm-success, over the iroh-authenticated TLS stream to the
            // now-trust-pinned peer. `seed` is Some here (a None seed aborted upstream).
            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(
                send,
                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),
            )
            .await?;
            let _ = send.finish();
            Ok(outcome)
        }
        Err(e) => {

    match responder.confirm(&initiator_tag) {
        Ok(_paired) => {
            // Confirmed live. Membership is recorded by the caller (the daemon's
            // pairing host) into the roster — this layer no longer writes trust
            // (Mesh-D6 deleted the TOFU `peers.json` store).
            write_frame(send, &enc_done(true)).await?;
            // F7: transfer the subnet seed so the new node becomes a full member /
            // future seed-holder ("every member node holds the seed", CONTEXT.md). Sent
            // ONLY on confirm-success, over the iroh-authenticated TLS stream to the
            // now-confirmed peer. `seed` is Some here (a None seed aborted upstream).
            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(
                send,
                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),
            )
            .await?;
            let _ = send.finish();
            Ok(())
        }
        Err(e) => {

-            // Confirmed live: pin the peer (TOFU). The peer id is the iroh-authenticated

-            // remote id; label/user None (v1).

-            let outcome = trust.record(p.subnet_name, &p.initiator_pub.to_hex(), None, None);

+            // Confirmed live. Membership is recorded by the caller (the daemon's

+            // pairing host) into the roster — this layer no longer writes trust

+            // (Mesh-D6 deleted the TOFU `peers.json` store).

-            // future seed-holder ("every trusted node holds the seed", CONTEXT.md). Sent

+            // future seed-holder ("every member node holds the seed", CONTEXT.md). Sent

-            // now-trust-pinned peer. `seed` is Some here (a None seed aborted upstream).

+            // now-confirmed peer. `seed` is Some here (a None seed aborted upstream).


Now `run_initiator` signature + body:


Edit

/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
/// the returned [`TrustOutcome`] is the TOFU decision.

/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the transferred subnet seed is written into `subnets` and the seed-holder's
/// roster is adopted into `roster` (the joiner becomes a full member / future seed-holder —
/// D2f, REQ-PAIR-5); the caller (the daemon's pairing host) records its own membership.


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

/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
/// the returned [`TrustOutcome`] is the TOFU decision.

/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the transferred subnet seed is written into `subnets` and the seed-holder's
/// roster is adopted into `roster` (the joiner becomes a full member / future seed-holder —
/// D2f, REQ-PAIR-5); the caller (the daemon's pairing host) records its own membership.

-/// On success the responder is pinned in `trust` AND the transferred subnet seed is written

-/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);

-/// the returned [`TrustOutcome`] is the TOFU decision.

+/// On success the transferred subnet seed is written into `subnets` and the seed-holder's

+/// roster is adopted into `roster` (the joiner becomes a full member / future seed-holder —

+/// D2f, REQ-PAIR-5); the caller (the daemon's pairing host) records its own membership.

/// `step_back` shifts the ANNOUNCED time-step below the local clock's (0 =

/// the normal attempt). The joiner cannot validate the typed code locally


pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    roster: &mut RosterStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<TrustOutcome, PairWireError> {

pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    subnets: &mut SubnetStore,
    roster: &mut RosterStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<(), PairWireError> {


pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    roster: &mut RosterStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<TrustOutcome, PairWireError> {

pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    subnets: &mut SubnetStore,
    roster: &mut RosterStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<(), PairWireError> {

-) -> Result<TrustOutcome, PairWireError> {


    // F6: Done — the responder's verdict on our tag.
    match read_frame(&mut recv).await? {
        Frame::Done { ok: true } => {
            // Pin the responder (its pubkey was proven live by the confirmation round).
            let outcome = trust.record(subnet_name, &paired.peer_pub.to_hex(), None, None);
            // F7: receive the subnet seed and become a full member / future seed-holder.

    // F6: Done — the responder's verdict on our tag.
    match read_frame(&mut recv).await? {
        Frame::Done { ok: true } => {
            // The responder's pubkey was proven live by the confirmation round; the
            // caller records membership into the roster (Mesh-D6 — no trust write here).
            let _ = &paired;
            // F7: receive the subnet seed and become a full member / future seed-holder.


    // F6: Done — the responder's verdict on our tag.
    match read_frame(&mut recv).await? {
        Frame::Done { ok: true } => {
            // Pin the responder (its pubkey was proven live by the confirmation round).
            let outcome = trust.record(subnet_name, &paired.peer_pub.to_hex(), None, None);
            // F7: receive the subnet seed and become a full member / future seed-holder.

    // F6: Done — the responder's verdict on our tag.
    match read_frame(&mut recv).await? {
        Frame::Done { ok: true } => {
            // The responder's pubkey was proven live by the confirmation round; the
            // caller records membership into the roster (Mesh-D6 — no trust write here).
            let _ = &paired;
            // F7: receive the subnet seed and become a full member / future seed-holder.

-            // Pin the responder (its pubkey was proven live by the confirmation round).

-            let outcome = trust.record(subnet_name, &paired.peer_pub.to_hex(), None, None);

+            // The responder's pubkey was proven live by the confirmation round; the

+            // caller records membership into the roster (Mesh-D6 — no trust write here).

// Read it on the same confirmed stream; a missing/short Seed frame is a

// protocol error (the responder always sends it on success).


                other => return Err(unexpected("Seed", other)),
            }
            Ok(outcome)
        }
        Frame::Done { ok: false } => Err(PairWireError::Pairing(PairError::Confirm)),

                other => return Err(unexpected("Seed", other)),
            }
            Ok(())
        }
        Frame::Done { ok: false } => Err(PairWireError::Pairing(PairError::Confirm)),


                other => return Err(unexpected("Seed", other)),
            }
            Ok(outcome)
        }
        Frame::Done { ok: false } => Err(PairWireError::Pairing(PairError::Confirm)),

                other => return Err(unexpected("Seed", other)),
            }
            Ok(())
        }
        Frame::Done { ok: false } => Err(PairWireError::Pairing(PairError::Confirm)),


let (paired, initiator_tag) = match initiator.finish(&msg_b, &responder_tag) {

let (_paired, initiator_tag) = match initiator.finish(&msg_b, &responder_tag) {


// Finish the PAKE and verify the responder's tag. On failure, signal the responder

// with an Abort so it doesn't wait out the ceremony timeout, then surface the error.

-    let (paired, initiator_tag) = match initiator.finish(&msg_b, &responder_tag) {

+    let (_paired, initiator_tag) = match initiator.finish(&msg_b, &responder_tag) {

abort(&mut send, A_CONFIRM_FAIL, 0).await?;


            // The responder's pubkey was proven live by the confirmation round; the
            // caller records membership into the roster (Mesh-D6 — no trust write here).
            let _ = &paired;
            // F7: receive the subnet seed and become a full member / future seed-holder.

            // The responder's pubkey was proven live by the confirmation round; the
            // caller records membership into the roster (Mesh-D6 — no trust write here).
            // F7: receive the subnet seed and become a full member / future seed-holder.


            // The responder's pubkey was proven live by the confirmation round; the
            // caller records membership into the roster (Mesh-D6 — no trust write here).
            let _ = &paired;
            // F7: receive the subnet seed and become a full member / future seed-holder.

            // The responder's pubkey was proven live by the confirmation round; the
            // caller records membership into the roster (Mesh-D6 — no trust write here).
            // F7: receive the subnet seed and become a full member / future seed-holder.


Now migrate wire.rs tests. Bulk-remove the now-gone args first.


Bash

cd crates/spt-net/src/net/pairing && \
python - <<'PY'
import re,io
p='wire.rs'
s=open(p,encoding='utf-8').read()
b=s
# remove run_responder trust arg
s=s.replace(', &mut rate, &mut trust, NOW)', ', &mut rate, NOW)')
# remove init_trust arg line in run_initiator calls
s=s.replace('            &mut init_trust,\n','')
# remove trust var decls in tests
s=s.replace('            let mut trust = TrustStore::default();\n','')
s=s.replace('        let mut init_trust = TrustStore::default();\n','')
print('responder arg left:', s.count('&mut trust, NOW'))
print('init_trust decls left:', s.count('let mut init_trust'))
print('trust decls left:', s.count('let mut trust = TrustStore'))
open(p,'w',encoding='utf-8').write(s)
print('changed' if s!=b else 'NOCHANGE')
PY

Bulk-strip trust args from wire tests


responder arg left: 0
init_trust decls left: 0
trust decls left: 0
changed


spt-store: delete trust.rs, drop mod + trust_file

Delete crates/spt-store/src/trust.rs, remove `pub mod trust` from lib.rs, remove perch::trust_file (keep trust_dir for access/grants).

spt-net wire.rs: strip trust from ceremony

Remove trust param + TrustOutcome from run_responder/run_initiator/responder_admitted; drop ResponderOutcome.trust field; migrate wire.rs tests to roster membership.

pairhost.rs: drop trust write

Remove TrustStore load/save/args in both legs; update run_responder/run_initiator calls; demote warn-on-change to machine_id notif at repair_evict_superseded point.

propagate.rs serve gate trust->roster

serve_update signature trust:&TrustStore -> roster:&RosterStore; both is_member-any checks; dispatch.rs:326 caller.

peerloop.rs THE WIDEN

push target trust.peers_in -> roster.members_in; reuse roster loaded for sync gate; drop PumpPaths.trust + TrustStore load.

registryhost.rs: repair_evict drop trust

repair_evict_superseded drop &mut TrustStore param (roster-only now); migrate its unit tests.

CLI cli.rs repoint to roster

subnet status/list/prune trust->roster (prune=local tombstone); 1305 load; render unit tests.

xtask + misc comment refs

xtask main.rs:897 trust dump -> roster; nethost/notifsync/sync/access doc comment refs.

Migrate daemon tests trust->roster

twohost.rs, propagate.rs, peerloop.rs, dispatch.rs tests: TrustStore -> RosterStore membership.

mesh.rs: un-ignore + regression assert

Un-ignore D0 staggered+star harness; migrate trust seeding to roster; add no-third-party-row origin==author assert.

Activate REQ-MESH-3 + complete REQ-MESH-5

traceable-reqs.toml: REQ-MESH-3 [impl,unit,int], REQ-MESH-5 [impl,unit]; run traceable-reqs check; clippy; daemon suite.


cd crates/spt-net/src/net/pairing && grep -n "trust\|TrustOutcome\|TrustStore\|resp_outcome\|init_outcome\|resp_task.await\|(trust" wire.rs

Find remaining trust refs in wire tests


/usr/bin/bash: line 4: cd: crates/spt-net/src/net/pairing: No such file or directory


5://! stream on a dedicated pre-trust **pairing ALPN** ([`SPT_PAIR_ALPN`]), gated by the
12://! trust write is gone.
42://! delivers the ceremony + mutual trust write only.
57:/// The pre-trust pairing ALPN. Distinct from the trusted
59:/// pre-trust contact (a seed-holder must be reachable by an untrusted new node) without
60:/// exposing the trusted message surface (ADR-0005; `pairing.rs`). Versioned for a clean
64:/// Hard cap on a single wire frame. Pre-trust frames (Hello/Spake/…) are tens of bytes;
263:    /// The joiner's iroh-authenticated node pubkey (the trust write target).
336:            // pairing host) into the roster — this layer no longer writes trust
452:            // caller records membership into the roster (Mesh-D6 — no trust write here).
856:/// a panic (the input is pre-trust attacker-reachable).
913:    use spt_store::trust::TrustStore;
969:    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust
981:        // Responder task: accept, run the ceremony, return its trust store + outcome.
1000:            (trust, outcome)
1009:        let init_outcome = run_initiator(
1024:        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");
1027:            init_outcome,
1028:            TrustOutcome::Pinned,
1032:            resp_outcome.trust,
1033:            TrustOutcome::Pinned,
1039:        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
1040:        assert_eq!(resp_outcome.subnet, "home");
1041:        assert!(resp_outcome.intro.is_none(), "no intro sent");
1053:        // Each side trusts the OTHER's real pubkey within the subnet.
1054:        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
1055:        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
1057:            !init_trust.is_trusted("home", &init_pub.to_hex()),
1068:    // seed-derived one → key confirmation fails on the wire. Neither side writes trust,
1072:    async fn wrong_code_fails_and_writes_no_trust() {
1092:            (trust, result.is_err(), retry)
1115:        let (resp_trust, resp_errored, retry) = resp_task.await.expect("responder task");
1121:        assert!(resp_errored, "responder rejects");
1122:        assert!(init_trust.peers.is_empty(), "no trust on initiator");
1123:        assert!(resp_trust.peers.is_empty(), "no trust on responder");
1137:    // the joiner as a refusal, and no trust is written.
1160:            (trust, result)
1183:        let (resp_trust, resp_result) = resp_task.await.expect("responder task");
1190:            resp_result,
1193:        assert!(init_trust.peers.is_empty());
1194:        assert!(resp_trust.peers.is_empty());
1221:            (trust, result)
1244:        let (resp_trust, resp_result) = resp_task.await.expect("responder task");
1250:        assert!(matches!(resp_result, Err(PairWireError::StepRejected)));
1251:        assert!(resp_trust.peers.is_empty());
1256:    // error, no trust written.
1277:            (trust, result)
1301:        let (resp_trust, resp_result) = resp_task.await.expect("responder task");
1307:        assert!(matches!(resp_result, Err(PairWireError::UnknownSubnet(_))));
1308:        assert!(resp_trust.peers.is_empty());
1365:        resp_task.await.expect("responder task");
1375:    // already trusts the peer) re-pairs cleanly — the outcome is AlreadyTrusted and the
1402:        // The joiner already trusts the responder AND already holds "home" with a DISTINCT
1404:        init_trust.record("home", &resp_pub.to_hex(), None, None);
1413:        let init_outcome = run_initiator(
1427:        let resp_outcome = resp_task.await.expect("responder task");
1430:            init_outcome,
1431:            TrustOutcome::AlreadyTrusted,
1432:            "idempotent trust"
1435:            resp_outcome.trust,
1436:            TrustOutcome::Pinned,
1484:    // truncated body without panicking (the codec is pre-trust attacker-reachable).


908	    use super::*;
909	    use crate::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
910	    use spt_proto::identity::Identity;
911	    use spt_store::roster::RosterStore;
912	    use spt_store::subnet::{SubnetRecord, SubnetStore};
913	    use spt_store::trust::TrustStore;
914
915	    /// Reference time: a fixed unix second so both sides share a TOTP step deterministically.
916	    const NOW: u64 = 1_711_111_110;
917
918	    /// A responder-side subnet store holding one subnet with a known seed at `epoch`.
919	    fn subnet_store(name: &str, seed: [u8; 20], epoch: u64) -> SubnetStore {
920	        SubnetStore {
921	            subnets: vec![SubnetRecord {
922	                name: name.to_string(),
923	                seed_hex: hex(&seed),
924	                epoch,
925	                user: None,
926	                created: "0".into(),
927	                hide_new_endpoints: false,
928	            }],
929	        }
930	    }
931
932	    fn hex(bytes: &[u8]) -> String {
933	        bytes.iter().map(|b| format!("{b:02x}")).collect()
934	    }
935
936	    /// The current code for a seed at `NOW` — what the user "reads off their phone".
937	    fn code_for(seed: [u8; 20]) -> String {
938	        code_at_step(&TotpSeed::from_bytes(seed), time_step(NOW))
939	    }
940
941	    /// Bind a responder + an initiator endpoint on the pairing ALPN over loopback
942	    /// (loopback-scoped sockets: hermetic, and no Windows firewall prompt per
943	    /// freshly-built test binary).
944	    async fn pair_endpoints() -> (NetEndpoint, NetEndpoint, Identity, Identity) {
945	        let resp_id = Identity::generate();
946	        let init_id = Identity::generate();
947	        let responder = NetEndpoint::bind_scoped(
948	            &resp_id,
949	            RelayPolicy::Disabled,
950	            LocalDiscovery::Off,
951	            vec![SPT_PAIR_ALPN.to_vec()],
952	            BindScope::Loopback,
953	        )
954	        .await
955	        .expect("responder bind");
956	        let initiator = NetEndpoint::bind_scoped(
957	            &init_id,
958	            RelayPolicy::Disabled,
959	            LocalDiscovery::Off,
960	            vec![SPT_PAIR_ALPN.to_vec()],
961	            BindScope::Loopback,
962	        )
963	        .await
964	        .expect("initiator bind");
965	        (responder, initiator, resp_id, init_id)
966	    }
967
968	    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
969	    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust
970	    // store — "two machines pair via one TOTP code" at the component level (loopback,
971	    // relays disabled). The two-host WAN form is the int stage at D9.
972	    #[tokio::test]
973	    async fn loopback_pairs_and_pins_both_sides() {
974	        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
975	        let resp_pub = resp_id.public_key();
976	        let init_pub = init_id.public_key();
977	        let seed = [7u8; 20];
978	        let code = code_for(seed);
979	        let resp_addr = responder.addr();
980
981	        // Responder task: accept, run the ceremony, return its trust store + outcome.
982	        let resp_task = tokio::spawn(async move {
983	            let incoming = responder.accept().await.expect("incoming");
984	            let conn = incoming
985	                .accept()
986	                .expect("accept")
987	                .await
988	                .expect("connecting");
989	            let subnets = subnet_store("home", seed, 3);
990	            let mut rate = PairingRateLimiter::new();
991	            // A non-empty roster to transfer: one live member + one tombstoned
992	            // (REQ-MESH-2 — the joiner must adopt both).
993	            let mut resp_roster = RosterStore::default();
994	            resp_roster.upsert_self("home", "alpha", "host-a", "mid-a", None, "10", 4);
995	            resp_roster.tombstone("home", "ghost", "20");
996	            let outcome = run_responder(&conn, resp_pub, &subnets, &resp_roster, &mut rate, NOW)
997	                .await
998	                .expect("responder pairs");
999	            conn.closed().await;
1000	            (trust, outcome)
1001	        });
1002
1003	        let conn = initiator
1004	            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
1005	            .await
1006	            .expect("connect");
1007	        let mut init_subnets = SubnetStore::default();
1008	        let mut init_roster = RosterStore::default();
1009	        let init_outcome = run_initiator(
1010	            &conn,
1011	            init_pub,
1012	            "home",
1013	            &code,
1014	            &mut init_subnets,
1015	            &mut init_roster,
1016	            NOW,
1017	            0,    // step_back: the normal attempt
1018	            None, // intro: pre-M8 joiner shape
1019	        )
1020	        .await
1021	        .expect("initiator pairs");
1022	        conn.close(0u32.into(), b"done");
1023
1024	        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");
1025
1026	        assert_eq!(
1027	            init_outcome,
1028	            TrustOutcome::Pinned,
1029	            "initiator pins responder"
1030	        );
1031	        assert_eq!(
1032	            resp_outcome.trust,
1033	            TrustOutcome::Pinned,
1034	            "responder pins initiator"
1035	        );
1036	        // The outcome reports the ceremony facts the M8 re-pair overwrite
1037	        // consumes: the joiner's authenticated identity + the subnet. No
1038	        // intro was sent on this pre-M8-shaped call.
1039	        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
1040	        assert_eq!(resp_outcome.subnet, "home");
1041	        assert!(resp_outcome.intro.is_none(), "no intro sent");
1042	        // The joiner ADOPTED the seed-holder's roster (REQ-MESH-2): the live
1043	        // member is now its member, the tombstone propagated, and both were
1044	        // stamped into the subnet it joined (not the empty wire subnet).
1045	        assert!(init_roster.is_member("home", "alpha"), "adopted live member");
1046	        assert!(init_roster.is_tombstoned("home", "ghost"), "adopted tombstone");
1047	        assert!(!init_roster.is_member("home", "ghost"), "tombstone dominates");
1048	        assert_eq!(
1049	            init_roster.find("home", "alpha").unwrap().label,
1050	            "host-a",
1051	            "entry fields transferred"
1052	        );
1053	        // Each side trusts the OTHER's real pubkey within the subnet.
1054	        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
1055	        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
1056	        assert!(
1057	            !init_trust.is_trusted("home", &init_pub.to_hex()),
1058	            "self not pinned"
1059	        );
1060	        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /
1061	        // future seed-holder): same name, seed, and the responder's epoch (3).
1062	        let joined = init_subnets.find("home").expect("joiner gained the subnet");
1063	        assert_eq!(joined.seed_bytes(), Some(seed), "seed transferred intact");
1064	        assert_eq!(joined.epoch, 3, "epoch transferred");
1065	    }
1066
1067	    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the

    use super::*;
    use crate::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
    use spt_proto::identity::Identity;
    use spt_store::roster::RosterStore;
    use spt_store::subnet::{SubnetRecord, SubnetStore};
    use spt_store::trust::TrustStore;

    /// Reference time: a fixed unix second so both sides share a TOTP step deterministically.
    const NOW: u64 = 1_711_111_110;

    /// A responder-side subnet store holding one subnet with a known seed at `epoch`.
    fn subnet_store(name: &str, seed: [u8; 20], epoch: u64) -> SubnetStore {
        SubnetStore {
            subnets: vec![SubnetRecord {
                name: name.to_string(),
                seed_hex: hex(&seed),
                epoch,
                user: None,
                created: "0".into(),
                hide_new_endpoints: false,
            }],
        }
    }

    fn hex(bytes: &[u8]) -> String {
        bytes.iter().map(|b| format!("{b:02x}")).collect()
    }

    /// The current code for a seed at `NOW` — what the user "reads off their phone".
    fn code_for(seed: [u8; 20]) -> String {
        code_at_step(&TotpSeed::from_bytes(seed), time_step(NOW))
    }

    /// Bind a responder + an initiator endpoint on the pairing ALPN over loopback
    /// (loopback-scoped sockets: hermetic, and no Windows firewall prompt per
    /// freshly-built test binary).
    async fn pair_endpoints() -> (NetEndpoint, NetEndpoint, Identity, Identity) {
        let resp_id = Identity::generate();
        let init_id = Identity::generate();
        let responder = NetEndpoint::bind_scoped(
            &resp_id,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::Loopback,
        )
        .await
        .expect("responder bind");
        let initiator = NetEndpoint::bind_scoped(
            &init_id,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::Loopback,
        )
        .await
        .expect("initiator bind");
        (responder, initiator, resp_id, init_id)
    }

    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust
    // store — "two machines pair via one TOTP code" at the component level (loopback,
    // relays disabled). The two-host WAN form is the int stage at D9.
    #[tokio::test]
    async fn loopback_pairs_and_pins_both_sides() {
        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
        let resp_pub = resp_id.public_key();
        let init_pub = init_id.public_key();
        let seed = [7u8; 20];
        let code = code_for(seed);
        let resp_addr = responder.addr();

        // Responder task: accept, run the ceremony, return its trust store + outcome.
        let resp_task = tokio::spawn(async move {
            let incoming = responder.accept().await.expect("incoming");
            let conn = incoming
                .accept()
                .expect("accept")
                .await
                .expect("connecting");
            let subnets = subnet_store("home", seed, 3);
            let mut rate = PairingRateLimiter::new();
            // A non-empty roster to transfer: one live member + one tombstoned
            // (REQ-MESH-2 — the joiner must adopt both).
            let mut resp_roster = RosterStore::default();
            resp_roster.upsert_self("home", "alpha", "host-a", "mid-a", None, "10", 4);
            resp_roster.tombstone("home", "ghost", "20");
            let outcome = run_responder(&conn, resp_pub, &subnets, &resp_roster, &mut rate, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            (trust, outcome)
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::Pinned,
            "initiator pins responder"
        );
        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // The outcome reports the ceremony facts the M8 re-pair overwrite
        // consumes: the joiner's authenticated identity + the subnet. No
        // intro was sent on this pre-M8-shaped call.
        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
        assert_eq!(resp_outcome.subnet, "home");
        assert!(resp_outcome.intro.is_none(), "no intro sent");
        // The joiner ADOPTED the seed-holder's roster (REQ-MESH-2): the live
        // member is now its member, the tombstone propagated, and both were
        // stamped into the subnet it joined (not the empty wire subnet).
        assert!(init_roster.is_member("home", "alpha"), "adopted live member");
        assert!(init_roster.is_tombstoned("home", "ghost"), "adopted tombstone");
        assert!(!init_roster.is_member("home", "ghost"), "tombstone dominates");
        assert_eq!(
            init_roster.find("home", "alpha").unwrap().label,
            "host-a",
            "entry fields transferred"
        );
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
        assert!(
            !init_trust.is_trusted("home", &init_pub.to_hex()),
            "self not pinned"
        );
        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /
        // future seed-holder): same name, seed, and the responder's epoch (3).
        let joined = init_subnets.find("home").expect("joiner gained the subnet");
        assert_eq!(joined.seed_bytes(), Some(seed), "seed transferred intact");
        assert_eq!(joined.epoch, 3, "epoch transferred");
    }

    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the


Edit

    use spt_store::roster::RosterStore;
    use spt_store::subnet::{SubnetRecord, SubnetStore};
    use spt_store::trust::TrustStore;

    use spt_store::roster::RosterStore;
    use spt_store::subnet::{SubnetRecord, SubnetStore};


    use spt_store::roster::RosterStore;
    use spt_store::subnet::{SubnetRecord, SubnetStore};
    use spt_store::trust::TrustStore;

/// Reference time: a fixed unix second so both sides share a TOTP step deterministically.


    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust
    // store — "two machines pair via one TOTP code" at the component level (loopback,
    // relays disabled). The two-host WAN form is the int stage at D9.
    #[tokio::test]
    async fn loopback_pairs_and_pins_both_sides() {

    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
    // the ceremony on the pairing ALPN — "two machines pair via one TOTP code" at the
    // component level (loopback, relays disabled). Membership is the caller's roster
    // write (Mesh-D6); here we prove the ceremony succeeds and transfers seed + roster.
    // The two-host WAN form is the int stage at D9.
    #[tokio::test]
    async fn loopback_pairs_and_transfers_seed_and_roster() {


    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust
    // store — "two machines pair via one TOTP code" at the component level (loopback,
    // relays disabled). The two-host WAN form is the int stage at D9.
    #[tokio::test]
    async fn loopback_pairs_and_pins_both_sides() {

    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
    // the ceremony on the pairing ALPN — "two machines pair via one TOTP code" at the
    // component level (loopback, relays disabled). Membership is the caller's roster
    // write (Mesh-D6); here we prove the ceremony succeeds and transfers seed + roster.
    // The two-host WAN form is the int stage at D9.
    #[tokio::test]
    async fn loopback_pairs_and_transfers_seed_and_roster() {

-    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust

-    // store — "two machines pair via one TOTP code" at the component level (loopback,

-    // relays disabled). The two-host WAN form is the int stage at D9.

+    // the ceremony on the pairing ALPN — "two machines pair via one TOTP code" at the

+    // component level (loopback, relays disabled). Membership is the caller's roster

+    // write (Mesh-D6); here we prove the ceremony succeeds and transfers seed + roster.

+    // The two-host WAN form is the int stage at D9.

-    async fn loopback_pairs_and_pins_both_sides() {

+    async fn loopback_pairs_and_transfers_seed_and_roster() {

let (responder, initiator, resp_id, init_id) = pair_endpoints().await;


        // Responder task: accept, run the ceremony, return its trust store + outcome.
        let resp_task = tokio::spawn(async move {

        // Responder task: accept, run the ceremony, return its outcome.
        let resp_task = tokio::spawn(async move {


        // Responder task: accept, run the ceremony, return its trust store + outcome.
        let resp_task = tokio::spawn(async move {

        // Responder task: accept, run the ceremony, return its outcome.
        let resp_task = tokio::spawn(async move {

-        // Responder task: accept, run the ceremony, return its trust store + outcome.

+        // Responder task: accept, run the ceremony, return its outcome.

let incoming = responder.accept().await.expect("incoming");


            conn.closed().await;
            (trust, outcome)
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::Pinned,
            "initiator pins responder"
        );
        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // The outcome reports the ceremony facts the M8 re-pair overwrite

            conn.closed().await;
            outcome
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

        let resp_outcome = resp_task.await.expect("responder task");

        // The outcome reports the ceremony facts the M8 re-pair overwrite


            conn.closed().await;
            (trust, outcome)
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::Pinned,
            "initiator pins responder"
        );
        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // The outcome reports the ceremony facts the M8 re-pair overwrite

            conn.closed().await;
            outcome
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

        let resp_outcome = resp_task.await.expect("responder task");

        // The outcome reports the ceremony facts the M8 re-pair overwrite

-        let init_outcome = run_initiator(

-        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");

+        let resp_outcome = resp_task.await.expect("responder task");

// consumes: the joiner's authenticated identity + the subnet. No

// intro was sent on this pre-M8-shaped call.


        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
        assert!(
            !init_trust.is_trusted("home", &init_pub.to_hex()),
            "self not pinned"
        );
        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /

// [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /


        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
        assert!(
            !init_trust.is_trusted("home", &init_pub.to_hex()),
            "self not pinned"
        );
        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /

-        // Each side trusts the OTHER's real pubkey within the subnet.

-        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));

-        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));

-            !init_trust.is_trusted("home", &init_pub.to_hex()),

// future seed-holder): same name, seed, and the responder's epoch (3).

let joined = init_subnets.find("home").expect("joiner gained the subnet");


1050	    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the
1051	    // seed-derived one → key confirmation fails on the wire. Neither side writes trust,
1052	    // and the responder charges the failure (so an immediate retry is backed off) — the
1053	    // negative path that proves the code actually authenticates the channel.
1054	    #[tokio::test]
1055	    async fn wrong_code_fails_and_writes_no_trust() {
1056	        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
1057	        let resp_pub = resp_id.public_key();
1058	        let init_pub = init_id.public_key();
1059	        let seed = [9u8; 20];
1060	        let resp_addr = responder.addr();
1061
1062	        let resp_task = tokio::spawn(async move {
1063	            let incoming = responder.accept().await.expect("incoming");
1064	            let conn = incoming
1065	                .accept()
1066	                .expect("accept")
1067	                .await
1068	                .expect("connecting");
1069	            let subnets = subnet_store("home", seed, 1);
1070	            let mut rate = PairingRateLimiter::new();
1071	            let result = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, NOW).await;
1072	            conn.closed().await;
1073	            // After a failed ceremony the slot is backed off (charged a failure).
1074	            let retry = rate.begin("home", NOW);
1075	            (trust, result.is_err(), retry)
1076	        });
1077
1078	        let conn = initiator
1079	            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
1080	            .await
1081	            .expect("connect");
1082	        let mut init_subnets = SubnetStore::default();
1083	        let mut init_roster = RosterStore::default();
1084	        let init_result = run_initiator(
1085	            &conn,
1086	            init_pub,
1087	            "home",
1088	            "000000",
1089	            &mut init_subnets,
1090	            &mut init_roster,
1091	            NOW,
1092	            0,    // step_back: the normal attempt
1093	            None, // intro: pre-M8 joiner shape
1094	        )
1095	        .await;
1096	        conn.close(0u32.into(), b"done");
1097
1098	        let (resp_trust, resp_errored, retry) = resp_task.await.expect("responder task");
1099
1100	        assert!(
1101	            matches!(init_result, Err(PairWireError::Pairing(_))),
1102	            "initiator rejects"
1103	        );
1104	        assert!(resp_errored, "responder rejects");
1105	        assert!(init_trust.peers.is_empty(), "no trust on initiator");
1106	        assert!(resp_trust.peers.is_empty(), "no trust on responder");
1107	        // [unit->REQ-PAIR-5] a failed ceremony transfers NO seed (the joiner is not a member).
1108	        assert!(
1109	            init_subnets.subnets.is_empty(),
1110	            "no seed on a failed pairing"
1111	        );
1112	        assert!(
1113	            matches!(retry, Err(RateLimited::Backoff { .. })),
1114	            "a wrong-code ceremony is charged a failure"
1115	        );
1116	    }
1117
1118	    // [unit->REQ-PAIR-1] BUSY: a ceremony is refused while another holds the subnet's
1119	    // one-at-a-time slot — the wire surfaces the rate limiter's serialization (#11) to
1120	    // the joiner as a refusal, and no trust is written.
1121	    #[tokio::test]
1122	    async fn busy_subnet_refuses_ceremony() {
1123	        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
1124	        let resp_pub = resp_id.public_key();
1125	        let init_pub = init_id.public_key();
1126	        let seed = [3u8; 20];
1127	        let code = code_for(seed);
1128	        let resp_addr = responder.addr();
1129
1130	        let resp_task = tokio::spawn(async move {
1131	            let incoming = responder.accept().await.expect("incoming");
1132	            let conn = incoming
1133	                .accept()
1134	                .expect("accept")
1135	                .await
1136	                .expect("connecting");
1137	            let subnets = subnet_store("home", seed, 1);
1138	            let mut rate = PairingRateLimiter::new();
1139	            // A different ceremony already holds the slot.
1140	            rate.begin("home", NOW).expect("pre-occupy slot");
1141	            let result = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, NOW).await;
1142	            conn.closed().await;
1143	            (trust, result)
1144	        });
1145
1146	        let conn = initiator
1147	            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
1148	            .await
1149	            .expect("connect");
1150	        let mut init_subnets = SubnetStore::default();
1151	        let mut init_roster = RosterStore::default();
1152	        let init_result = run_initiator(
1153	            &conn,
1154	            init_pub,
1155	            "home",
1156	            &code,
1157	            &mut init_subnets,
1158	            &mut init_roster,
1159	            NOW,
1160	            0,    // step_back: the normal attempt
1161	            None, // intro: pre-M8 joiner shape
1162	        )
1163	        .await;
1164	        conn.close(0u32.into(), b"done");
1165
1166	        let (resp_trust, resp_result) = resp_task.await.expect("responder task");
1167
1168	        assert!(
1169	            matches!(init_result, Err(PairWireError::Refused(RateLimited::Busy))),
1170	            "joiner sees Busy, got {init_result:?}"
1171	        );
1172	        assert!(matches!(
1173	            resp_result,
1174	            Err(PairWireError::Refused(RateLimited::Busy))
1175	        ));
1176	        assert!(init_trust.peers.is_empty());
1177	        assert!(resp_trust.peers.is_empty());
1178	    }
1179
1180	    // [unit->REQ-PAIR-1] STALE STEP: a dialer whose clock is >1 TOTP window off is
1181	    // rejected before the PAKE — the ±1 skew tolerance enforced on the wire.
1182	    #[tokio::test]
1183	    async fn stale_step_is_rejected() {
1184	        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
1185	        let resp_pub = resp_id.public_key();
1186	        let init_pub = init_id.public_key();
1187	        let seed = [5u8; 20];
1188	        let resp_addr = responder.addr();
1189	        // The initiator's clock is 5 TOTP periods ahead of the responder's.

    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the
    // seed-derived one → key confirmation fails on the wire. Neither side writes trust,
    // and the responder charges the failure (so an immediate retry is backed off) — the
    // negative path that proves the code actually authenticates the channel.
    #[tokio::test]
    async fn wrong_code_fails_and_writes_no_trust() {
        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
        let resp_pub = resp_id.public_key();
        let init_pub = init_id.public_key();
        let seed = [9u8; 20];
        let resp_addr = responder.addr();

        let resp_task = tokio::spawn(async move {
            let incoming = responder.accept().await.expect("incoming");
            let conn = incoming
                .accept()
                .expect("accept")
                .await
                .expect("connecting");
            let subnets = subnet_store("home", seed, 1);
            let mut rate = PairingRateLimiter::new();
            let result = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, NOW).await;
            conn.closed().await;
            // After a failed ceremony the slot is backed off (charged a failure).
            let retry = rate.begin("home", NOW);
            (trust, result.is_err(), retry)
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        let init_result = run_initiator(
            &conn,
            init_pub,
            "home",
            "000000",
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await;
        conn.close(0u32.into(), b"done");

        let (resp_trust, resp_errored, retry) = resp_task.await.expect("responder task");

        assert!(
            matches!(init_result, Err(PairWireError::Pairing(_))),
            "initiator rejects"
        );
        assert!(resp_errored, "responder rejects");
        assert!(init_trust.peers.is_empty(), "no trust on initiator");
        assert!(resp_trust.peers.is_empty(), "no trust on responder");
        // [unit->REQ-PAIR-5] a failed ceremony transfers NO seed (the joiner is not a member).
        assert!(
            init_subnets.subnets.is_empty(),
            "no seed on a failed pairing"
        );
        assert!(
            matches!(retry, Err(RateLimited::Backoff { .. })),
            "a wrong-code ceremony is charged a failure"
        );
    }

    // [unit->REQ-PAIR-1] BUSY: a ceremony is refused while another holds the subnet's
    // one-at-a-time slot — the wire surfaces the rate limiter's serialization (#11) to
    // the joiner as a refusal, and no trust is written.
    #[tokio::test]
    async fn busy_subnet_refuses_ceremony() {
        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
        let resp_pub = resp_id.public_key();
        let init_pub = init_id.public_key();
        let seed = [3u8; 20];
        let code = code_for(seed);
        let resp_addr = responder.addr();

        let resp_task = tokio::spawn(async move {
            let incoming = responder.accept().await.expect("incoming");
            let conn = incoming
                .accept()
                .expect("accept")
                .await
                .expect("connecting");
            let subnets = subnet_store("home", seed, 1);
            let mut rate = PairingRateLimiter::new();
            // A different ceremony already holds the slot.
            rate.begin("home", NOW).expect("pre-occupy slot");
            let result = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, NOW).await;
            conn.closed().await;
            (trust, result)
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        let init_result = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await;
        conn.close(0u32.into(), b"done");

        let (resp_trust, resp_result) = resp_task.await.expect("responder task");

        assert!(
            matches!(init_result, Err(PairWireError::Refused(RateLimited::Busy))),
            "joiner sees Busy, got {init_result:?}"
        );
        assert!(matches!(
            resp_result,
            Err(PairWireError::Refused(RateLimited::Busy))
        ));
        assert!(init_trust.peers.is_empty());
        assert!(resp_trust.peers.is_empty());
    }

    // [unit->REQ-PAIR-1] STALE STEP: a dialer whose clock is >1 TOTP window off is
    // rejected before the PAKE — the ±1 skew tolerance enforced on the wire.
    #[tokio::test]
    async fn stale_step_is_rejected() {
        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
        let resp_pub = resp_id.public_key();
        let init_pub = init_id.public_key();
        let seed = [5u8; 20];
        let resp_addr = responder.addr();
        // The initiator's clock is 5 TOTP periods ahead of the responder's.


    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the
    // seed-derived one → key confirmation fails on the wire. Neither side writes trust,
    // and the responder charges the failure (so an immediate retry is backed off) — the
    // negative path that proves the code actually authenticates the channel.
    #[tokio::test]
    async fn wrong_code_fails_and_writes_no_trust() {

    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the
    // seed-derived one → key confirmation fails on the wire. Neither side records
    // membership, and the responder charges the failure (so an immediate retry is backed
    // off) — the negative path that proves the code actually authenticates the channel.
    #[tokio::test]
    async fn wrong_code_fails_and_records_no_membership() {


            // After a failed ceremony the slot is backed off (charged a failure).
            let retry = rate.begin("home", NOW);
            (trust, result.is_err(), retry)
        });

            // After a failed ceremony the slot is backed off (charged a failure).
            let retry = rate.begin("home", NOW);
            (result.is_err(), retry)
        });


    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the
    // seed-derived one → key confirmation fails on the wire. Neither side writes trust,
    // and the responder charges the failure (so an immediate retry is backed off) — the
    // negative path that proves the code actually authenticates the channel.
    #[tokio::test]
    async fn wrong_code_fails_and_writes_no_trust() {

    // [unit->REQ-PAIR-1] WRONG CODE: the joiner types a code that doesn't match the
    // seed-derived one → key confirmation fails on the wire. Neither side records
    // membership, and the responder charges the failure (so an immediate retry is backed
    // off) — the negative path that proves the code actually authenticates the channel.
    #[tokio::test]
    async fn wrong_code_fails_and_records_no_membership() {

-    // seed-derived one → key confirmation fails on the wire. Neither side writes trust,

-    // and the responder charges the failure (so an immediate retry is backed off) — the

-    // negative path that proves the code actually authenticates the channel.

+    // seed-derived one → key confirmation fails on the wire. Neither side records

+    // membership, and the responder charges the failure (so an immediate retry is backed

+    // off) — the negative path that proves the code actually authenticates the channel.

-    async fn wrong_code_fails_and_writes_no_trust() {

+    async fn wrong_code_fails_and_records_no_membership() {

let (responder, initiator, resp_id, init_id) = pair_endpoints().await;


            // After a failed ceremony the slot is backed off (charged a failure).
            let retry = rate.begin("home", NOW);
            (trust, result.is_err(), retry)
        });

            // After a failed ceremony the slot is backed off (charged a failure).
            let retry = rate.begin("home", NOW);
            (result.is_err(), retry)
        });

-            (trust, result.is_err(), retry)


        let (resp_trust, resp_errored, retry) = resp_task.await.expect("responder task");

        assert!(
            matches!(init_result, Err(PairWireError::Pairing(_))),
            "initiator rejects"
        );
        assert!(resp_errored, "responder rejects");
        assert!(init_trust.peers.is_empty(), "no trust on initiator");
        assert!(resp_trust.peers.is_empty(), "no trust on responder");
        // [unit->REQ-PAIR-5] a failed ceremony transfers NO seed (the joiner is not a member).

        let (resp_errored, retry) = resp_task.await.expect("responder task");

        assert!(
            matches!(init_result, Err(PairWireError::Pairing(_))),
            "initiator rejects"
        );
        assert!(resp_errored, "responder rejects");
        assert!(
            init_roster.members.is_empty(),
            "no membership adopted on a failed pairing"
        );
        // [unit->REQ-PAIR-5] a failed ceremony transfers NO seed (the joiner is not a member).


        let (resp_trust, resp_errored, retry) = resp_task.await.expect("responder task");

        assert!(
            matches!(init_result, Err(PairWireError::Pairing(_))),
            "initiator rejects"
        );
        assert!(resp_errored, "responder rejects");
        assert!(init_trust.peers.is_empty(), "no trust on initiator");
        assert!(resp_trust.peers.is_empty(), "no trust on responder");
        // [unit->REQ-PAIR-5] a failed ceremony transfers NO seed (the joiner is not a member).

        let (resp_errored, retry) = resp_task.await.expect("responder task");

        assert!(
            matches!(init_result, Err(PairWireError::Pairing(_))),
            "initiator rejects"
        );
        assert!(resp_errored, "responder rejects");
        assert!(
            init_roster.members.is_empty(),
            "no membership adopted on a failed pairing"
        );
        // [unit->REQ-PAIR-5] a failed ceremony transfers NO seed (the joiner is not a member).

-        let (resp_trust, resp_errored, retry) = resp_task.await.expect("responder task");

+        let (resp_errored, retry) = resp_task.await.expect("responder task");

-        assert!(init_trust.peers.is_empty(), "no trust on initiator");

-        assert!(resp_trust.peers.is_empty(), "no trust on responder");

+            init_roster.members.is_empty(),

+            "no membership adopted on a failed pairing"


pairhost.rs: drop trust write

propagate.rs serve gate trust->roster

peerloop.rs THE WIDEN

CLI cli.rs repoint to roster

xtask + misc comment refs

Migrate daemon tests trust->roster

mesh.rs: un-ignore + regression assert


Bash

cd "C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing" 2>/dev/null; python - <<'PY'
p=r'C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs'
s=open(p,encoding='utf-8').read();b=s
s=s.replace('            (trust, result)\n        });','            result\n        });')
s=s.replace('let (resp_trust, resp_result) = resp_task.await.expect("responder task");','let resp_result = resp_task.await.expect("responder task");')
s=s.replace('        assert!(init_trust.peers.is_empty());\n        assert!(resp_trust.peers.is_empty());\n','        assert!(init_roster.members.is_empty(), "no membership adopted");\n')
s=s.replace('        assert!(matches!(resp_result, Err(PairWireError::StepRejected)));\n        assert!(resp_trust.peers.is_empty());','        assert!(matches!(resp_result, Err(PairWireError::StepRejected)));')
s=s.replace('        assert!(matches!(resp_result, Err(PairWireError::UnknownSubnet(_))));\n        assert!(resp_trust.peers.is_empty());','        assert!(matches!(resp_result, Err(PairWireError::UnknownSubnet(_))));')
open(p,'w',encoding='utf-8').write(s)
print('changed' if s!=b else 'NOCHANGE')
PY

Fix busy/stale/unknown test returns + asserts


changed


1360	    async fn repair_member_keeps_seed() {
1361	        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
1362	        let resp_pub = resp_id.public_key();
1363	        let init_pub = init_id.public_key();
1364	        let seed = [4u8; 20];
1365	        let code = code_for(seed);
1366	        let resp_addr = responder.addr();
1367
1368	        let resp_task = tokio::spawn(async move {
1369	            let incoming = responder.accept().await.expect("incoming");
1370	            let conn = incoming
1371	                .accept()
1372	                .expect("accept")
1373	                .await
1374	                .expect("connecting");
1375	            let subnets = subnet_store("home", seed, 2);
1376	            let mut rate = PairingRateLimiter::new();
1377	            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, NOW)
1378	                .await
1379	                .expect("responder pairs");
1380	            conn.closed().await;
1381	            outcome
1382	        });
1383
1384	        // The joiner already trusts the responder AND already holds "home" with a DISTINCT
1385	        // stored seed (so we can prove the re-pair does not overwrite it).
1386	        init_trust.record("home", &resp_pub.to_hex(), None, None);
1387	        let existing_seed = [99u8; 20];
1388	        let mut init_subnets = subnet_store("home", existing_seed, 7);
1389	        let mut init_roster = RosterStore::default();
1390
1391	        let conn = initiator
1392	            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
1393	            .await
1394	            .expect("connect");
1395	        let init_outcome = run_initiator(
1396	            &conn,
1397	            init_pub,
1398	            "home",
1399	            &code,
1400	            &mut init_subnets,
1401	            &mut init_roster,
1402	            NOW,
1403	            0,    // step_back: the normal attempt
1404	            None, // intro: pre-M8 joiner shape
1405	        )
1406	        .await
1407	        .expect("re-pair succeeds");
1408	        conn.close(0u32.into(), b"done");
1409	        let resp_outcome = resp_task.await.expect("responder task");
1410
1411	        assert_eq!(
1412	            init_outcome,
1413	            TrustOutcome::AlreadyTrusted,
1414	            "idempotent trust"
1415	        );
1416	        assert_eq!(
1417	            resp_outcome.trust,
1418	            TrustOutcome::Pinned,
1419	            "responder's first sight of joiner"
1420	        );
1421	        // The pre-existing seed record is untouched (Exists ignored, not overwritten).
1422	        let kept = init_subnets.find("home").expect("still a member");
1423	        assert_eq!(kept.seed_bytes(), Some(existing_seed), "existing seed kept");
1424	        assert_eq!(kept.epoch, 7, "existing epoch kept");
1425	    }
1426
1427	    // [unit->REQ-SUBNET-7] the Hello intro is additive on the wire: absent
1428	    // decodes None (the pre-M8 joiner), present round-trips both fields,
1429	    // and a HALF-written intro is a protocol error — never a half-claim.
1430	    #[test]
1431	    fn hello_intro_is_additive_and_roundtrips() {
1432	        // Absent → None.
1433	        match decode_frame(&enc_hello("home", 7, None)).expect("decodes") {
1434	            Frame::Hello { intro, .. } => assert!(intro.is_none()),
1435	            _ => panic!("expected Hello"),
1436	        }
1437	        // Present → both fields intact.
1438	        let sent = NodeIntro {
1439	            label: "KITSUBITO".into(),
1440	            machine_id: "cd".repeat(32),
1441	        };
1442	        match decode_frame(&enc_hello("home", 7, Some(&sent))).expect("decodes") {
1443	            Frame::Hello {
1444	                subnet_name,
1445	                step,
1446	                intro,
1447	            } => {
1448	                assert_eq!(subnet_name, "home");
1449	                assert_eq!(step, 7);
1450	                assert_eq!(intro, Some(sent));
1451	            }
1452	            _ => panic!("expected Hello"),
1453	        }
1454	        // Half an intro (label field only) refuses, never half-claims.
1455	        let mut half = vec![T_HELLO];
1456	        push_field(&mut half, b"home");
1457	        half.extend_from_slice(&7u64.to_be_bytes());
1458	        push_field(&mut half, b"KITSUBITO");
1459	        assert!(matches!(
1460	            decode_frame(&half),
1461	            Err(PairWireError::Protocol(_))
1462	        ));
1463	    }
1464
1465	    // [unit->REQ-PAIR-1] the frame codec round-trips every variant and rejects a
1466	    // truncated body without panicking (the codec is pre-trust attacker-reachable).
1467	    #[test]
1468	    fn frame_codec_round_trips_and_rejects_garbage() {
1469	        let cases = [

    async fn repair_member_keeps_seed() {
        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
        let resp_pub = resp_id.public_key();
        let init_pub = init_id.public_key();
        let seed = [4u8; 20];
        let code = code_for(seed);
        let resp_addr = responder.addr();

        let resp_task = tokio::spawn(async move {
            let incoming = responder.accept().await.expect("incoming");
            let conn = incoming
                .accept()
                .expect("accept")
                .await
                .expect("connecting");
            let subnets = subnet_store("home", seed, 2);
            let mut rate = PairingRateLimiter::new();
            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            outcome
        });

        // The joiner already trusts the responder AND already holds "home" with a DISTINCT
        // stored seed (so we can prove the re-pair does not overwrite it).
        init_trust.record("home", &resp_pub.to_hex(), None, None);
        let existing_seed = [99u8; 20];
        let mut init_subnets = subnet_store("home", existing_seed, 7);
        let mut init_roster = RosterStore::default();

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("re-pair succeeds");
        conn.close(0u32.into(), b"done");
        let resp_outcome = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::AlreadyTrusted,
            "idempotent trust"
        );
        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );
        // The pre-existing seed record is untouched (Exists ignored, not overwritten).
        let kept = init_subnets.find("home").expect("still a member");
        assert_eq!(kept.seed_bytes(), Some(existing_seed), "existing seed kept");
        assert_eq!(kept.epoch, 7, "existing epoch kept");
    }

    // [unit->REQ-SUBNET-7] the Hello intro is additive on the wire: absent
    // decodes None (the pre-M8 joiner), present round-trips both fields,
    // and a HALF-written intro is a protocol error — never a half-claim.
    #[test]
    fn hello_intro_is_additive_and_roundtrips() {
        // Absent → None.
        match decode_frame(&enc_hello("home", 7, None)).expect("decodes") {
            Frame::Hello { intro, .. } => assert!(intro.is_none()),
            _ => panic!("expected Hello"),
        }
        // Present → both fields intact.
        let sent = NodeIntro {
            label: "KITSUBITO".into(),
            machine_id: "cd".repeat(32),
        };
        match decode_frame(&enc_hello("home", 7, Some(&sent))).expect("decodes") {
            Frame::Hello {
                subnet_name,
                step,
                intro,
            } => {
                assert_eq!(subnet_name, "home");
                assert_eq!(step, 7);
                assert_eq!(intro, Some(sent));
            }
            _ => panic!("expected Hello"),
        }
        // Half an intro (label field only) refuses, never half-claims.
        let mut half = vec![T_HELLO];
        push_field(&mut half, b"home");
        half.extend_from_slice(&7u64.to_be_bytes());
        push_field(&mut half, b"KITSUBITO");
        assert!(matches!(
            decode_frame(&half),
            Err(PairWireError::Protocol(_))
        ));
    }

    // [unit->REQ-PAIR-1] the frame codec round-trips every variant and rejects a
    // truncated body without panicking (the codec is pre-trust attacker-reachable).
    #[test]
    fn frame_codec_round_trips_and_rejects_garbage() {
        let cases = [


Edit

        // The joiner already trusts the responder AND already holds "home" with a DISTINCT
        // stored seed (so we can prove the re-pair does not overwrite it).
        init_trust.record("home", &resp_pub.to_hex(), None, None);
        let existing_seed = [99u8; 20];
        let mut init_subnets = subnet_store("home", existing_seed, 7);
        let mut init_roster = RosterStore::default();

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("re-pair succeeds");
        conn.close(0u32.into(), b"done");
        let resp_outcome = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::AlreadyTrusted,
            "idempotent trust"
        );
        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );
        // The pre-existing seed record is untouched (Exists ignored, not overwritten).

        // The joiner already holds "home" with a DISTINCT stored seed (so we can prove
        // the re-pair does not overwrite it).
        let existing_seed = [99u8; 20];
        let mut init_subnets = subnet_store("home", existing_seed, 7);
        let mut init_roster = RosterStore::default();

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("re-pair succeeds");
        conn.close(0u32.into(), b"done");
        let resp_outcome = resp_task.await.expect("responder task");

        // The ceremony proved the joiner's identity (the caller would record membership).
        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
        // The pre-existing seed record is untouched (Exists ignored, not overwritten).


        // The joiner already trusts the responder AND already holds "home" with a DISTINCT
        // stored seed (so we can prove the re-pair does not overwrite it).
        init_trust.record("home", &resp_pub.to_hex(), None, None);
        let existing_seed = [99u8; 20];
        let mut init_subnets = subnet_store("home", existing_seed, 7);
        let mut init_roster = RosterStore::default();

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("re-pair succeeds");
        conn.close(0u32.into(), b"done");
        let resp_outcome = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::AlreadyTrusted,
            "idempotent trust"
        );
        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );
        // The pre-existing seed record is untouched (Exists ignored, not overwritten).

        // The joiner already holds "home" with a DISTINCT stored seed (so we can prove
        // the re-pair does not overwrite it).
        let existing_seed = [99u8; 20];
        let mut init_subnets = subnet_store("home", existing_seed, 7);
        let mut init_roster = RosterStore::default();

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("re-pair succeeds");
        conn.close(0u32.into(), b"done");
        let resp_outcome = resp_task.await.expect("responder task");

        // The ceremony proved the joiner's identity (the caller would record membership).
        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
        // The pre-existing seed record is untouched (Exists ignored, not overwritten).

-        // The joiner already trusts the responder AND already holds "home" with a DISTINCT

-        // stored seed (so we can prove the re-pair does not overwrite it).

-        init_trust.record("home", &resp_pub.to_hex(), None, None);

+        // The joiner already holds "home" with a DISTINCT stored seed (so we can prove

+        // the re-pair does not overwrite it).

-        let init_outcome = run_initiator(

-            TrustOutcome::AlreadyTrusted,

-            "responder's first sight of joiner"

+        // The ceremony proved the joiner's identity (the caller would record membership).

+        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());

let kept = init_subnets.find("home").expect("still a member");

assert_eq!(kept.seed_bytes(), Some(existing_seed), "existing seed kept");


1352	        assert_eq!(joined.seed_bytes(), Some(seed));
1353	        assert_eq!(joined.epoch, 1);
1354	    }
1355
1356	    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet (and
1357	    // already trusts the peer) re-pairs cleanly — the outcome is AlreadyTrusted and the
1358	    // existing seed record is KEPT, not overwritten (add_joined's Exists is ignored).
1359	    #[tokio::test]

        assert_eq!(joined.seed_bytes(), Some(seed));
        assert_eq!(joined.epoch, 1);
    }

    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet (and
    // already trusts the peer) re-pairs cleanly — the outcome is AlreadyTrusted and the
    // existing seed record is KEPT, not overwritten (add_joined's Exists is ignored).
    #[tokio::test]


    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet (and
    // already trusts the peer) re-pairs cleanly — the outcome is AlreadyTrusted and the
    // existing seed record is KEPT, not overwritten (add_joined's Exists is ignored).

    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet
    // re-pairs cleanly — the existing seed record is KEPT, not overwritten (add_joined's
    // Exists is ignored).


    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet (and
    // already trusts the peer) re-pairs cleanly — the outcome is AlreadyTrusted and the
    // existing seed record is KEPT, not overwritten (add_joined's Exists is ignored).

    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet
    // re-pairs cleanly — the existing seed record is KEPT, not overwritten (add_joined's
    // Exists is ignored).

-    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet (and

-    // already trusts the peer) re-pairs cleanly — the outcome is AlreadyTrusted and the

-    // existing seed record is KEPT, not overwritten (add_joined's Exists is ignored).

+    // [unit->REQ-PAIR-5] IDEMPOTENT RE-PAIR: a node that already holds the subnet

+    // re-pairs cleanly — the existing seed record is KEPT, not overwritten (add_joined's

let (responder, initiator, resp_id, init_id) = pair_endpoints().await;


Now build spt-net to confirm wire.rs is clean.


Bash

cargo build -p spt-net 2>&1 | tail -25; echo "===EXIT:${PIPESTATUS[0]}==="

Build spt-net


   Compiling spt-store v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.07s
===EXIT:0===


cargo test -p spt-net --lib pairing::wire 2>&1 | tail -25; echo "===EXIT:${PIPESTATUS[0]}==="

Run wire.rs unit tests


   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 7.31s
     Running unittests src\lib.rs (C:\Users\decid\Documents\projects\spt-core\target\debug\deps\spt_net-b028b09ce8c0cc73.exe)

running 10 tests
test net::pairing::wire::tests::hello_intro_is_additive_and_roundtrips ... ok
test net::pairing::wire::tests::frame_codec_round_trips_and_rejects_garbage ... ok
test net::pairing::wire::tests::seed_frame_roster_is_additive_and_roundtrips ... ok
test net::pairing::wire::tests::unknown_subnet_is_rejected ... ok
test net::pairing::wire::tests::busy_subnet_refuses_ceremony ... ok
test net::pairing::wire::tests::stale_step_is_rejected ... ok
test net::pairing::wire::tests::wrong_code_fails_and_records_no_membership ... ok
test net::pairing::wire::tests::repair_member_keeps_seed ... ok
test net::pairing::wire::tests::create_new_then_join_learns_named_subnet ... ok
test net::pairing::wire::tests::loopback_pairs_and_transfers_seed_and_roster ... ok

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

===EXIT:0===


Tasks 1,2 done. Read req definitions before the daemon-layer edits (warn-on-change demotion is REQ-MESH-5).


grep -n -A12 'REQ-MESH-3\|REQ-MESH-5\|REQ-SUBNET-7' "C:/Users/decid/Documents/projects/spt-core/traceable-reqs.toml" | head -90

Read MESH req definitions


710:id = "REQ-SUBNET-7"
711-title = "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)"
712-required_stages = ["impl", "unit"]  # activated M8-D2: spt_daemon::machineid (OS id → domain-separated sha256, minted-fallback chain; raw never leaves the machine) + Instance.machine_id additive field stamped by advertise_local + the Hello NodeIntro wire extension (trailing additive fields; old responder ignores, half-intro refuses) + ResponderOutcome carrying the ceremony facts + pairhost post-confirm eviction via registryhost::{superseded_identities, repair_evict_superseded} (ceremony-subnet-scoped trust drop + disk snapshot rewrite + repair-evict marker) + RegistryHost::consume_repair_evictions on the pump's registry tick (in-memory rows + heard entry go before the snapshot mirror could resurrect them; peer-side replication = the superseded node's silence under the 4.10 eviction + the fresh identity's rows). unit = hash domain-separation/stability + intro wire roundtrip/half-refusal + superseded both-anchors decision + evict trust/snapshot/memory/marker-once + pre-M8 row serde. Acceptance 7 = the rig re-pair proof
713-[[requirements]]
714-id = "REQ-SUBNET-8"
715-title = "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)"
716-required_stages = ["impl", "unit"]  # activated M8-D2: standalone_text(daemon_running) keyed on the live socket ping; hints flag threaded from the dispatch (bare subnet = true, explicit status = false); pump_stall_warning over read_pump_heartbeat (3-cadence staleness; absent file = pre-D4, no false alarm) appended to both status views. unit = daemon-up/down standalone variants + hints-only-on-bare + bare-vs-status dispatch
717-[[requirements]]
718-id = "REQ-INSTALL-6"
719-title = "Linux elevation install leg: install.sh symlinks the binary into a sudo-reachable path (/usr/local/bin; graceful print-the-one-liner when unelevated) so sudo spt resolves; first sudo spt detects elevation and prompts ONCE for the default user account — thereafter any elevated daemon launch runs daemon + state under that account, never root (KH 5.7 interplay verified) (M8 decision 8)"
720-required_stages = ["impl", "unit"]  # activated M8-D3: install.sh symlink leg (writable /usr/local/bin links, else prints the exact sudo one-liner; SPT_INSTALL_NO_SYMLINK gates CI) + deelevate::daemon_target_user election ladder (/etc/spt-core/default-user wins → sudo invoker, electing it on first use: interactive prompt with the invoker default, loud announce otherwise; root never electable — KH 5.7) consumed by BOTH elevated daemon paths (spawn_detached + the Daemon::run entry guard). unit = election_resolves_real_users_never_root (missing/garbage/root/real-account ladder). Live sudo flow = acceptance 1 (kitsubito)
721-[[requirements]]
722-id = "REQ-INSTALL-7"
--
747:title = "Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)"
748:required_stages = []  # rule 5 + M8 decision 24: documented hazard (KNOWN-HAZARDS 4.11 at D5), guard waits for a field hit of the narrow slice; the re-pair eviction path is tested under REQ-SUBNET-7
749-
750-# ── Subnet full-mesh: membership seed-proof + roster-only relay (ADR-0017, 2026-06-08 grill) ──
751-# Minted inactive (rule 5: no required_stages until the mesh milestone executes). Design: ADR-0017 + SUBNET-MESH-PLAN.md + CONTEXT §Pairing & trust.
752-[[requirements]]
753-id = "REQ-MESH-1"
754-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)."
755-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.
756-[[requirements]]
757-id = "REQ-MESH-2"
758-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)."
759-required_stages = ["impl", "unit", "int"]  # Mesh-D3 activated impl+unit: roster store (RosterStore union-merge + strictly-greater lease, tombstone dominate/suppress-reinsert/clear-on-repair, atomic, corrupt-degrades-empty) + pairing-seed full-roster transfer (Frame::Seed gains the roster, additive) + ceremony adopt-on-join wiring (pairhost both legs). Mesh-D4 enriches impl (on-connect roster exchange riding the proof control stream after both prove, REQ-MESH-2's "exchanged only over seed-proof'd member connections" by construction; self-address advertised from endpoint.addr(); peer-addrs gap-fill seam — roster feeds the dial cache only when absent, never clobbering a locally-observed addr) and activates int. unit = merge lease (strictly-greater self-entry) + union convergence (commutative/idempotent) + tombstone truth table + persist-through-silence + serde additive + extended-Seed codec round-trip + loopback ceremony roster transfer + member-conn roster frame round-trip/reject + gap-fill fills-absent-only/skips-self. int = on-connect transitive propagation (B knows offline C; A connects B; A learns C + C's address; B learns A) + no cross-subnet leak (only proven subnets exchanged).
760-[[requirements]]
761:id = "REQ-MESH-3"
762-title = "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."
763-required_stages = []  # activate at mesh execution. impl = peerloop/advertise push target = roster members; remove the directly-paired-peers restriction. unit/int = the staggered A→B→C convergence harness is the acceptance (B offline at the critical step; assert A↔C converge); all-online star (A reaches C without B relaying). Regression: no row is ever relayed (author == handshake origin on every applied row).
764-[[requirements]]
765-id = "REQ-MESH-4"
766-title = "Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair."
767-required_stages = []  # activate at mesh execution. impl = revoke CLI (list) + tombstone+window scheduler (coalesce, --force-rotate-seed) + confidential seed push over member conns + force-drop sweep + re-seed-only restricted handshake (prior-epoch ∧ roster-gated). unit = window coalescing (N revokes→1 bump) + re-seed grants benign-N-1-on-roster / denies revoked-off-roster / denies N-2 + seed-never-in-gossip + force-rotate skips window. int = revoke-then-offliner-returns auto-heals on the rig.
768-[[requirements]]
769:id = "REQ-MESH-5"
770:title = "Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead."
771-required_stages = []  # activate at mesh execution. impl = remove TrustStore authorization use + peers.json (RegistryGatePolicy.trust → membership); re-home warn-on-change onto machine_id as a notice (notif, not a gate). unit = no is_trusted reference remains in the gate path; machine_id-change emits the notice without blocking; absent machine_id → no false notice.
772-[[requirements]]
773-id = "REQ-MESH-6"
774-title = "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.)"
775-required_stages = []  # activate at mesh execution. impl = join_all/futures fan-out of the serve-probes under one bound, not a sequential await loop. unit = N offline nodes complete within ~one ceiling, not N×ceiling (deterministic mock-clock seam).


REQ-MESH-5 = demoted warn-on-change anchored on machine_id at the eviction point. Implementing in registryhost. Remove trust import + rework `repair_evict_superseded`.


38	use spt_store::epoch::EpochSource;
39	use spt_store::liveness::is_perch_alive;
40	use spt_store::perch::{self, ParentHint};
41	use spt_store::roster::RosterStore;
42	use spt_store::subnet::SubnetStore;
43	use spt_store::trust::TrustStore;
44	use spt_store::visibility::VisibilityStore;
45
46	/// What the host did with one inbound registry record — loud + typed.
47	#[derive(Debug, Clone, PartialEq, Eq)]

use spt_store::epoch::EpochSource;
use spt_store::liveness::is_perch_alive;
use spt_store::perch::{self, ParentHint};
use spt_store::roster::RosterStore;
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use spt_store::visibility::VisibilityStore;

/// What the host did with one inbound registry record — loud + typed.
#[derive(Debug, Clone, PartialEq, Eq)]


Edit

use spt_store::roster::RosterStore;
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use spt_store::visibility::VisibilityStore;

use spt_store::roster::RosterStore;
use spt_store::subnet::SubnetStore;
use spt_store::visibility::VisibilityStore;


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

use spt_store::roster::RosterStore;
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use spt_store::visibility::VisibilityStore;

use spt_store::roster::RosterStore;
use spt_store::subnet::SubnetStore;
use spt_store::visibility::VisibilityStore;

/// What the host did with one inbound registry record — loud + typed.